From 39b2017f9c4aa2142fcd5f03eb718f5019456085 Mon Sep 17 00:00:00 2001 From: tianma-if Date: Sun, 19 Jul 2026 03:51:42 +0800 Subject: [PATCH] feat(mobile): align settings with PWA mobile flow --- apps/mobile/src/lib/mobile-locale.tsx | 1 + .../src/screens/AccountSecurityModal.tsx | 280 +++++++------ apps/mobile/src/screens/WorkspaceScreen.tsx | 372 ++++++++++-------- 3 files changed, 352 insertions(+), 301 deletions(-) diff --git a/apps/mobile/src/lib/mobile-locale.tsx b/apps/mobile/src/lib/mobile-locale.tsx index 79ae34cb..b03f9569 100644 --- a/apps/mobile/src/lib/mobile-locale.tsx +++ b/apps/mobile/src/lib/mobile-locale.tsx @@ -33,6 +33,7 @@ const mobileOnlyTranslations = new Map([ ["笔记标题", "Note title"], ["笔记标签", "Note tags"], ["选择笔记本", "Choose notebook"], + ["刷新 Token", "Refresh tokens"], ["没有正文预览", "No content preview"], ["原生运行时启动", "Native runtime startup"], ["启动至 JS 执行", "Launch to JavaScript execution"], diff --git a/apps/mobile/src/screens/AccountSecurityModal.tsx b/apps/mobile/src/screens/AccountSecurityModal.tsx index 8e669a3f..491ddbd1 100644 --- a/apps/mobile/src/screens/AccountSecurityModal.tsx +++ b/apps/mobile/src/screens/AccountSecurityModal.tsx @@ -1,39 +1,34 @@ import { ApiRequestError } from "@edgeever/client"; import type { AuthUser } from "@edgeever/shared"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { KeyRound, Plus, UserRound, Users, X } from "../components/icons"; -import { useEffect, useState, type ReactNode } from "react"; +import { KeyRound, Plus, UserRound } from "../components/icons"; +import { useEffect, useState } from "react"; import { ActivityIndicator, Modal, - ScrollView, StyleSheet, Switch, View, } from "react-native"; -import { SafeAreaView } from "react-native-safe-area-context"; import { Alert, Pressable, Text, TextInput } from "../components/LocalizedText"; import { resolveMobileThemeStyles, useMobileTheme, type MobileResolvedTheme } from "../lib/mobile-theme"; import { useSession } from "../lib/session"; -type Section = "password" | "users"; +export type AccountSecuritySection = "password" | "users"; -export const AccountSecurityModal = ({ +export const AccountSecurityPanel = ({ + active, currentUser, - initialSection = "password", - onClose, - visible, + section, }: { + active: boolean; currentUser: AuthUser | null; - initialSection?: Section; - onClose: () => void; - visible: boolean; + section: AccountSecuritySection; }) => { const { resolvedTheme } = useMobileTheme(); refreshAccountSecurityThemeStyles(resolvedTheme); const { client } = useSession(); const queryClient = useQueryClient(); - const [section, setSection] = useState
("password"); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); @@ -50,7 +45,7 @@ export const AccountSecurityModal = ({ if (!client) throw new Error("Client is not ready"); return client.listUsers(); }, - enabled: Boolean(client && visible && currentUser?.role === "owner" && section === "users"), + enabled: Boolean(client && active && currentUser?.role === "owner" && section === "users"), }); const passwordMutation = useMutation({ @@ -92,140 +87,150 @@ export const AccountSecurityModal = ({ }); useEffect(() => { - if (visible) { - setSection(initialSection); - } else { - setSection("password"); + if (!active) { passwordMutation.reset(); createUserMutation.reset(); updateUserMutation.reset(); } - }, [initialSection, visible]); + }, [active]); const errorMessage = (error: unknown) => { if (error instanceof ApiRequestError && error.code === "invalid_current_password") return "当前密码不正确"; if (error instanceof ApiRequestError && error.code === "username_exists") return "该用户名已经存在"; return error instanceof Error ? error.message : "操作失败,请稍后再试"; }; + const resetUser = usersQuery.data?.users.find((user) => user.id === resetUserId) ?? null; + const closeCreateDialog = () => { + setCreateOpen(false); + setUsername(""); + setDisplayName(""); + setMemberPassword(""); + createUserMutation.reset(); + }; + const closeResetDialog = () => { + setResetUserId(null); + setResetPasswordValue(""); + updateUserMutation.reset(); + }; - return ( - - - - - - - 账户与安全 - + return section === "password" ? ( + + + + + 修改密码 + 修改后会保留当前设备登录,并退出其他设备上的登录会话。 + + + + + {passwordMutation.error ? {errorMessage(passwordMutation.error)} : null} + passwordMutation.mutate()} + /> + + ) : ( + + + + 成员管理 + 为家人或团队成员创建独立的个人笔记空间。实例不开放公开注册。 + + setCreateOpen(true)} style={styles.addButton}> + + 添加成员 + + - {currentUser?.role === "owner" ? ( - - } label="修改密码" onPress={() => setSection("password")} /> - } label="用户管理" onPress={() => setSection("users")} /> - - ) : null} - - {section === "password" ? ( - - - - - 修改登录密码 - 密码更新后,现有 App 会话继续有效。 - + {usersQuery.isLoading ? : null} + {usersQuery.error ? {errorMessage(usersQuery.error)} : null} + {usersQuery.data?.users.map((user) => ( + + + + + {user.displayName || user.username} + @{user.username} · {user.role === "owner" ? "实例管理员" : user.isDisabled ? "已停用" : "已启用"} + { setResetUserId(user.id); setResetPasswordValue(""); }}>重置密码 - - - - {passwordMutation.error ? {errorMessage(passwordMutation.error)} : null} - passwordMutation.mutate()} - /> - - ) : ( - - - - 实例用户 - 每个用户拥有隔离的笔记空间。 - - setCreateOpen((value) => !value)} style={styles.addButton}> - - 添加 - - - - {createOpen ? ( - - - - - {createUserMutation.error ? {errorMessage(createUserMutation.error)} : null} - createUserMutation.mutate()} - /> - + {user.role !== "owner" ? ( + updateUserMutation.mutate({ userId: user.id, input: { isDisabled: !enabled } })} + value={!user.isDisabled} + /> ) : null} - - {usersQuery.isLoading ? : null} - {usersQuery.error ? {errorMessage(usersQuery.error)} : null} - {usersQuery.data?.users.map((user) => ( - - - - - {user.displayName || user.username} - @{user.username} · {user.role === "owner" ? "所有者" : user.isDisabled ? "已停用" : "已启用"} - { setResetUserId(user.id); setResetPasswordValue(""); }}>重置密码 - - {user.role !== "owner" ? ( - updateUserMutation.mutate({ userId: user.id, input: { isDisabled: !enabled } })} - value={!user.isDisabled} - /> - ) : null} - - {resetUserId === user.id ? ( - - - - setResetUserId(null)}>取消 - updateUserMutation.mutate( - { userId: user.id, input: { password: resetPasswordValue } }, - { onSuccess: () => { setResetUserId(null); setResetPasswordValue(""); } } - )} - /> - - - ) : null} - - ))} - - )} - - + + + ))} + + + + 添加新成员 + 新账号会获得完全独立的笔记、附件、回收站和 MCP Token。 + + + + {createUserMutation.error ? {errorMessage(createUserMutation.error)} : null} + + 取消 + createUserMutation.mutate()} + /> + + + + + + + + {`重置 ${resetUser?.username ?? ""} 的密码`} + 重置后,该账号在其他设备上的登录会话将失效。 + + + 取消 + resetUser && updateUserMutation.mutate( + { userId: resetUser.id, input: { password: resetPasswordValue } }, + { onSuccess: closeResetDialog }, + )} + /> + + + + + ); }; -const Tab = ({ active, icon, label, onPress }: { active: boolean; icon: ReactNode; label: string; onPress: () => void }) => ( - - {icon}{label} - -); - -const Field = ({ label, onChangeText, secure = true, value }: { label: string; onChangeText: (value: string) => void; secure?: boolean; value: string }) => ( +const Field = ({ help, label, onChangeText, placeholder, secure = true, value }: { + help?: string; + label: string; + onChangeText: (value: string) => void; + placeholder?: string; + secure?: boolean; + value: string; +}) => ( {label} - + + {help ? {help} : null} ); @@ -236,16 +241,6 @@ const PrimaryButton = ({ disabled, label, onPress }: { disabled: boolean; label: ); const baseAccountSecurityStyles = StyleSheet.create({ - safeArea: { backgroundColor: "#f7faf7", flex: 1 }, - header: { alignItems: "center", borderBottomColor: "#dce7dd", borderBottomWidth: 1, flexDirection: "row", justifyContent: "space-between", padding: 16 }, - title: { color: "#17211a", fontSize: 17, fontWeight: "800" }, - iconButton: { alignItems: "center", height: 40, justifyContent: "center", width: 40 }, - iconPlaceholder: { width: 40 }, - tabs: { flexDirection: "row", gap: 8, padding: 12 }, - tab: { alignItems: "center", backgroundColor: "#ffffff", borderColor: "#dce7dd", borderRadius: 10, borderWidth: 1, flex: 1, flexDirection: "row", gap: 7, justifyContent: "center", padding: 11 }, - tabActive: { backgroundColor: "#15803d", borderColor: "#15803d" }, - tabText: { color: "#475569", fontSize: 13, fontWeight: "700" }, - tabTextActive: { color: "#ffffff" }, content: { gap: 14, padding: 16, paddingBottom: 40 }, hero: { alignItems: "center", backgroundColor: "#ecfdf3", borderRadius: 14, flexDirection: "row", gap: 12, padding: 16 }, sectionHeader: { alignItems: "center", flexDirection: "row", gap: 12 }, @@ -255,17 +250,18 @@ const baseAccountSecurityStyles = StyleSheet.create({ field: { gap: 7 }, label: { color: "#334155", fontSize: 13, fontWeight: "700" }, input: { backgroundColor: "#ffffff", borderColor: "#cad8cc", borderRadius: 10, borderWidth: 1, color: "#17211a", minHeight: 48, paddingHorizontal: 13 }, - primaryButton: { alignItems: "center", backgroundColor: "#15803d", borderRadius: 10, minHeight: 48, justifyContent: "center" }, + primaryButton: { alignItems: "center", backgroundColor: "#15803d", borderRadius: 10, minHeight: 48, justifyContent: "center", paddingHorizontal: 16 }, primaryButtonText: { color: "#ffffff", fontSize: 14, fontWeight: "800" }, disabled: { opacity: 0.45 }, error: { color: "#be123c", fontSize: 13, lineHeight: 19 }, addButton: { alignItems: "center", backgroundColor: "#15803d", borderRadius: 9, flexDirection: "row", gap: 6, paddingHorizontal: 12, paddingVertical: 9 }, addButtonText: { color: "#ffffff", fontSize: 13, fontWeight: "800" }, - createCard: { backgroundColor: "#ffffff", borderColor: "#dce7dd", borderRadius: 14, borderWidth: 1, gap: 13, padding: 14 }, userCard: { alignItems: "center", backgroundColor: "#ffffff", borderColor: "#dce7dd", borderRadius: 14, borderWidth: 1, flexDirection: "row", gap: 12, padding: 14 }, userBlock: { gap: 8 }, - resetCard: { backgroundColor: "#f8fafc", borderColor: "#dce7dd", borderRadius: 12, borderWidth: 1, gap: 10, padding: 12 }, - resetActions: { alignItems: "center", flexDirection: "row", gap: 14, justifyContent: "flex-end" }, + dialogBackdrop: { alignItems: "center", backgroundColor: "rgba(15, 23, 42, 0.48)", flex: 1, justifyContent: "center", padding: 20 }, + dialogCard: { backgroundColor: "#ffffff", borderRadius: 14, gap: 14, maxWidth: 520, padding: 18, width: "100%" }, + dialogActions: { alignItems: "center", flexDirection: "row", gap: 10, justifyContent: "flex-end" }, + cancelButton: { alignItems: "center", borderColor: "#dce7dd", borderRadius: 10, borderWidth: 1, justifyContent: "center", minHeight: 48, paddingHorizontal: 16 }, cancelText: { color: "#64748b", fontSize: 13, fontWeight: "700" }, userIcon: { alignItems: "center", backgroundColor: "#ecfdf3", borderRadius: 20, height: 40, justifyContent: "center", width: 40 }, userName: { color: "#17211a", fontSize: 14, fontWeight: "800" }, diff --git a/apps/mobile/src/screens/WorkspaceScreen.tsx b/apps/mobile/src/screens/WorkspaceScreen.tsx index 8b88671a..c679fdce 100644 --- a/apps/mobile/src/screens/WorkspaceScreen.tsx +++ b/apps/mobile/src/screens/WorkspaceScreen.tsx @@ -76,7 +76,7 @@ import { import { SafeAreaView, useSafeAreaInsets } from "react-native-safe-area-context"; import { Alert, Pressable, Text, TextInput } from "../components/LocalizedText"; import Markdown from "react-native-markdown-display"; -import { createExcerpt, docToMarkdown, docToText, getNotebookDescendantIds, markdownToDoc, type ApiToken, type MemoDetail, type MemoRevision, type MemoSummary, type Notebook, type ResourceListItem, type TagSummary, type TiptapDoc } from "@edgeever/shared"; +import { createExcerpt, docToMarkdown, docToText, getNotebookDescendantIds, markdownToDoc, type ApiToken, type AuthUser, type MemoDetail, type MemoRevision, type MemoSummary, type Notebook, type ResourceListItem, type TagSummary, type TiptapDoc } from "@edgeever/shared"; import { clearMobileMemoDraft, clearMobileNewMemoDraft, readMobileMemoDraft, readMobileNewMemoDraft, writeMobileMemoDraft, writeMobileNewMemoDraft } from "../lib/mobile-drafts"; import { readMobileImageCompressionEnabled, @@ -116,8 +116,8 @@ import { syncMobileLocalMirror, upsertLocalMemo, } from "../lib/local-mirror"; -import { AccountSecurityModal } from "./AccountSecurityModal"; -import { getStartupPerformanceItems, markStartup, recordEditorStartup } from "../lib/startup-performance"; +import { AccountSecurityPanel } from "./AccountSecurityModal"; +import { markStartup, recordEditorStartup } from "../lib/startup-performance"; import LocalTiptapEditor, { type LocalTiptapEditorRef } from "../components/LocalTiptapEditor"; import { resolveMobileThemeStyles, useMobileTheme, type MobileResolvedTheme } from "../lib/mobile-theme"; @@ -339,10 +339,6 @@ export const WorkspaceScreen = () => { const [tagsManagerOpen, setTagsManagerOpen] = useState(false); const [resourcesOpen, setResourcesOpen] = useState(false); const [apiTokensOpen, setApiTokensOpen] = useState(false); - const [advancedPlayOpen, setAdvancedPlayOpen] = useState(false); - const [systemInfoOpen, setSystemInfoOpen] = useState(false); - const [accountSecurityOpen, setAccountSecurityOpen] = useState(false); - const [accountSecuritySection, setAccountSecuritySection] = useState<"password" | "users">("password"); const [syncQueueOpen, setSyncQueueOpen] = useState(false); const [revisionMemo, setRevisionMemo] = useState(null); const [selectionMode, setSelectionMode] = useState(false); @@ -1152,18 +1148,9 @@ export const WorkspaceScreen = () => { {activeView === "account" ? : null} {activeView === "settings" ? ( setAdvancedPlayOpen(true)} + baseUrl={session?.baseUrl ?? ""} + currentUser={session?.user ?? null} onClose={() => setActiveView("notes")} - onOpenPassword={() => { - setAccountSecuritySection("password"); - setAccountSecurityOpen(true); - }} - onOpenUserManagement={() => { - setAccountSecuritySection("users"); - setAccountSecurityOpen(true); - }} - onOpenApiTokens={() => setApiTokensOpen(true)} - onOpenSystemInfo={() => setSystemInfoOpen(true)} localePreference={localePreference} onLocalePreferenceChange={handleLocalePreferenceChange} imageCompressionEnabled={imageCompressionEnabled} @@ -1233,7 +1220,6 @@ export const WorkspaceScreen = () => { {tagsManagerOpen ? setTagsManagerOpen(false)} visible /> : null} {resourcesOpen ? setResourcesOpen(false)} visible /> : null} {apiTokensOpen ? setApiTokensOpen(false)} visible /> : null} - {advancedPlayOpen ? setAdvancedPlayOpen(false)} visible /> : null} {syncQueueOpen ? setSyncQueueOpen(false)} onChanged={async () => setSyncQueueSummary(await loadMobileSyncQueueSummary(syncQueueScope))} @@ -1241,8 +1227,6 @@ export const WorkspaceScreen = () => { scope={syncQueueScope} visible /> : null} - {systemInfoOpen ? setSystemInfoOpen(false)} visible /> : null} - {accountSecurityOpen ? setAccountSecurityOpen(false)} visible /> : null} {revisionMemo ? setRevisionMemo(null)} @@ -1383,9 +1367,10 @@ export const WorkspaceScreen = () => { /> ) : null} - + {activeView !== "settings" ? ( + } @@ -1402,12 +1387,13 @@ export const WorkspaceScreen = () => { } + active={false} + icon={} label="我的" onPress={() => setActiveView("settings")} /> - + + ) : null} ); }; @@ -1945,30 +1931,24 @@ const AccountInfoCopyRow = ({ instance, userName }: { instance: string; userName }; const SettingsView = ({ + baseUrl, + currentUser, imageCompressionEnabled, isOwner, localePreference, onClose, onImageCompressionChange, onLocalePreferenceChange, - onOpenAdvancedPlay, - onOpenApiTokens, - onOpenPassword, - onOpenSystemInfo, - onOpenUserManagement, onSignOut, }: { + baseUrl: string; + currentUser: AuthUser | null; imageCompressionEnabled: boolean; isOwner: boolean; localePreference: MobileLocaleMode; onClose: () => void; onImageCompressionChange: (enabled: boolean) => void; onLocalePreferenceChange: (locale: MobileLocaleMode) => void; - onOpenAdvancedPlay: () => void; - onOpenApiTokens: () => void; - onOpenPassword: () => void; - onOpenSystemInfo: () => void; - onOpenUserManagement: () => void; onSignOut: () => void; }) => { const { resolvedTheme, toggleTheme } = useMobileTheme(); @@ -1981,6 +1961,19 @@ const SettingsView = ({ ]; const title = tabs.find((tab) => tab.key === activeTab)?.label ?? "我的"; + useEffect(() => { + const subscription = BackHandler.addEventListener("hardwareBackPress", () => { + if (activeTab) { + setActiveTab(null); + } else { + onClose(); + } + return true; + }); + + return () => subscription.remove(); + }, [activeTab, onClose]); + const renderContent = () => { if (activeTab === "general") { return ( @@ -2014,29 +2007,30 @@ const SettingsView = ({ - } - onPress={onOpenSystemInfo} - title="系统信息" - /> + ); } if (activeTab === "users") { - return } onPress={onOpenUserManagement} title="成员管理" />; + return ( + + + + ); } if (activeTab === "ai") { return ( - } onPress={onOpenAdvancedPlay} title="进阶玩法" /> - } onPress={onOpenApiTokens} title="生成 MCP 配置" /> + + ); } return ( - } onPress={onOpenPassword} title="修改密码" /> + + + 退出登录 @@ -2090,18 +2084,6 @@ const SettingsGroup = ({ children, icon, title }: { children: ReactNode; icon?: ); -const SettingsLinkGroup = ({ description, icon, onPress, title }: { description: string; icon: ReactNode; onPress: () => void; title: string }) => ( - - - - {icon}{title} - {description} - - - - -); - const CreateMemoModal = ({ activeNotebookId, baseUrl, @@ -3082,14 +3064,18 @@ const TagsManagerModal = ({ onClose, visible }: { onClose: () => void; visible: ); }; -const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClose: () => void; visible: boolean }) => { +const ApiTokensContent = ({ active, baseUrl }: { active: boolean; baseUrl: string }) => { const { client } = useSession(); + const { translate } = useMobileLocale(); const queryClient = useQueryClient(); const [name, setName] = useState("MCP Agent"); + const [nameDefaultsSynced, setNameDefaultsSynced] = useState(false); const [selectedScopes, setSelectedScopes] = useState>(() => new Set(ALL_TOKEN_SCOPES)); const [scopeDefaultsSynced, setScopeDefaultsSynced] = useState(false); + const [scopesExpanded, setScopesExpanded] = useState(false); const [createdToken, setCreatedToken] = useState(null); const [copiedValue, setCopiedValue] = useState(null); + const [exampleOpen, setExampleOpen] = useState(false); const tokensQuery = useQuery({ queryKey: ["mobile", "api-tokens"], @@ -3100,12 +3086,24 @@ const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClos return client.listApiTokens(); }, - enabled: Boolean(client && visible), + enabled: Boolean(client && active), }); const availableScopes = tokensQuery.data?.availableScopes ?? ALL_TOKEN_SCOPES; const tokens = tokensQuery.data?.apiTokens ?? []; + useEffect(() => { + if (nameDefaultsSynced || !tokensQuery.data) { + return; + } + const highestTokenNumber = tokens.reduce((highest, token) => { + const match = token.name.match(/^MCP Token (\d+)$/i); + return match ? Math.max(highest, Number(match[1])) : highest; + }, 0); + setName(`MCP Token ${highestTokenNumber + 1}`); + setNameDefaultsSynced(true); + }, [nameDefaultsSynced, tokens, tokensQuery.data]); + useEffect(() => { if (scopeDefaultsSynced || !tokensQuery.data?.availableScopes) { return; @@ -3189,21 +3187,22 @@ const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClos }; return ( - - - - - - - MCP 与 API Token - tokensQuery.refetch()}> - {tokensQuery.isFetching ? : } - + + + + + + 生成 MCP 配置 + + 让 AI Agent 可以读取和整理你的笔记。 + tokensQuery.refetch()}> + {tokensQuery.isFetching ? : } + + - - 创建远程 MCP 或 API 调用使用的 Bearer Token。 - copyText(buildMcpRemoteConfig(baseUrl, "YOUR_TOKEN_HERE"), "example-config")}> + + setExampleOpen(true)}> @@ -3211,7 +3210,7 @@ const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClos - Token 已创建 + API Token 已成功生成 {createdToken} @@ -3224,25 +3223,30 @@ const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClos - 请立即保存。离开后服务端不会再次显示完整 Token。 + 安全提醒:此 Token 属于高危凭证,请勿对外泄露。 ) : null} - Token 名称 - + - 权限范围 - + setScopesExpanded((value) => !value)} style={styles.tokenScopeHeader}> + + Token 权限范围 + {translate(`已选择 ${selectedScopes.size}/${availableScopes.length}`)} + + {scopesExpanded ? : } + + {scopesExpanded ? {availableScopes.map((scope) => { const selected = selectedScopes.has(scope); return ( toggleScope(scope)} style={[styles.scopePill, selected && styles.scopePillActive]}> - {getTokenScopeLabel(scope)} + {translate(getTokenScopeLabel(scope))} ); })} - + : null} {createTokenMutation.isPending ? : } - {createTokenMutation.isPending ? "创建中" : "创建 Token"} + {createTokenMutation.isPending ? "正在创建..." : "生成 Token"} {createTokenMutation.error ? ( {createTokenMutation.error instanceof Error ? createTokenMutation.error.message : "创建失败"} ) : null} - 活跃 Token + Token 列表 {tokensQuery.isLoading ? ( @@ -3264,7 +3268,7 @@ const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClos ) : tokens.length === 0 ? ( - 暂无 Token + 暂无 API Token ) : ( tokens.map((token) => ( @@ -3282,12 +3286,44 @@ const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClos {revokeTokenMutation.error ? ( {revokeTokenMutation.error instanceof Error ? revokeTokenMutation.error.message : "撤销失败"} ) : null} - - - + + setExampleOpen(false)} transparent visible={exampleOpen}> + setExampleOpen(false)} style={styles.settingsDialogBackdrop}> + + + Remote MCP 示例 + setExampleOpen(false)}> + + + + {buildMcpRemoteConfig(baseUrl, "YOUR_TOKEN_HERE")} + copyText(buildMcpRemoteConfig(baseUrl, "YOUR_TOKEN_HERE"), "example-config")}> + + + + + + ); }; +const ApiTokensModal = ({ baseUrl, onClose, visible }: { baseUrl: string; onClose: () => void; visible: boolean }) => ( + + + + + + + MCP 与 API Token + + + + + + + +); + const ApiTokenRow = ({ baseUrl, copiedValue, @@ -3303,6 +3339,7 @@ const ApiTokenRow = ({ onDelete: (token: ApiToken) => void; token: ApiToken; }) => { + const { resolvedLocale, translate } = useMobileLocale(); const tokenCopyLabel = `token-${token.id}`; const configCopyLabel = `config-${token.id}`; const canCopyToken = Boolean(token.token && !token.isRevoked); @@ -3315,18 +3352,18 @@ const ApiTokenRow = ({ {token.name} - {token.scopes.map(getTokenScopeLabel).join("、") || "无权限"} + {token.scopes.map((scope) => translate(getTokenScopeLabel(scope))).join(resolvedLocale === "en-US" ? ", " : "、") || translate("无权限")} - {token.lastUsedAt ? `最近使用 ${formatDate(token.lastUsedAt, localePreference)}` : "从未使用"} + {token.lastUsedAt ? `上次调用时间:${formatDate(token.lastUsedAt, localePreference)}` : "从未被调用"} - token.token && onCopy(token.token, tokenCopyLabel)}> + token.token && onCopy(token.token, tokenCopyLabel)}> {copiedValue === tokenCopyLabel ? : } - token.token && onCopy(buildMcpRemoteConfig(baseUrl, token.token), configCopyLabel)}> + token.token && onCopy(buildMcpRemoteConfig(baseUrl, token.token), configCopyLabel)}> {copiedValue === configCopyLabel ? : } - !isDeleting && onDelete(token)}> + !isDeleting && onDelete(token)}> @@ -3334,8 +3371,9 @@ const ApiTokenRow = ({ ); }; -const AdvancedPlayModal = ({ onClose, visible }: { onClose: () => void; visible: boolean }) => { +const AdvancedPlayCard = () => { const [copiedPromptId, setCopiedPromptId] = useState(null); + const [expanded, setExpanded] = useState(false); const localePreference = useMobileLocalePreference(); const advancedPrompts = useMemo(() => getMobileAdvancedPrompts(localePreference), [localePreference]); @@ -3346,17 +3384,19 @@ const AdvancedPlayModal = ({ onClose, visible }: { onClose: () => void; visible: }; return ( - - - - - - - 进阶玩法 - + + setExpanded((value) => !value)} style={styles.settingsAccordionHeader}> + + + + 进阶玩法 + + 搭配 AI Agent 的进阶玩法。 - - + {expanded ? : } + + {expanded ? ( + 搭配 AI Agent 的进阶工作流 @@ -3376,9 +3416,9 @@ const AdvancedPlayModal = ({ onClose, visible }: { onClose: () => void; visible: ))} - - - + + ) : null} + ); }; @@ -3497,46 +3537,20 @@ const SyncQueueModal = ({ ); }; -const SystemInfoModal = ({ - baseUrl, - memoCount, - notebookCount, - onClose, - visible, -}: { - baseUrl: string; - memoCount: number; - notebookCount: number; - onClose: () => void; - visible: boolean; -}) => { +const SystemInfoCard = () => { const [copied, setCopied] = useState(false); + const [expanded, setExpanded] = useState(false); const localePreference = useMobileLocalePreference(); const resolvedLocale = getResolvedMobileLocale(localePreference); const copy = getMobileSystemInfoText(localePreference); - const expoConfig = Constants.expoConfig; - const expoExtra = expoConfig?.extra as { eas?: { projectId?: string } } | undefined; - const nativeIdentifier = Platform.select({ - android: expoConfig?.android?.package, - ios: expoConfig?.ios?.bundleIdentifier, - default: expoConfig?.slug, - }); const infoItems = [ { label: copy.version, value: `v${MOBILE_APP_VERSION}` }, { label: copy.build, value: __DEV__ ? "development" : "production" }, { label: copy.platform, value: Platform.OS }, { label: copy.platformVersion, value: String(Platform.Version) }, - { label: copy.installMode, value: formatExecutionEnvironment(Constants.executionEnvironment, localePreference) }, - { label: copy.appIdentifier, value: nativeIdentifier || copy.unknown }, - { label: "Expo Owner", value: expoConfig?.owner || copy.notSet }, - { label: "Expo Slug", value: expoConfig?.slug || copy.unknown }, - { label: "EAS Project ID", value: Constants.easConfig?.projectId || expoExtra?.eas?.projectId || copy.disconnected }, - { label: copy.instanceUrl, value: baseUrl || copy.disconnected }, - { label: copy.notebookCount, value: String(notebookCount) }, - { label: copy.memoCount, value: String(memoCount) }, - { label: copy.timeZone, value: Intl.DateTimeFormat().resolvedOptions().timeZone || copy.unknown }, { label: copy.language, value: localePreference === "system" ? `${resolvedLocale} (${copy.followSystem})` : resolvedLocale }, - ...getStartupPerformanceItems(), + { label: copy.timeZone, value: Intl.DateTimeFormat().resolvedOptions().timeZone || copy.unknown }, + { label: copy.installMode, value: formatExecutionEnvironment(Constants.executionEnvironment, localePreference) }, ]; const copySystemInfo = async () => { @@ -3546,26 +3560,28 @@ const SystemInfoModal = ({ }; return ( - - - - - - - {copy.title} - - {copied ? : } - + + setExpanded((value) => !value)} style={styles.settingsAccordionHeader}> + + + + {copy.title} + + {copy.description} - - - {copy.description} + {expanded ? : } + + {expanded ? ( + + + {copied ? : } + {infoItems.map((item) => ( ))} - - - + + ) : null} + ); }; @@ -6365,13 +6381,13 @@ const toCompressedImageFilename = (filename: string) => { const getTokenScopeLabel = (scope: string) => { const labels: Record = { "read:notebooks": "读取笔记本", - "write:notebooks": "写入笔记本", + "write:notebooks": "创建与修改笔记本", "read:memos": "读取笔记", - "write:memos": "写入笔记", - "read:resources": "读取资源", - "write:resources": "写入资源", + "write:memos": "创建与修改笔记", + "read:resources": "读取附件资源", + "write:resources": "上传与修改附件", "read:tags": "读取标签", - "write:tags": "写入标签", + "write:tags": "创建与修改标签", }; return labels[scope] ?? scope; @@ -6728,12 +6744,39 @@ const baseWorkspaceStyles = StyleSheet.create({ lineHeight: 17, paddingHorizontal: 16, }, - settingsLinkHeader: { + settingsAccordionHeader: { alignItems: "center", flexDirection: "row", justifyContent: "space-between", paddingRight: 16, }, + settingsInlineCardHeader: { + alignItems: "center", + flexDirection: "row", + justifyContent: "space-between", + paddingRight: 16, + }, + settingsAccordionContent: { + borderTopColor: "#f1f5f9", + borderTopWidth: 1, + gap: 10, + padding: 16, + }, + settingsDialogBackdrop: { + alignItems: "center", + backgroundColor: "rgba(15, 23, 42, 0.46)", + flex: 1, + justifyContent: "center", + padding: 20, + }, + settingsExampleDialog: { + backgroundColor: "#ffffff", + borderRadius: 14, + gap: 14, + maxWidth: 620, + padding: 16, + width: "100%", + }, settingsLinkCopy: { flex: 1, paddingBottom: 16, @@ -7944,6 +7987,17 @@ const baseWorkspaceStyles = StyleSheet.create({ flexWrap: "wrap", gap: 8, }, + tokenScopeHeader: { + alignItems: "center", + borderBottomColor: "#f1f5f9", + borderBottomWidth: 1, + borderTopColor: "#f1f5f9", + borderTopWidth: 1, + flexDirection: "row", + justifyContent: "space-between", + minHeight: 52, + paddingVertical: 8, + }, scopePill: { backgroundColor: "#ffffff", borderColor: "#e2e8f0",