Parcourir la source

按第三组设计统一 Bot 与个人设置弹窗

Samien il y a 1 mois
Parent
commit
84ea26f77a

+ 55 - 124
design-qa.md

@@ -1,142 +1,73 @@
-# 设置中心 Design QA
+# Bot 设置与我的设置 · 第 3 组设计对照
 
 ## Findings
 
-- 当前没有可执行的 P0、P1 或 P2 差异。
-- P3:补充稿的“模型与 Provider”页包含统一保存栏,而实际 Provider 的新增、编辑、测试、设为默认和删除均是独立即时操作。实现有意不显示一个无真实提交语义的全局“保存更改”,避免误导。
-- P3:账号安全补充稿本身为 `1505 × 1045`,与主设计的桌面比例不一致。实现遵循主设计的固定壳体,并让账号内容仅在右侧区域滚动;固定保存栏始终可见。
+- 当前无未解决的 P0、P1 或 P2 问题。
+- P3:项目成员使用产品现有的真实头像组件,示例图中的纯色圆点与 +1 占位没有照搬。所有成员姓名可以查看。
+- P3:保留原有管理与基础机器人功能,超出设计图显示范围的分类可在左栏滚动访问。
 
 ## Source visual truth
 
-- 主视觉:`/Users/samien/.codex/generated_images/01a04b82-a660-7c43-a4d1-5c0a84482b5c/exec-9f6f7523-6cfa-418f-9103-d748e5958d68.png`
-- 模型目录:`/Users/samien/.codex/generated_images/01a04b82-a660-7c43-a4d1-5c0a84482b5c/exec-b44ab548-2154-4432-944f-70b012ca9567.png`
-- 账号安全:`/Users/samien/.codex/generated_images/01a04b82-a660-7c43-a4d1-5c0a84482b5c/exec-73b26de9-b178-48dd-a616-3933f87990f3.png`
-- 系统概览:`/Users/samien/.codex/generated_images/01a04b82-a660-7c43-a4d1-5c0a84482b5c/exec-d8382bf2-4998-4ff8-bfd3-35d297071297.png`
-
-源图像素尺寸依次为 `1717 × 916`、`1683 × 934`、`1505 × 1045`、`1717 × 916`。主视觉、模型和系统概览按相同比例归一到 `1637 × 874`;账号安全只作为内容层级参考,未拉伸为桌面比例。
+- 用户选择:本次 ideation 第 3 张实际输出。
+- 文件:`docs/design/settings-reference.png`(1983 × 793)。
+- 原图:`/Users/samien/.codex/generated_images/01a0a29b-2f08-7f51-8a4e-81b93631bf5b/exec-4ae99c1e-db43-4f23-9b67-7f9df8424943.png`。
+- 图中每个弹窗约 900 × 648;两张图分别呈现 Bot 项目页和个人外观页。
 
 ## Implementation evidence
 
-- 身份与头像:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/08-identity-desktop-fixed-footer.png`
-- 模型与 Provider:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/11-models-desktop-final.png`
-- 登录与安全:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/09-security-desktop-fixed-footer.png`
-- 系统概览:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/10-overview-desktop-final.png`
-- 移动端分类:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/12-mobile-menu-final.png`
-- 移动端账号页:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/07-mobile-security-fixed-footer.png`
-
-桌面实现截图与 CSS 视口均为 `1637 × 874`,`devicePixelRatio = 1`。移动端实现截图与 CSS 视口均为 `390 × 844`,`devicePixelRatio = 1`。
-
-## Comparison evidence
-
-- 身份全景并排:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/compare-identity.png`
-- 模型密集内容并排:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/compare-models.png`
-- 账号表单并排:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/compare-security.png`
-- 系统概览并排:`/Users/samien/.codex/visualizations/2026/08/29/01a04b82-a660-7c43-a4d1-5c0a84482b5c/settings-center-implementation/compare-overview.png`
+- Bot 桌面:`docs/design/settings-bot-desktop.jpg`,1280 × 720。
+- 我的设置桌面:`docs/design/settings-personal-desktop.jpg`,1280 × 720。
+- Bot 手机:`docs/design/settings-bot-mobile.jpg`,390 × 844。
+- 我的设置手机:`docs/design/settings-personal-mobile.jpg`,390 × 844。
+- 深色:`docs/design/settings-personal-dark.jpg`,1280 × 720。
+- 截图来自本次 CUA 浏览器捕获,原始捕获字节从当前任务记录保存,未重绘或改写图片。
+- 验证入口:`http://127.0.0.1:5173/tests/fixtures/settings-design/`,个人外观状态使用 `?view=appearance`。
+- 这是测试夹具,使用正式 Vue 组件。API 在夹具内存中处理,不修改生产项目或账号。
 
-全景比较用于壳体、分栏、层级和整体密度;模型目录与账号表单比较作为聚焦区域,用于检查 ID、标签、字段、按钮、滚动和固定操作栏。
+## Viewport / normalization / state
 
-## Required fidelity surfaces
-
-- 字体与排版:沿用现有 `--font-ui` 系统字体;页面标题 `23px`,导航 `14px`,正文与表单主要为 `13–16px`。标题、分组、标签和帮助文本层级与设计一致,没有截断关键 Provider ID。
-- 间距与布局:桌面壳体实测 `1080 × 780`,标题栏 `64px`、左栏 `292px`、编辑页底栏 `76px`。系统概览右侧 `clientHeight = scrollHeight = 604px`;设置左栏 `clientHeight = scrollHeight = 714px`,全部导航在首屏完整可见。长表单仅滚动右内容区。
-- 颜色与视觉 token:复用现有白色/暖灰表面、`--line`、`--surface-soft`、`--accent`、`--success` 和 `--danger`。选中态用浅灰,键盘焦点用紫色描边;成功与危险状态同时有文字,不只依赖颜色。
-- 图像与资产:继续使用真实 `AgentAvatar` 数据和现有品牌/`AppIcon` 体系;设计没有要求新增位图资产,未使用占位图、CSS 绘图或临时 SVG 替代。
-- 文案与内容:保留完整 Provider/model 目录与完整 `custom:tingly` ID;双流语音明确标记“全局”;账号、Agent 和系统作用域均在页面标题下持续显示。
-- 响应式与可访问性:`390 × 844` 下设置中心为全屏导航栈,页面无横向溢出(`scrollWidth = innerWidth = 390`),分类页 `clientHeight = scrollHeight = 788px`,固定底栏为 `390 × 68`。当前分类使用 `aria-current="page"`;控制行、返回/关闭和底栏按钮命中区域达到约 `40–44px`,表单使用永久标签并保留明显焦点态。关闭后焦点实测返回 `aria-label="打开设置中心"` 的原按钮。
-
-## Primary interactions tested
-
-- Agent 切换弹层只显示 Agent 列表,独立设置按钮打开统一中心。
-- 桌面设置导航可进入身份、模型、账号安全、系统概览、用户、Hermes 连接、双流语音和更新页面。
-- 管理员系统组与非管理员隐藏逻辑由组件测试覆盖。
-- 未保存内容切页确认、系统升级锁定切页/关闭由组件测试覆盖。
-- 账号登录与节点配对按作用域拆分,定时器与扫码资源清理由组件测试覆盖。
-- 移动抽屉打开后背景 header/main 均不可聚焦,焦点进入“关闭导航”;进入设置详情后焦点移到页面标题,返回后移到当前分类,关闭中心后回到“打开导航”。
-- 两处 Agent 选择器均使用 `listbox / option` 状态,并支持方向键、Home、End 与 Escape;用户行操作的 accessible name 包含目标用户名。
-- 9119 用户名、密码及双流语音 API Key 在浏览器中保持空白;Provider 完整目录与只读/可编辑状态来自真实本地服务。
-- 浏览器控制台最终无 warning/error。
+- 桌面 CSS viewport:1280 × 720;截图同尺寸,按 1:1 CSS 像素检查。
+- 实现弹窗实测:900 × 650,位置 x=190、y=35;标题栏 60px、左栏 208px。
+- 源图是两个弹窗并排的设计画板。对比弹窗内容区域,排除画板边距、页面背景及 2px 高度差异;未将整张画板拉伸来比较。
+- 手机 CSS viewport:390 × 844,截图同尺寸;个人设置改为分类页与详情页,Bot 三个分类横向排列。
+- 主题:浅色、深色均验证。项目页为两个示例项目;源图示例数据没有写入生产。
 
 ## Comparison history
 
-1. 初次比较发现 P2:身份和账号安全的保存操作位于滚动内容中,未匹配固定底栏。
-2. 修复:为两页增加外部表单 ID 和统一固定 `取消 / 保存更改` 底栏;桌面底栏实测 `786 × 76`,移动端底栏实测 `390 × 68`。
-3. 复核:`08-identity-desktop-fixed-footer.png`、`09-security-desktop-fixed-footer.png` 与 `07-mobile-security-fixed-footer.png` 证明操作栏固定,右内容区仍可独立滚动,无横向溢出。
-4. 独立代码审查发现更新任务恢复/启动锁、Agent 草稿刷新、扫码资源竞态、APNs/FCM 草稿互相覆盖等 P1/P2 风险;实现加入生命周期 token、真实 dirty baseline、分离配置快照、并发锁与业务 401 隔离。
-5. 第二次复核确认 Agent 弹层仅剩 Agent 列表,桌面与移动设置导航首屏完整可见,关闭焦点正确恢复;新增组件测试覆盖 Provider/语音丢稿、更新恢复、摄像头停止、账号 401 和推送并发草稿。
-6. 无障碍浏览器复核补齐移动详情/返回焦点、抽屉背景隔离、Agent 复合控件键盘操作及用户行按钮上下文;`390 × 844` 与 `1637 × 874` 实测焦点位置正确,控制台仍无 warning/error。
-
-## Verification
-
-- `npm run typecheck`:通过。
-- `npm test`:61 个测试文件、317 项测试全部通过。
-- `npm run build`:品牌校验、发布版本校验、客户端与服务端生产构建全部通过。
-- `git diff --check`:通过。
-- 两轮独立最终审查:无剩余 P0、P1 或 P2。
-
-## Final result
-
-final result: passed
-
----
-
-# 自有动态 Agent 角色 Design QA(2026-09-02)
-
-## Evidence
-
-- Source visual language: `/Users/samien/git/OpenMausBot/docs/screenshots/agent-profile-desktop.png`.
-- Implementation screenshot: `/Users/samien/.codex/visualizations/2026/09/02/01a05f7c-9ccc-76f2-99f3-116cd8ca64cb/agent-identity/web-desktop.jpg`.
-- Browser viewport: `1440 × 1024`; fixture: `/?fixture=agent-identity`.
-
-## Review
+1. 初查发现 [P2] Bot 项目详情行高偏大,900 × 650 弹窗出现额外纵向滚动。
+   - 调整详情行高 56px → 48px,标题下间距 16px → 12px,底部按钮间距 14px → 8px。
+2. 修正后在同一个 CUA 对照输入中同时展示源图、Bot 桌面截图和个人设置桌面截图。
+   - Bot 内容区 clientHeight=588、scrollHeight=588,完整显示列表和详情。
+   - 整体分栏、标题位置、灰色分组、选中行、紧凑操作按钮与主题预览卡符合所选方向。
+   - 正文及控件在 900px 弹窗截图中可直接阅读,重点检查了项目成员行、编辑按钮、主题卡及左侧分类,无需另做放大图。
 
-- OpenMausBot 的渐变、白色眼睛、极简嘴型与状态动效语言得到保留;原细长指针轮廓未复制。
-- 圆形、圆角方形和短三角使用统一视觉面积;短三角接近 1:1 包围盒,尖端未超出同组卡片的光学边界。
-- `working` 的实时 transform 在采样间变化;`notifying` 首屏截图可见提醒光环;页面控制台无 warning/error。
-- 角色名称、三种形状、工作、新消息和等待操作均有可访问名称;减少动态效果由 CSS media query 覆盖。
-- 无剩余 P0、P1 或 P2 视觉问题。
-
-final result: passed
-
-## 群聊组合头像补充验收
-
-- 浏览器证据:`/Users/samien/.codex/visualizations/2026/09/02/01a05f7c-9ccc-76f2-99f3-116cd8ca64cb/group-mascot/web.jpg`。
-- 群聊默认头像由前三位成员角色组成;不足三位时使用圆、方、短三角补足,旧动物值按新组合头像显示。
-- 上传图片仍优先;创建与管理界面改为“成员组合 / 上传图片”,不再出现动物选择器。
-- `1440 × 1024` 浏览器检查确认三个成员均可辨识,短三角未被完全遮挡,控制台无 warning/error。
-
-final result: passed
-
-## Bot 通信提示验收 · 2026-09-14
-
-### 范围与视觉依据
-
-- 本次只调整 Bot 模式的通信展示,并移除 iOS 输入区左下角的“记忆与协作”;现有标题栏、普通消息、输入框和其他入口保持原样。
-- 用户确认的视觉:`docs/verification/2026-09-14-bot-communication/reference.png`,即第二稿去除横线、进一步缩小提示文字与头像后的版本。
-- 原图为 853 × 1844,逻辑参考约 390 × 844;iPhone 17 Pro 实测截图为 1206 × 2622,对应 402 × 874 @3x。比较提示组件时按逻辑尺寸归一,系统状态栏与既有标题栏不作为本次重绘目标。
-- iOS 浅色实现:`docs/verification/2026-09-14-bot-communication/ios-light.png`。
-- iOS 深色实现:`docs/verification/2026-09-14-bot-communication/ios-dark.png`。
-- 完整参考和两张原生截图已在同一比较输入中查看;重点核对两处提示行和输入区。没有把不同密度或既有标题栏差异误判为本次改动。
-
-### 五项视觉核对
-
-- 字体:提示为系统 caption(基准 12pt),小于消息正文;方向文字与名字同级,不额外加粗。Web 实测 12px,Android 12sp。
-- 间距:文字和头像间距 6,头像基准 16;整行居中,提示在通信正文上方。Web 390px 视口中两行中心均为 x=195;伪元素 before/after 均为 none。
-- 颜色:沿用各端语义次级文字色和主题气泡色;浅色、深色均能区分提示与正文,没有新增横线或卡片边框。
-- 资产:使用对方实际 Bot 头像,复用既有头像组件;不使用字母占位或额外装饰图标。截图中的研究员使用同一深色角色头像。
-- 文案:独立显示“发送给 / 消息来自 + 头像 + 名字”,正文显示实际通信内容。原始记录及旧客户端的兼容正文保留,新客户端通过 communication 展示信息渲染;普通消息不解析文案猜测方向。
-
-### 比较与修正记录
-
-1. Web 首次查看发现普通系统消息的样式仍给通信正文添加边框并居中(P2)。已增加仅针对通信消息的样式覆盖。
-2. 修正后在 Codex 内置浏览器的 390 × 844 视口重新查看:正文边框 0px、左对齐;只有提示行居中,头像 16px,控制台 error 为 0。
-3. iOS 使用原生界面测试分别启动浅色和深色,等待提示及原始用户问题可见后截图;两处提示均有独立可访问性标签,提示行高度不超过 24pt,输入区不存在“记忆与协作”按钮。
-4. iOS 深色自动截图出现普通用户气泡文字未绘制的现象,辅助功能就绪检查仍可通过;浅色截图完整。普通用户文本与颜色的渲染分支未改动,原因尚未确认。深色截图保留该现象,未标记为已修复,也不作为全页面视觉通过的证据。
-
-### 结果
+## Required fidelity surfaces
 
-- 本次作用范围内没有剩余 P0、P1、P2 视觉差异。
-- 原生 iOS 的大标题、时间分隔与气泡宽度是产品原有布局,依本次修改范围保留。
-- Web 手动检查覆盖登录、历史消息、提示方向、实际正文、字号、头像尺寸、居中位置与控制台。
-- 自动回归覆盖实时事件与历史投影、原始数据不被重写、普通消息不误识别、三端类型兼容,以及 iOS 入口移除。
+- 字体:使用现有 Inter / SF Pro / PingFang SC 字体栈;弹窗标题和内容标题 18px,导航与详情正文 14px,辅助文本 12–13px。
+- 间距与布局:两个弹窗统一 900 × 650、14px 圆角、208px 左栏;内容区 24px 内边距。项目列表与详情在桌面完整显示。
+- 颜色:增加设置专用的 panel/sidebar/selected 语义色,浅色使用中性灰;深色保持可读对比,选中主题同时有边框、标记与 aria-checked。
+- 图片:三张主题缩略图由 ImageGen 生成,480 × 300,使用真实 PNG 资产;图标与头像复用现有组件/图标库。
+- 文案:界面使用“Bot 设置”“我的设置”;项目名称、成员和群聊均绑定实际数据,账号、权限与管理入口的作用范围保留。
+
+## Interaction verification
+
+- 示例项目选择后,详情正确切换。
+- 编辑示例项目名称并保存,列表与详情同步更新。
+- 原有用户记忆与项目记忆入口保持可用;相关组件测试通过。
+- 深色/浅色切换更新选择状态与实际主题,夹具内不保存用户浏览器偏好。
+- 手机个人设置 Escape 返回分类,并将焦点还给当前分类。
+- 手机个人设置实测 clientWidth=scrollWidth=390,内容区域无水平溢出;Bot 手机截图无裁切或水平滚动。
+- 普通账号实际预览只显示个人分类;管理员页面和虚拟机设置权限由组件测试覆盖。
+- 设置内补充 Tab 首尾循环,隐藏头像文件输入不进入键盘顺序。
+- 本次浏览器预览控制台未发现应用错误;预览最初缺少 CSRF 夹具响应,已补齐后完成保存验证。
+
+## Implementation checklist
+
+- [x] Bot 左侧分类与项目列表/详情。
+- [x] 个人设置搜索、分类与主题预览。
+- [x] 账号资料与安全内容分开,保留原有保存接口。
+- [x] 现有权限、未保存提醒及更新锁保留。
+- [x] 桌面、手机与深色视觉检查。
+- [x] 34 项相关组件测试通过,类型检查通过。
 
 final result: passed

BIN
docs/design/settings-bot-desktop.jpg


BIN
docs/design/settings-bot-mobile.jpg


BIN
docs/design/settings-personal-dark.jpg


BIN
docs/design/settings-personal-desktop.jpg


BIN
docs/design/settings-personal-mobile.jpg


BIN
docs/design/settings-reference.png


BIN
public/setting-previews/dark.png


BIN
public/setting-previews/light.png


BIN
public/setting-previews/system.png


+ 10 - 7
src/client/components/app/AccountSecurityPanel.vue

@@ -10,11 +10,13 @@ const props = withDefaults(defineProps<{
   open?: boolean
   formId?: string
   showActions?: boolean
+  section?: 'all' | 'profile' | 'security'
 }>(), {
   active: true,
   open: undefined,
   formId: undefined,
   showActions: true,
+  section: 'all',
 })
 
 const emit = defineEmits<{
@@ -171,7 +173,7 @@ watch(canSave, value => emit('can-save-change', value), { immediate: true, flush
 </script>
 
 <template>
-  <section class="account-security-panel" aria-label="登录与安全">
+  <section class="account-security-panel" :aria-label="section === 'profile' ? '账号资料' : '登录与安全'">
     <header class="panel-heading">
       <div>
         <p class="panel-eyebrow">当前账号</p>
@@ -181,16 +183,17 @@ watch(canSave, value => emit('can-save-change', value), { immediate: true, flush
       <span class="account-badge">{{ auth.user?.username || '当前账号' }}</span>
     </header>
 
-    <ServerIdentityPanel v-if="auth.serverIdentity" @dirty-change="serverNameDirty = $event" />
-    <section class="account-avatar-card" aria-label="账号头像">
+    <section v-if="section !== 'security'" class="account-avatar-card" aria-label="账号头像">
       <AccountInitialAvatar :name="auth.user?.username || '当前账号'" :image-url="auth.user?.avatar" :size="64" />
-      <div><strong>账号头像</strong><small>默认显示用户名首字母。头像只在 Web 修改,iOS 会同步显示。</small></div>
+      <div><strong>{{section === 'profile' ? auth.user?.username || '当前账号' : '账号头像'}}</strong><small>头像会同步到你的其他设备。</small></div>
       <button type="button" :disabled="busy || avatarBusy" @click="avatarInput?.click()">{{ avatarBusy ? '正在保存…' : '更换头像' }}</button>
       <button v-if="auth.user?.avatar" type="button" :disabled="busy || avatarBusy" @click="resetAvatar">使用首字母</button>
-      <input ref="avatarInput" class="sr-only" type="file" accept="image/png,image/jpeg,image/webp" @change="chooseAvatar" />
+      <input ref="avatarInput" class="sr-only" type="file" tabindex="-1" aria-hidden="true" accept="image/png,image/jpeg,image/webp" @change="chooseAvatar" />
     </section>
+    <ServerIdentityPanel v-if="section !== 'security' && auth.serverIdentity" @dirty-change="serverNameDirty = $event" />
+    <p v-if="section === 'profile' && (error || notice)" class="form-message" :class="error ? 'form-message--error' : 'form-message--success'" :role="error ? 'alert' : 'status'">{{error || notice}}</p>
 
-    <form :id="formId" class="security-form" @submit.prevent="save">
+    <form v-if="section !== 'profile'" :id="formId" class="security-form" @submit.prevent="save">
       <h3 v-if="isAdmin" class="security-section-title">账号信息</h3>
       <label v-if="isAdmin" class="field">
         <span>管理员用户名</span>
@@ -235,7 +238,7 @@ watch(canSave, value => emit('can-save-change', value), { immediate: true, flush
       </footer>
     </form>
 
-    <section class="logout-card" aria-label="退出登录">
+    <section v-if="section !== 'profile'" class="logout-card" aria-label="退出登录">
       <div>
         <strong>退出当前账号</strong>
         <p>只退出这个浏览器中的当前 Web 会话,不会停止服务,也不会影响其他设备。</p>

+ 102 - 117
src/client/components/app/SettingsCenterDialog.vue

@@ -13,13 +13,14 @@ import SystemOverviewPanel from '@/components/app/SystemOverviewPanel.vue'
 import FileAccessPanel from '@/components/app/FileAccessPanel.vue'
 import SystemUpdatePanel from '@/components/app/SystemUpdatePanel.vue'
 import AgentAvatar from '@/components/common/AgentAvatar.vue'
-import AccountInitialAvatar from '@/components/common/AccountInitialAvatar.vue'
 import type { ProfileIdentityInput } from '@/api/profiles'
 import AppIcon from '@/components/common/AppIcon.vue'
+import { CheckmarkCircle, EllipseOutline } from '@vicons/ionicons5'
 
 type SettingsPage =
   | 'agent-identity'
   | 'agent-models'
+  | 'account-profile'
   | 'account-security'
   | 'account-mobile'
   | 'appearance'
@@ -95,26 +96,32 @@ const mobileDetailOpen = ref(false)
 const updateLocked = ref(false)
 const accountCanSave = ref(false)
 const dirtyPages = reactive<Partial<Record<SettingsPage, boolean>>>({})
+const settingsQuery = ref('')
 
 const agentItems = computed<NavigationItem[]>(() => !props.isAdmin ? [] : [
   { key: 'agent-identity', label: '身份与头像', icon: 'users' },
   ...(props.isAdmin ? [{ key: 'agent-models', label: '模型与 Provider', icon: 'model' } satisfies NavigationItem] : []),
 ])
 const accountItems = computed<NavigationItem[]>(() => [
+  { key: 'account-profile', label: '账号资料', icon: 'users' },
   { key: 'account-security', label: '登录与安全', icon: 'settings' },
-  ...(props.isAdmin ? [{ key: 'account-mobile', label: '手机登录', icon: 'panel' } satisfies NavigationItem] : []),
   { key: 'appearance', label: '外观', icon: 'sun' },
+  ...(props.isAdmin ? [{ key: 'account-mobile', label: '手机登录', icon: 'panel' } satisfies NavigationItem] : []),
 ])
 const systemItems: NavigationItem[] = [
   { key: 'system-overview', label: '系统概览', icon: 'panel' },
-  { key: 'system-file-access', label: '文件访问', icon: 'panel' },
-  { key: 'system-users', label: '用户与权限', icon: 'users' },
   { key: 'system-connection', label: 'Hermes 连接', icon: 'link' },
-  { key: 'system-push', label: '消息推送', icon: 'bell' },
+  { key: 'system-users', label: '用户与权限', icon: 'users' },
   { key: 'system-nodes', label: '节点与设备', icon: 'panel' },
+  { key: 'system-file-access', label: '文件访问', icon: 'panel' },
+  { key: 'system-push', label: '消息推送', icon: 'bell' },
   { key: 'system-voice', label: '双流语音', icon: 'audio' },
   { key: 'system-update', label: '更新与回滚', icon: 'refresh' },
 ]
+const matchesSearch = (item: NavigationItem) => item.label.toLocaleLowerCase().includes(settingsQuery.value.trim().toLocaleLowerCase())
+const visibleAccounts = computed(() => accountItems.value.filter(matchesSearch))
+const visibleSystems = computed(() => systemItems.filter(matchesSearch))
+const visibleAgents = computed(() => agentItems.value.filter(matchesSearch))
 
 const allAllowedPages = computed(() => new Set<SettingsPage>([
   ...agentItems.value.map(item => item.key),
@@ -126,6 +133,7 @@ const showFixedFooter = computed(() => activePage.value === 'agent-identity' ||
 const activeTitle = computed(() => ({
   'agent-identity': '身份与头像',
   'agent-models': '模型与 Provider',
+  'account-profile': '账号资料',
   'account-security': '登录与安全',
   'account-mobile': '手机登录',
   appearance: '外观',
@@ -142,8 +150,9 @@ const accountName = computed(() => props.pairingUserName || props.userName || '
 const showAgentSelector = computed(() => activePage.value.startsWith('agent-'))
 const activeScope = computed(() => {
   if (activePage.value.startsWith('agent-')) return `正在设置:${profileTitle(props.activeProfile)} / ${props.activeProfile?.name || '未选择'}`
+  if (activePage.value === 'account-profile') return '管理你的账号头像与服务器名称。'
   if (activePage.value.startsWith('account-')) return `当前账号:${accountName.value}${props.isAdmin ? ' · 管理员' : ''}`
-  if (activePage.value === 'appearance') return '仅影响当前浏览器'
+  if (activePage.value === 'appearance') return '选择当前浏览器的显示方式。'
   return '全局设置 · 仅管理员'
 })
 
@@ -238,6 +247,13 @@ function handleEscape() {
   if (window.innerWidth < 768 && mobileDetailOpen.value) { backToMenu(); return }
   requestClose()
 }
+function trapFocus(event: KeyboardEvent) {
+  const items = [...(dialog.value?.querySelectorAll<HTMLElement>('button:not(:disabled),input:not(:disabled),select:not(:disabled),textarea:not(:disabled),a[href],[tabindex="0"]') ?? [])].filter(el => el.tabIndex >= 0 && el.getClientRects().length)
+  const first = items[0], last = items.at(-1)
+  if (!first || !last) return
+  if (event.shiftKey && (document.activeElement === first || document.activeElement === dialog.value)) { event.preventDefault(); last.focus() }
+  else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus() }
+}
 
 watch(() => [props.open, props.initialPage] as const, ([open, initialPage]) => {
   if (!open) {
@@ -247,7 +263,8 @@ watch(() => [props.open, props.initialPage] as const, ([open, initialPage]) => {
   }
   activePage.value = allAllowedPages.value.has(initialPage)
     ? initialPage
-    : props.activeProfile ? 'agent-identity' : 'account-security'
+    : props.isAdmin && props.activeProfile ? 'agent-identity' : 'account-profile'
+  settingsQuery.value = ''
   for (const key of Object.keys(dirtyPages) as SettingsPage[]) dirtyPages[key] = false
   profileMenuOpen.value = false
   mobileDetailOpen.value = false
@@ -269,17 +286,20 @@ function requestModeSwitch() {
           :class="{ 'settings-center--mobile-detail': mobileDetailOpen }"
           role="dialog"
           aria-modal="true"
-          aria-label="设置中心"
+          aria-label="我的设置"
           tabindex="-1"
           @keydown.esc.capture.prevent.stop="handleEscape"
+          @keydown.tab="trapFocus"
         >
+          <header class="settings-center__topbar"><h2>我的设置</h2><button class="settings-center__close" type="button" aria-label="关闭我的设置" :disabled="updateLocked" @click="requestClose"><AppIcon name="close" :size="20"/></button></header>
           <div class="settings-center__body">
             <aside class="settings-sidebar" aria-label="设置分类">
               <header class="settings-sidebar__header">
-                <h2 id="settings-center-title">设置中心</h2>
-                <button class="settings-center__close" type="button" aria-label="关闭设置中心" :disabled="updateLocked" @click="requestClose"><AppIcon name="close" :size="18" /></button>
+                <h2 id="settings-center-title">我的设置</h2>
+                <button class="settings-center__close" type="button" aria-label="关闭我的设置" :disabled="updateLocked" @click="requestClose"><AppIcon name="close" :size="18" /></button>
               </header>
               <div class="settings-sidebar__scroll">
+                <label class="settings-search"><AppIcon name="search" :size="16"/><input v-model="settingsQuery" type="search" aria-label="搜索设置" placeholder="搜索设置" /></label>
                 <div v-if="showAgentSelector" class="settings-agent-selector">
                   <button ref="settingsAgentTrigger" type="button" aria-haspopup="listbox" :aria-expanded="profileMenuOpen" @click="toggleAgentMenu">
                     <AgentAvatar :name="profileTitle(activeProfile)" :avatar="activeProfile?.agentAvatar || ''" :size="34" />
@@ -294,25 +314,21 @@ function requestModeSwitch() {
                     </button>
                   </div>
                 </div>
-                <div v-else class="settings-account-summary" aria-label="当前账号">
-                  <AccountInitialAvatar :name="accountName" :image-url="userAvatar" :size="34" />
-                  <span><strong>{{ accountName }}</strong><small>当前账号</small></span>
-                </div>
-
                 <nav>
-                  <section v-if="isAdmin"><button type="button" :disabled="updateLocked" @click="requestModeSwitch"><AppIcon :name="botMode ? 'chat' : 'users'" :size="18" /><span>{{ botMode ? '进入聊天模式' : '进入 Bot 模式' }}</span></button></section>
-                  <section>
-                    <h3>当前机器人</h3>
-                    <button v-for="item in agentItems" :key="item.key" type="button" :class="{ active: activePage === item.key }" :aria-current="activePage === item.key ? 'page' : undefined" @click="selectPage(item.key)"><AppIcon :name="item.icon" :size="20" /><span>{{ item.label }}</span></button>
+                  <section v-if="visibleAccounts.length">
+                    <h3>个人</h3>
+                    <button v-for="item in visibleAccounts" :key="item.key" type="button" :class="{ active: activePage === item.key }" :aria-current="activePage === item.key ? 'page' : undefined" @click="selectPage(item.key)"><AppIcon :name="item.icon" :size="18" /><span>{{ item.label }}</span></button>
                   </section>
-                  <section>
-                    <h3>账号</h3>
-                    <button v-for="item in accountItems" :key="item.key" type="button" :class="{ active: activePage === item.key }" :aria-current="activePage === item.key ? 'page' : undefined" @click="selectPage(item.key)"><AppIcon :name="item.icon" :size="20" /><span>{{ item.label }}</span></button>
+                  <section v-if="isAdmin && visibleSystems.length">
+                    <h3>管理</h3>
+                    <button v-for="item in visibleSystems" :key="item.key" type="button" :class="{ active: activePage === item.key }" :aria-current="activePage === item.key ? 'page' : undefined" @click="selectPage(item.key)"><AppIcon :name="item.icon" :size="18" /><span>{{ item.label }}</span><em v-if="item.key === 'system-voice'">全局</em></button>
                   </section>
-                  <section v-if="isAdmin">
-                    <h3>系统 · 仅管理员</h3>
-                    <button v-for="item in systemItems" :key="item.key" type="button" :class="{ active: activePage === item.key }" :aria-current="activePage === item.key ? 'page' : undefined" @click="selectPage(item.key)"><AppIcon :name="item.icon" :size="20" /><span>{{ item.label }}</span><em v-if="item.key === 'system-voice'">全局</em></button>
+                  <section v-if="visibleAgents.length">
+                    <h3>基础机器人</h3>
+                    <button v-for="item in visibleAgents" :key="item.key" type="button" :class="{ active: activePage === item.key }" :aria-current="activePage === item.key ? 'page' : undefined" @click="selectPage(item.key)"><AppIcon :name="item.icon" :size="18" /><span>{{ item.label }}</span></button>
                   </section>
+                  <section v-if="isAdmin && !settingsQuery"><button type="button" :disabled="updateLocked" @click="requestModeSwitch"><AppIcon :name="botMode ? 'chat' : 'users'" :size="18" /><span>{{ botMode ? '进入聊天模式' : '进入 Bot 模式' }}</span></button></section>
+                  <p v-if="!visibleAccounts.length && (!isAdmin || !visibleSystems.length) && !visibleAgents.length" class="settings-search-empty">没有匹配的设置</p>
                 </nav>
               </div>
             </aside>
@@ -321,7 +337,7 @@ function requestModeSwitch() {
               <header class="settings-content__header">
                 <button v-if="mobileDetailOpen" class="mobile-back" type="button" aria-label="返回设置分类" :disabled="updateLocked" @click="backToMenu"><AppIcon name="chevron-left" :size="20" /></button>
                 <div class="settings-content__heading"><h3 ref="contentTitle" tabindex="-1">{{ activeTitle }}</h3><p>{{ activeScope }}</p></div>
-                <button class="settings-center__close" type="button" aria-label="关闭设置中心" :disabled="updateLocked" @click="requestClose"><AppIcon name="close" :size="18" /></button>
+                <button class="settings-center__close settings-detail-close" type="button" aria-label="关闭我的设置" :disabled="updateLocked" @click="requestClose"><AppIcon name="close" :size="18" /></button>
               </header>
               <div class="settings-content__scroll">
                 <AgentIdentityPanel
@@ -339,25 +355,28 @@ function requestModeSwitch() {
                 <p v-else-if="activePage === 'agent-identity'" class="settings-empty">尚未选择机器人。</p>
                 <ModelServicesPanel v-else-if="activePage === 'agent-models' && activeProfile && isAdmin" :key="activeProfile.name" :profile="activeProfile.name" @dirty-change="setDirty('agent-models', $event)" />
                 <AccountSecurityPanel
-                  v-else-if="activePage === 'account-security'"
+                  v-else-if="activePage === 'account-security' || activePage === 'account-profile'"
+                  :key="activePage"
+                  :section="activePage === 'account-profile' ? 'profile' : 'security'"
                   :active="true"
                   form-id="settings-account-security-form"
                   :show-actions="false"
-                  @dirty-change="setDirty('account-security', $event)"
+                  @dirty-change="setDirty(activePage, $event)"
                   @can-save-change="accountCanSave = $event"
-                  @saved="setDirty('account-security', false)"
+                  @saved="setDirty(activePage, false)"
                   @logout="emit('logout')"
                 />
                 <NodePairingPanel v-else-if="activePage === 'account-mobile'" mode="account" :active="true" :insecure-transport="insecureTransport" :user-name="accountName" />
                 <section v-else-if="activePage === 'appearance'" class="appearance-panel" aria-label="外观">
-                  <p>选择当前浏览器使用的界面主题。</p>
+                  <h4>界面主题</h4>
                   <div class="theme-options" role="radiogroup" aria-label="界面主题">
-                    <button v-for="option in ([['system', '跟随系统', '自动匹配设备外观'], ['light', '浅色', '始终使用浅色界面'], ['dark', '深色', '始终使用深色界面']] as const)" :key="option[0]" type="button" role="radio" :aria-checked="themePreference === option[0]" :class="{ active: themePreference === option[0] }" @click="emit('set-theme', option[0])">
-                      <AppIcon :name="option[0] === 'dark' ? 'moon' : 'sun'" :size="22" />
-                      <span><strong>{{ option[1] }}</strong><small>{{ option[2] }}</small></span>
-                      <AppIcon v-if="themePreference === option[0]" name="check" :size="18" />
+                    <button v-for="option in ([['system', '跟随系统'], ['light', '浅色'], ['dark', '深色']] as const)" :key="option[0]" type="button" role="radio" :aria-checked="themePreference === option[0]" :class="{ active: themePreference === option[0] }" @click="emit('set-theme', option[0])">
+                      <img :src="`/setting-previews/${option[0]}.png`" alt="" width="480" height="300"/>
+                      <strong>{{ option[1] }}</strong>
+                      <component :is="themePreference === option[0] ? CheckmarkCircle : EllipseOutline" class="theme-choice-mark" aria-hidden="true"/>
                     </button>
                   </div>
+                  <p class="appearance-note"><AppIcon name="info" :size="16"/>更改仅影响当前浏览器。</p>
                 </section>
                 <SystemOverviewPanel v-else-if="activePage === 'system-overview' && isAdmin" :active="true" :upstream-ready="upstreamReady" :upstream-error="upstreamError" @navigate="selectPage" />
                 <FileAccessPanel v-else-if="activePage === 'system-file-access' && isAdmin" :profile="activeProfile?.name" @dirty-change="setDirty('system-file-access', $event)" />
@@ -394,87 +413,53 @@ function requestModeSwitch() {
 </template>
 
 <style scoped>
-.settings-center-layer { position: fixed; z-index: 300; inset: 0; display: grid; place-items: center; padding: 64px; background: color-mix(in srgb, #000 24%, transparent); backdrop-filter: blur(4px); }
-.settings-center { display: block; width: min(820px, calc(100vw - 128px)); height: min(600px, calc(100dvh - 128px)); overflow: hidden; border: 1px solid var(--line); border-radius: 16px; outline: 0; background: var(--surface-raised); box-shadow: 0 20px 60px rgba(0,0,0,.18); color: var(--text-primary); }
-.settings-center__close,.mobile-back { display: grid; width: 44px; height: 44px; place-items: center; padding: 0; border: 0; border-radius: 10px; background: transparent; color: var(--text-primary); cursor: pointer; }
-.settings-center__close:hover,.mobile-back:hover { background: var(--surface-soft); }
-.settings-center__close:disabled,.mobile-back:disabled { cursor: not-allowed; opacity: .45; }
-.mobile-back { display: none; }
-.settings-center__body { display: grid; height: 100%; min-height: 0; grid-template-columns: 220px minmax(0, 1fr); }
-.settings-sidebar { position: relative; min-height: 0; overflow-y: auto; padding: 12px 14px 14px; border-right: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 42%, var(--surface-raised)); overscroll-behavior: contain; }
-.settings-sidebar__header { display: none; }.settings-sidebar__header h2 { margin: 0; font-size: 14px; letter-spacing: -.01em; }
-.settings-sidebar__scroll { min-height: 0; }
-.settings-agent-selector { position: sticky; z-index: 3; top: -12px; margin: -12px 0 8px; padding: 12px 0 8px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 42%, var(--surface-raised)); }
-.settings-account-summary { position: sticky; z-index: 3; top: -12px; display: grid; min-height: 42px; grid-template-columns: 34px minmax(0, 1fr); align-items: center; gap: 8px; margin: -12px 0 8px; padding: 12px 4px 8px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 42%, var(--surface-raised)); }
-.settings-account-summary span { display: grid; min-width: 0; gap: 2px; }.settings-account-summary strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }.settings-account-summary small { color: var(--text-muted); font-size: 11px; }
-.settings-agent-selector > button { display: grid; width: 100%; min-height: 42px; grid-template-columns: 34px minmax(0, 1fr) 16px; align-items: center; gap: 8px; padding: 2px 4px; border: 0; border-radius: 9px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
-.settings-agent-selector > button:hover { background: var(--surface-hover); }
-.settings-agent-selector span,.settings-agent-menu span { display: grid; min-width: 0; gap: 2px; }
-.settings-agent-selector strong { overflow: hidden; font-size: 14px; text-overflow: ellipsis; white-space: nowrap; }
-.settings-agent-selector small,.settings-agent-menu small { overflow: hidden; color: var(--text-muted); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
-.settings-agent-menu { position: absolute; z-index: 5; top: calc(100% - 8px); right: 0; left: 0; padding: 6px; border: 1px solid var(--line); border-radius: 12px; background: var(--surface-raised); box-shadow: var(--shadow-float); }
-.settings-agent-menu button { display: grid; width: 100%; min-height: 38px; grid-template-columns: 24px minmax(0, 1fr) 16px; align-items: center; gap: 8px; padding: 4px 6px; border: 0; border-radius: 8px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
-.settings-agent-menu button:hover,.settings-agent-menu button[aria-selected="true"] { background: var(--surface-hover); }
-.settings-agent-menu strong { overflow: hidden; font-size: 12px; text-overflow: ellipsis; white-space: nowrap; }
-.settings-sidebar nav { display: grid; gap: 6px; }
-.settings-sidebar nav section { display: grid; gap: 1px; padding-bottom: 7px; border-bottom: 1px solid var(--line); }
-.settings-sidebar nav section:last-child { padding-bottom: 0; border-bottom: 0; }
-.settings-sidebar h3 { margin: 0 6px 3px; color: var(--text-muted); font-size: 12px; font-weight: 560; }
-.settings-sidebar nav button { display: flex; width: 100%; min-height: 40px; align-items: center; gap: 14px; padding: 0 10px; border: 0; border-radius: 9px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; font: 600 14px var(--font-ui); }
-.settings-sidebar nav button:hover { background: var(--surface-hover); }
-.settings-sidebar nav button.active { background: var(--surface-soft); }
-.settings-sidebar nav button:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px color-mix(in srgb, #7c4dff 72%, var(--line-strong)); }
-.settings-sidebar nav button span { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-.settings-sidebar nav button em { padding: 2px 6px; border: 1px solid var(--line); border-radius: 999px; color: var(--text-muted); font: normal 10px var(--font-ui); }
-.settings-content { display: grid; min-width: 0; min-height: 0; grid-template-rows: auto minmax(0, 1fr); background: var(--surface-raised); }
-.settings-content--with-footer { grid-template-rows: auto minmax(0, 1fr) 68px; }
-.settings-content__header { display: grid; min-height: 52px; box-sizing: border-box; grid-template-columns: minmax(0, 1fr) 44px; align-items: center; gap: 8px; padding: 7px 8px 7px 18px; border-bottom: 1px solid var(--line); }
-.settings-content__heading { min-width: 0; }
-.settings-content__header h3 { margin: 0; font-size: 14px; letter-spacing: -.01em; }
-.settings-content__header p { overflow: hidden; margin: 3px 0 0; color: var(--text-secondary); font-size: 11px; text-overflow: ellipsis; white-space: nowrap; }
-.settings-content__scroll { min-height: 0; overflow-y: auto; padding: 16px 24px 18px; overscroll-behavior: contain; }
-.settings-content__footer { display: flex; align-items: center; justify-content: flex-end; gap: 10px; padding: 0 20px; border-top: 1px solid var(--line); background: var(--surface-raised); }
-.settings-content__footer button { display: inline-flex; min-width: 104px; min-height: 40px; align-items: center; justify-content: center; padding: 0 16px; border-radius: 9px; cursor: pointer; font: 650 13px var(--font-ui); }
-.settings-footer__cancel { border: 1px solid var(--line); background: var(--surface-raised); color: var(--text-primary); }
-.settings-footer__save { border: 0; background: var(--accent); color: var(--text-on-solid); }
-.settings-content__footer button:disabled { cursor: not-allowed; opacity: .45; }
-.settings-empty { margin: 0; padding: 24px; border-radius: 12px; background: var(--surface-soft); color: var(--text-muted); font-size: 14px; text-align: center; }
-.appearance-panel { display: grid; gap: 18px; }
-.appearance-panel > p { margin: 0; color: var(--text-secondary); font-size: 14px; }
-.theme-options { display: grid; border-top: 1px solid var(--line); }
-.theme-options button { display: grid; min-height: 72px; grid-template-columns: 28px minmax(0, 1fr) 20px; align-items: center; gap: 14px; padding: 10px 8px; border: 0; border-bottom: 1px solid var(--line); border-radius: 9px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
-.theme-options button:hover,.theme-options button.active { background: var(--surface-soft); }
-.theme-options button:focus-visible { outline: 0; box-shadow: inset 0 0 0 2px color-mix(in srgb, #7c4dff 72%, var(--line-strong)); }
-.theme-options span { display: grid; gap: 5px; }
-.theme-options strong { font-size: 15px; }
-.theme-options small { color: var(--text-muted); font-size: 13px; }
-.settings-content__scroll :deep(.account-security-panel > .panel-heading) { display: none; }
-.settings-center-fade-enter-active,.settings-center-fade-leave-active { transition: opacity 150ms ease; }
-.settings-center-fade-enter-active .settings-center,.settings-center-fade-leave-active .settings-center { transition: transform 180ms var(--ease-out); }
-.settings-center-fade-enter-from,.settings-center-fade-leave-to { opacity: 0; }
-.settings-center-fade-enter-from .settings-center,.settings-center-fade-leave-to .settings-center { transform: translateY(8px) scale(.99); }
-@media (max-width: 1023px) {
-  .settings-center-layer { padding: 32px; }
-  .settings-center { width: min(820px, calc(100vw - 64px)); height: min(600px, calc(100dvh - 64px)); }
-  .settings-center__body { grid-template-columns: 220px minmax(0, 1fr); }
-  .settings-content__scroll { padding-inline: 24px; }
-}
-@media (max-width: 767px) {
-  .settings-center-layer { padding: 0; }
-  .settings-center { width: 100vw; height: 100dvh; border: 0; border-radius: 0; }
-  .settings-center__body { display: block; height: 100%; }
-  .settings-sidebar { display: grid; width: 100%; height: 100%; box-sizing: border-box; grid-template-rows: 56px minmax(0, 1fr); overflow: hidden; padding: 0; border-right: 0; }
-  .settings-sidebar__header { display: flex; align-items: center; justify-content: space-between; padding: 0 8px 0 16px; border-bottom: 1px solid var(--line); }.settings-sidebar__header .settings-center__close { display: grid; }
-  .settings-sidebar__scroll { min-height: 0; overflow-y: auto; padding: 18px 20px max(24px, env(safe-area-inset-bottom)); overscroll-behavior: contain; }
-  .settings-center--mobile-detail .settings-sidebar { display: none; }
-  .settings-content { display: none; width: 100%; height: 100%; }
-  .settings-center--mobile-detail .settings-content { display: grid; }
-  .settings-content__header { min-height: 56px; grid-template-columns: 44px minmax(0, 1fr) 44px; gap: 4px; padding: 6px 8px 6px 6px; }
-  .settings-center--mobile-detail .mobile-back { display: grid; }
-  .settings-content__scroll { padding: 22px 20px max(28px, env(safe-area-inset-bottom)); }
-  .settings-content--with-footer { grid-template-rows: auto minmax(0, 1fr) calc(68px + env(safe-area-inset-bottom)); }
-  .settings-content__footer { padding: 0 20px env(safe-area-inset-bottom); }
-  .settings-content__footer button { flex: 1; }
-  .settings-agent-selector,.settings-account-summary { top: -18px; margin-top: -18px; padding-top: 18px; }
-}
+.settings-center-layer{position:fixed;z-index:300;inset:0;display:grid;place-items:center;padding:24px;background:var(--scrim);backdrop-filter:blur(3px)}
+.settings-center{display:flex;flex-direction:column;width:min(900px,calc(100vw - 48px));height:min(650px,calc(100dvh - 64px));overflow:hidden;border:1px solid var(--line);border-radius:14px;outline:0;background:var(--surface);box-shadow:0 24px 72px #0003;color:var(--text-primary)}
+.settings-center__topbar{height:60px;box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;padding:8px 16px 8px 20px;border-bottom:1px solid var(--line)}
+.settings-center__topbar h2{margin:0;font-size:18px;font-weight:650;letter-spacing:-.02em}
+.settings-center__close,.mobile-back{display:grid;width:44px;height:44px;place-items:center;flex-shrink:0;padding:0;border:0;border-radius:9px;background:transparent;color:var(--text-secondary);cursor:pointer}
+.settings-center__close:hover,.mobile-back:hover{background:var(--surface-hover);color:var(--text-primary)}
+.settings-center__close:disabled,.mobile-back:disabled{cursor:not-allowed;opacity:.45}
+.settings-center button:focus-visible,.settings-center input:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
+.mobile-back,.settings-detail-close{display:none}
+.settings-center__body{display:grid;flex:1;min-height:0;grid-template-columns:208px minmax(0,1fr)}
+.settings-sidebar{position:relative;min-height:0;overflow-y:auto;padding:16px 12px;border-right:1px solid var(--line);background:var(--settings-sidebar);overscroll-behavior:contain;scrollbar-width:thin}
+.settings-sidebar__header{display:none}.settings-sidebar__header h2{margin:0;font-size:18px;font-weight:650}
+.settings-sidebar__scroll{min-height:0}
+.settings-search{display:flex;align-items:center;gap:8px;height:38px;padding:0 10px;margin-bottom:22px;border:1px solid var(--line);border-radius:8px;background:var(--settings-panel);color:var(--text-muted)}
+.settings-search input{min-width:0;width:100%;height:100%;padding:0;border:0;outline:none;background:transparent;color:var(--text-primary);font:13px var(--font-ui)}
+.settings-search:focus-within{outline:2px solid var(--accent);outline-offset:2px}
+.settings-agent-selector{position:relative;margin:0 0 20px;padding-bottom:12px;border-bottom:1px solid var(--line)}
+.settings-agent-selector>button{display:grid;width:100%;min-height:44px;grid-template-columns:34px minmax(0,1fr) 16px;align-items:center;gap:8px;padding:2px 4px;border:0;border-radius:9px;background:transparent;color:var(--text-primary);cursor:pointer;text-align:left}
+.settings-agent-selector>button:hover{background:var(--surface-hover)}
+.settings-agent-selector span,.settings-agent-menu span{display:grid;min-width:0;gap:2px}
+.settings-agent-selector strong{overflow:hidden;font-size:13px;text-overflow:ellipsis;white-space:nowrap}.settings-agent-selector small,.settings-agent-menu small{overflow:hidden;color:var(--text-muted);font-size:11px;text-overflow:ellipsis;white-space:nowrap}
+.settings-agent-menu{position:absolute;z-index:5;top:calc(100% - 8px);right:0;left:0;padding:6px;border:1px solid var(--line);border-radius:10px;background:var(--surface);box-shadow:var(--shadow-float)}
+.settings-agent-menu button{display:grid;width:100%;min-height:40px;grid-template-columns:24px minmax(0,1fr) 16px;align-items:center;gap:8px;padding:4px 6px;border:0;border-radius:8px;background:transparent;color:var(--text-primary);cursor:pointer;text-align:left}
+.settings-agent-menu button:hover,.settings-agent-menu button[aria-selected=true]{background:var(--surface-hover)}.settings-agent-menu strong{overflow:hidden;font-size:12px;text-overflow:ellipsis;white-space:nowrap}
+.settings-sidebar nav{display:grid;gap:22px}
+.settings-sidebar nav section{display:grid;gap:3px}.settings-sidebar nav section+section{padding-top:20px;border-top:1px solid var(--line)}
+.settings-sidebar h3{margin:0 10px 6px;color:var(--text-muted);font-size:12px;font-weight:500}
+.settings-sidebar nav button{display:flex;width:100%;min-height:44px;align-items:center;gap:12px;padding:8px 12px;border:0;border-radius:9px;background:transparent;color:var(--text-primary);cursor:pointer;text-align:left;font:500 14px var(--font-ui)}
+.settings-sidebar nav button:hover{background:var(--surface-hover)}.settings-sidebar nav button.active{background:var(--settings-selected);font-weight:600}
+.settings-sidebar nav button span{min-width:0;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.settings-sidebar nav button em{color:var(--text-muted);font:normal 10px var(--font-ui)}
+.settings-search-empty{margin:0 10px;font-size:13px;line-height:1.6;color:var(--text-secondary)}
+.settings-content{display:grid;min-width:0;min-height:0;grid-template-rows:auto minmax(0,1fr);background:var(--surface)}
+.settings-content--with-footer{grid-template-rows:auto minmax(0,1fr) 64px}
+.settings-content__header{display:grid;grid-template-columns:minmax(0,1fr);align-items:center;gap:8px;padding:24px 24px 16px}
+.settings-content__heading{min-width:0}.settings-content__header h3{margin:0;font-size:18px;font-weight:650;letter-spacing:-.01em}.settings-content__header p{margin:6px 0 0;color:var(--text-secondary);font-size:13px;line-height:1.6}
+.settings-content__scroll{min-height:0;overflow-y:auto;padding:8px 24px 24px;overscroll-behavior:contain;scrollbar-width:thin}
+.settings-content__footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:0 24px;border-top:1px solid var(--line);background:var(--surface)}
+.settings-content__footer button{display:inline-flex;min-width:88px;min-height:40px;align-items:center;justify-content:center;padding:0 16px;border-radius:8px;cursor:pointer;font:550 13px var(--font-ui)}
+.settings-footer__cancel{border:1px solid var(--line);background:var(--settings-panel);color:var(--text-primary)}.settings-footer__save{border:0;background:var(--accent);color:var(--text-on-solid)}.settings-content__footer button:disabled{cursor:not-allowed;opacity:.45}
+.settings-empty{margin:0;padding:24px;border-radius:12px;background:var(--settings-panel);color:var(--text-muted);font-size:14px;text-align:center}
+.appearance-panel{display:grid;gap:16px;padding:20px;background:var(--settings-panel);border-radius:12px}.appearance-panel h4{margin:0;font-size:15px;font-weight:600}
+.theme-options{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}
+.theme-options button{display:flex;flex-direction:column;align-items:center;gap:14px;min-width:0;padding:10px 10px 12px;border:2px solid transparent;border-radius:10px;background:var(--surface);color:var(--text-primary);cursor:pointer;font:inherit;box-shadow:0 0 0 1px var(--line)}
+.theme-options button:hover{border-color:var(--line-strong)}.theme-options button.active{border-color:var(--accent);box-shadow:none}.theme-options img{display:block;width:100%;height:auto;aspect-ratio:8/5;object-fit:cover;border-radius:7px}.theme-options strong{font-size:13px;font-weight:550}.theme-choice-mark{width:20px;height:20px;color:var(--text-primary)}
+.appearance-note{display:flex;align-items:flex-start;gap:8px;margin:2px 0 0;padding-top:16px;border-top:1px solid var(--line);color:var(--text-secondary);font-size:13px;line-height:1.6}
+.settings-content__scroll :deep(.account-security-panel>.panel-heading){display:none}.settings-content__scroll :deep(.account-security-panel){gap:20px}.settings-content__scroll :deep(.account-avatar-card),.settings-content__scroll :deep(.security-form),.settings-content__scroll :deep(.server-identity){padding:20px;border:0;border-radius:12px;background:var(--settings-panel)}.settings-content__scroll :deep(.security-form){gap:16px}.settings-content__scroll :deep(.field input){background:var(--surface)}
+.settings-center-fade-enter-active,.settings-center-fade-leave-active{transition:opacity 150ms ease}.settings-center-fade-enter-active .settings-center,.settings-center-fade-leave-active .settings-center{transition:transform 180ms var(--ease-out)}.settings-center-fade-enter-from,.settings-center-fade-leave-to{opacity:0}.settings-center-fade-enter-from .settings-center,.settings-center-fade-leave-to .settings-center{transform:translateY(6px) scale(.99)}
+@media(max-width:767px){.settings-center-layer{padding:0}.settings-center{width:100vw;height:100dvh;border:0;border-radius:0}.settings-center__topbar{display:none}.settings-center__body{display:block;height:100%}.settings-sidebar{display:grid;width:100%;height:100%;box-sizing:border-box;grid-template-rows:60px minmax(0,1fr);overflow:hidden;padding:0;border-right:0}.settings-sidebar__header{display:flex;align-items:center;justify-content:space-between;padding:0 12px 0 20px;border-bottom:1px solid var(--line)}.settings-sidebar__scroll{min-height:0;overflow-y:auto;padding:20px 16px max(24px,env(safe-area-inset-bottom))}.settings-center--mobile-detail .settings-sidebar{display:none}.settings-content{display:none;width:100%;height:100%}.settings-center--mobile-detail .settings-content{display:grid}.settings-content__header{min-height:60px;grid-template-columns:44px minmax(0,1fr) 44px;gap:4px;padding:8px 8px 8px 6px;border-bottom:1px solid var(--line)}.settings-content__header h3{font-size:17px}.settings-content__header p{display:none}.settings-center--mobile-detail .mobile-back,.settings-detail-close{display:grid}.settings-content__scroll{padding:20px 16px max(24px,env(safe-area-inset-bottom))}.settings-content--with-footer{grid-template-rows:auto minmax(0,1fr) calc(64px + env(safe-area-inset-bottom))}.settings-content__footer{padding:0 16px env(safe-area-inset-bottom)}.settings-content__footer button{flex:1}.appearance-panel{padding:14px;gap:14px}.theme-options{gap:8px}.theme-options button{padding:6px 6px 10px;gap:12px}.theme-options strong{font-size:12px}}
+@media(prefers-reduced-motion:reduce){.settings-center-fade-enter-active,.settings-center-fade-leave-active,.settings-center-fade-enter-active .settings-center,.settings-center-fade-leave-active .settings-center{transition:none}}
 </style>

+ 3 - 2
src/client/components/app/WorkspaceShell.vue

@@ -25,6 +25,7 @@ type NavItem = {
 type SettingsPage =
   | 'agent-identity'
   | 'agent-models'
+  | 'account-profile'
   | 'account-security'
   | 'account-mobile'
   | 'appearance'
@@ -269,7 +270,7 @@ function chooseSettingsAction(action: 'settings' | 'bots' | 'about') {
     standalone.value = 'about'
     return
   }
-  openSettings(applicationWorkspace.value ? 'account-security' : 'agent-identity')
+  openSettings(applicationWorkspace.value ? 'account-profile' : 'agent-identity')
 }
 function closeStandalone() { standalone.value = null; void nextTick(() => standaloneReturnFocus.value?.isConnected && standaloneReturnFocus.value.focus()) }
 function closeToolsMenu() { toolsMenuOpen.value = false; void nextTick(() => toolsTrigger?.isConnected && toolsTrigger.focus()) }
@@ -733,7 +734,7 @@ defineExpose({openLocalVm:showLocalVmSettings})
     <Teleport to="body">
       <div v-if="settingsMenuOpen" class="workspace-create-dismiss" @pointerdown.self="closeSettingsMenu" @keydown.esc.prevent.stop="closeSettingsMenu">
         <div ref="settingsMenu" class="workspace-create-menu workspace-settings-menu" :style="settingsMenuPosition" role="menu" aria-label="账号菜单" @keydown="actionMenuKeydown">
-          <button type="button" role="menuitem" @click="chooseSettingsAction('settings')"><AppIcon name="settings" :size="17" />设置</button>
+          <button type="button" role="menuitem" @click="chooseSettingsAction('settings')"><AppIcon name="settings" :size="17" />我的设置</button>
           <button type="button" role="menuitem" @click="chooseSettingsAction('about')"><AppIcon name="info" :size="17" />关于</button>
           <button v-if="canCheckUpdates" type="button" role="menuitem" :disabled="openingUpdate" :aria-busy="openingUpdate" @click="checkForUpdates"><AppIcon name="refresh" :size="17" />{{ openingUpdate ? '正在打开…' : '检测更新' }}</button>
           <p v-if="updateError" class="update-menu-error" role="alert">{{ updateError }}</p>

+ 5 - 2
src/client/components/common/StandaloneDialog.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
 import AppIcon from './AppIcon.vue'
-const props = defineProps<{ title: string; compact?: boolean; beforeClose?: () => boolean }>()
+const props = defineProps<{ title: string; compact?: boolean; settings?: boolean; beforeClose?: () => boolean }>()
 const emit = defineEmits<{ close: [] }>()
 const dialog = ref<HTMLDialogElement>()
 function close() { if (props.beforeClose?.() !== false) emit('close') }
@@ -14,7 +14,7 @@ onMounted(async () => { await nextTick(); dialog.value?.showModal() })
 onBeforeUnmount(() => dialog.value?.close())
 </script>
 <template>
-  <Teleport to="body"><dialog ref="dialog" class="standalone-dialog" :class="{ 'standalone-dialog--compact': compact }" :aria-label="title" @cancel.prevent="close" @click="backdrop">
+  <Teleport to="body"><dialog ref="dialog" class="standalone-dialog" :class="{ 'standalone-dialog--compact': compact, 'standalone-dialog--settings': settings }" :aria-label="title" @cancel.prevent="close" @click="backdrop">
     <header><h2>{{ title }}</h2><button type="button" :aria-label="`关闭${title}`" autofocus @click="close"><AppIcon name="close" :size="20" /></button></header>
     <div class="standalone-dialog__content"><slot /></div>
   </dialog></Teleport>
@@ -22,3 +22,6 @@ onBeforeUnmount(() => dialog.value?.close())
 <style scoped>
 .standalone-dialog{width:min(1040px,calc(100vw - 48px));height:min(780px,calc(100dvh - 48px));max-width:none;max-height:none;margin:auto;padding:0;border:1px solid var(--line);border-radius:22px;background:var(--surface);color:var(--text-primary);box-shadow:0 24px 100px #0005;overflow:hidden}.standalone-dialog[open]{display:flex;flex-direction:column}.standalone-dialog::backdrop{background:#0007;backdrop-filter:blur(3px)}header{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px 24px 16px;border-bottom:1px solid var(--line);flex-shrink:0}h2{font-size:21px;font-weight:650;margin:0;letter-spacing:-.02em}header button{display:grid;place-items:center;width:44px;height:44px;padding:0;flex-shrink:0;border:0;border-radius:10px;color:inherit;background:transparent;cursor:pointer}header button:hover{background:var(--surface-hover)}header button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.standalone-dialog__content{padding:24px;overflow:auto;min-height:0;flex:1}.standalone-dialog--compact{width:min(380px,calc(100vw - 32px));height:fit-content;max-height:calc(100dvh - 32px);border-radius:18px}.standalone-dialog--compact header{padding:10px 12px 10px 20px}.standalone-dialog--compact h2{font-size:15px}.standalone-dialog--compact .standalone-dialog__content{padding:20px 24px 28px}@media(max-width:600px){.standalone-dialog:not(.standalone-dialog--compact){width:100vw;height:100dvh;border:0;border-radius:0}header{padding:10px 12px 10px 20px}h2{font-size:18px}.standalone-dialog__content{padding:20px}}
 </style>
+<style scoped>
+.standalone-dialog--settings{width:min(900px,calc(100vw - 48px));height:min(650px,calc(100dvh - 64px));border-radius:14px;box-shadow:0 24px 72px #0003}.standalone-dialog--settings>header{height:60px;padding:8px 16px 8px 20px;box-sizing:border-box}.standalone-dialog--settings h2{font-size:18px;font-weight:650}.standalone-dialog--settings .standalone-dialog__content{padding:0;overflow:hidden}.standalone-dialog--settings::backdrop{background:var(--scrim);backdrop-filter:blur(3px)}@media(max-width:600px){.standalone-dialog--settings{width:100vw;height:100dvh;border-radius:0}.standalone-dialog--settings>header{padding-left:20px}}
+</style>

Fichier diff supprimé car celui-ci est trop grand
+ 5 - 5
src/client/components/workspace/BotSettingsDialog.vue


Fichier diff supprimé car celui-ci est trop grand
+ 16 - 2
src/client/components/workspace/WorkspaceKnowledgePanel.vue


+ 6 - 0
src/client/styles/tokens.css

@@ -8,6 +8,9 @@
   --surface-raised: #ffffff;
   --surface-soft: #f2f2ef;
   --surface-hover: #ecece8;
+  --settings-panel: #f2f2f4;
+  --settings-sidebar: #fafafa;
+  --settings-selected: #eaeaed;
   --rail-bg: #151515;
   --rail-text: rgba(255, 255, 255, .58);
   --rail-active: #ffffff;
@@ -44,6 +47,9 @@
   --surface-raised: #242423;
   --surface-soft: #282827;
   --surface-hover: #30302e;
+  --settings-panel: #2b2b2e;
+  --settings-sidebar: #202022;
+  --settings-selected: #353538;
   --rail-bg: #0d0d0d;
   --rail-text: rgba(255, 255, 255, .54);
   --rail-active: #ffffff;

+ 18 - 10
tests/client/settingsCenterDialog.test.ts

@@ -81,6 +81,16 @@ afterEach(() => {
 })
 
 describe('Settings center dialog', () => {
+  it('shows theme previews and filters settings without changing account scope', async () => {
+    const wrapper = mountSettings({initialPage:'appearance',themePreference:'light'})
+    expect(wrapper.get('[role="dialog"]').attributes('aria-label')).toBe('我的设置')
+    expect(wrapper.findAll('.theme-options img')).toHaveLength(3)
+    await wrapper.findAll('[role="radio"]').find(button=>button.text()==='深色')!.trigger('click')
+    expect(wrapper.emitted('set-theme')).toEqual([['dark']])
+    await wrapper.get('input[aria-label="搜索设置"]').setValue('Hermes')
+    expect(wrapper.findAll('.settings-sidebar nav button').map(button=>button.text())).toEqual(['Hermes 连接'])
+    wrapper.unmount()
+  })
   it.each([false, true])('keeps independent tools and About outside settings (Bot mode=%s)', botMode => {
     const wrapper = mountSettings({ botMode, initialPage: 'bot-plugins' })
     const labels = wrapper.findAll('.settings-sidebar nav button').map(button => button.text())
@@ -93,9 +103,9 @@ describe('Settings center dialog', () => {
     const wrapper = mountSettings()
 
     expect(wrapper.findAll('.settings-sidebar nav h3').map(heading => heading.text())).toEqual([
-      '当前机器人',
-      '账号',
-      '系统 · 仅管理员',
+      '个人',
+      '管理',
+      '基础机器人',
     ])
 
     await navigationButton(wrapper, '模型与 Provider').trigger('click')
@@ -106,13 +116,11 @@ describe('Settings center dialog', () => {
 
     await navigationButton(wrapper, '登录与安全').trigger('click')
     expect(wrapper.find('.settings-agent-selector').exists()).toBe(false)
-    expect(wrapper.get('.settings-account-summary').text()).toContain('owner')
-    expect(wrapper.get('.settings-account-summary').text()).toContain('当前账号')
-    expect(wrapper.get('.settings-account-summary .account-initial-avatar').text()).toBe('O')
+    expect(wrapper.get('.settings-content__header').text()).toContain('当前账号:owner')
 
     const voiceButton = navigationButton(wrapper, '双流语音')
     expect(voiceButton.text()).toContain('全局')
-    expect(voiceButton.element.closest('section')?.textContent).toContain('系统 · 仅管理员')
+    expect(voiceButton.element.closest('section')?.textContent).toContain('管理')
     await voiceButton.trigger('click')
     expect(wrapper.find('[data-testid="duplex-voice"]').exists()).toBe(true)
     expect(wrapper.get('.settings-content__header').text()).toContain('全局设置 · 仅管理员')
@@ -122,14 +130,14 @@ describe('Settings center dialog', () => {
     const wrapper = mountSettings({ isAdmin: false, initialPage: 'system-update' })
     const navigation = wrapper.get('.settings-sidebar nav')
 
-    expect(navigation.findAll('h3').map(heading => heading.text())).toEqual(['当前机器人', '账号'])
+    expect(navigation.findAll('h3').map(heading => heading.text())).toEqual(['个人'])
     expect(navigation.text()).not.toContain('模型与 Provider')
-    expect(navigation.text()).not.toContain('系统 · 仅管理员')
+    expect(navigation.text()).not.toContain('管理')
     expect(navigation.text()).not.toContain('系统概览')
     expect(navigation.text()).not.toContain('双流语音')
     expect(navigation.text()).not.toContain('更新与回滚')
     expect(wrapper.find('[data-testid="system-update"]').exists()).toBe(false)
-    expect(wrapper.get('.settings-content__header').text()).toContain('身份与头像')
+    expect(wrapper.get('.settings-content__header').text()).toContain('账号资料')
   })
 
   it('blocks closing and page changes while the update panel reports a lock', async () => {

+ 3 - 3
tests/client/workspaceShell.test.ts

@@ -120,7 +120,7 @@ describe('Workspace shell account controls', () => {
     expect(settingsTrigger.attributes('aria-label')).toBe('设置与模式')
     await settingsTrigger.trigger('click')
     const actions = document.querySelectorAll<HTMLButtonElement>('.workspace-settings-menu [role="menuitem"]')
-    expect([...actions].map(button => button.textContent?.trim())).toEqual(['设置', '关于', '检测更新', '帮助', '进入 Bot 模式'])
+    expect([...actions].map(button => button.textContent?.trim())).toEqual(['我的设置', '关于', '检测更新', '帮助', '进入 Bot 模式'])
     actions[0]!.click()
     await wrapper.vm.$nextTick()
     const settings = wrapper.get('[data-testid="settings-center"]')
@@ -168,10 +168,10 @@ it('uses the reference Bot list header and keeps mode changes in settings', asyn
   expect(rail.find('.sidebar-context__heading').exists()).toBe(false)
   await rail.get('.sidebar-account-switcher__main').trigger('click')
   const accountItems = [...document.querySelectorAll<HTMLElement>('.workspace-settings-menu [role="menuitem"]')]
-  expect(accountItems.map(item => item.textContent?.trim())).toEqual(['设置', '关于', '检测更新', '帮助', '进入聊天模式'])
+  expect(accountItems.map(item => item.textContent?.trim())).toEqual(['我的设置', '关于', '检测更新', '帮助', '进入聊天模式'])
   expect(document.querySelector<HTMLAnchorElement>('.workspace-settings-menu a')?.href).toBe('https://yaoyao.samien.cn/')
   accountItems[0]!.click(); await wrapper.vm.$nextTick()
-  expect(wrapper.get('[data-testid="settings-center"]').attributes('data-page')).toBe('account-security')
+  expect(wrapper.get('[data-testid="settings-center"]').attributes('data-page')).toBe('account-profile')
   await wrapper.get('[data-testid="close-settings"]').trigger('click')
   await rail.get('.sidebar-create-trigger').trigger('click')
   const items = document.querySelectorAll<HTMLButtonElement>('.workspace-create-menu [role="menuitem"]')

+ 2 - 0
tests/fixtures/settings-design/index.html

@@ -0,0 +1,2 @@
+<!doctype html>
+<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>设置界面验证</title></head><body><div id="app"></div><script type="module" src="./main.ts"></script></body></html>

+ 60 - 0
tests/fixtures/settings-design/main.ts

@@ -0,0 +1,60 @@
+// Browser-only visual fixture. All /api requests stay in memory; no user data is changed.
+import {createApp, defineComponent, h, ref} from 'vue'
+import {createPinia} from 'pinia'
+import BotSettingsDialog from '@/components/workspace/BotSettingsDialog.vue'
+import SettingsCenterDialog from '@/components/app/SettingsCenterDialog.vue'
+import {useAuthStore} from '@/stores/auth'
+import '@/styles/tokens.css'
+import '@/styles/global.css'
+
+const query=new URLSearchParams(location.search)
+const names=['测试2','竹儿','测试','瑶儿']
+const agents=names.map((name,i)=>({id:`00000000-0000-4000-8000-00000000000${i+1}`,name,profile:'default',nodeId:'local',archived:false}))
+const conversations=[{id:'group-a',name:'方案分析团队',kind:'group',memberIds:agents.map(a=>a.id),archived:false},{id:'group-b',name:'内容运营组',kind:'group',memberIds:agents.slice(0,3).map(a=>a.id),archived:false}]
+const projects=query.has('empty')?[]:[
+  {id:'project-a',name:'夭夭产品开发',description:'',memberIds:agents.map(a=>a.id),groupIds:['group-a','group-b'],revision:1,archived:false,createdAt:1,updatedAt:1},
+  {id:'project-b',name:'内容运营',description:'',memberIds:agents.slice(0,3).map(a=>a.id),groupIds:['group-b'],revision:1,archived:false,createdAt:1,updatedAt:1},
+]
+const identity={serverId:'00000000-0000-4000-8000-000000000099',name:'我的工作空间',displayName:'我的工作空间',revision:1}
+const realFetch=window.fetch.bind(window)
+window.fetch=async(input,init)=>{
+  const url=new URL(typeof input==='string'?input:input instanceof URL?input.href:input.url,location.href)
+  if(!url.pathname.startsWith('/api/'))return realFetch(input,init)
+  const body=typeof init?.body==='string'?JSON.parse(init.body):{}
+  let data:unknown
+  if(url.pathname==='/api/app/capabilities')data={features:['bot-file-memory-v1']}
+  else if(url.pathname==='/api/app/bootstrap')data={csrfToken:'fixture-only',userId:'fixture-user',authRequired:true}
+  else if(url.pathname==='/api/app/agents')data={agents}
+  else if(url.pathname==='/api/app/conversations')data={conversations}
+  else if(url.pathname==='/api/app/workspace/projects'){
+    if(init?.method==='POST'){
+      const previous=projects.find(p=>p.id===body.id)
+      const project={...body,id:body.id??crypto.randomUUID(),revision:(previous?.revision??0)+1,createdAt:1,updatedAt:1}
+      if(previous)Object.assign(previous,project);else projects.push(project)
+      data={project}
+    }else data={projects}
+  }else if(url.pathname.includes('/memory-jobs'))data={jobs:[]}
+  else if(url.pathname.includes('/memories'))data={memories:[]}
+  else if(url.pathname.includes('server-identity'))data=identity
+  else if(url.pathname==='/api/app/computers')data={computers:[]}
+  else if(url.pathname==='/api/app/admin/local-vm')data={configured:true,enabled:true,mode:'shared',maxInstances:2,idleStopMinutes:0,runtime:'docker',daemonUp:true,image:true,images:[{key:'standard',ready:true},{key:'cursor',ready:true}],instances:[]}
+  else return new Response(JSON.stringify({error:{message:'此预览没有连接服务'}}),{status:404,headers:{'content-type':'application/json'}})
+  return new Response(JSON.stringify(data),{headers:{'content-type':'application/json'}})
+}
+const theme=ref<'light'|'dark'|'system'>(query.get('theme')==='dark'?'dark':'light')
+function setTheme(value:'light'|'dark'|'system'){theme.value=value;document.documentElement.classList.toggle('dark',value==='dark')}
+setTheme(theme.value)
+const App=defineComponent({setup(){
+  const auth=useAuthStore()
+  auth.user={id:'fixture-user',username:'samien',role:query.has('member')?'member':'admin'} as never
+  auth.status='authenticated';auth.serverIdentity=identity
+  const opened=ref<'bot'|'personal'|null>(query.get('view')==='appearance'||query.get('view')==='profile'?'personal':'bot')
+  return()=>h('main',{style:'min-height:100dvh;background:var(--settings-panel);padding:18px;box-sizing:border-box'},[
+    h('div',{style:'display:flex;gap:12px;align-items:center;font:13px var(--font-ui)'},[
+      h('span','界面验证 · 示例数据'),h('button',{onClick:()=>opened.value='bot'},'Bot 设置'),h('button',{onClick:()=>opened.value='personal'},'我的设置'),
+    ]),
+    opened.value==='bot'?h(BotSettingsDialog,{isAdmin:!query.has('member'),onClose:()=>opened.value=null}):null,
+    h(SettingsCenterDialog,{open:opened.value==='personal',initialPage:query.get('view')==='profile'?'account-profile':'appearance',isAdmin:!query.has('member'),botMode:true,userName:'samien',theme:theme.value==='dark'?'dark':'light',themePreference:theme.value,profiles:[{name:'default',agentName:'默认机器人',isDefault:true}],activeProfile:{name:'default',agentName:'默认机器人',isDefault:true},onClose:()=>opened.value=null,'onSet-theme':setTheme}),
+  ])
+}})
+createApp(App).use(createPinia()).mount('#app')

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff