Sfoglia il codice sorgente

发布 v0.2.18 资源导航与设置中心版本

LSamien 2 mesi fa
parent
commit
ed2e7cd201

+ 1 - 1
README.md

@@ -78,7 +78,7 @@ ID 和包名。浏览器只提交路径和公开元数据;8800 本地校验 RS
 插件安装接口安装。默认源为
 `https://git.samien.cn/samien/hermes-yaoyao.git#hermes-plugins/yaoyao`。
 
-当前发布版本:**Git `v0.2.17` / 夭夭 Web `0.2.17` / Hermes Dashboard 插件 `1.7.3`**。版本组合由仓库根目录的 `release.json` 唯一声明,并由 `npm run release:verify` 校验。
+当前发布版本:**Git `v0.2.18` / 夭夭 Web `0.2.18` / Hermes Dashboard 插件 `1.7.3`**。版本组合由仓库根目录的 `release.json` 唯一声明,并由 `npm run release:verify` 校验。
 
 需要由自动化 Agent 部署或升级时,请直接使用 [Agent 安装手册](docs/agent-install.md)。其中包含固定版本校验、备份、同步、单一 Dashboard 重载与验证步骤。
 

+ 5 - 5
docs/agent-install.md

@@ -6,8 +6,8 @@
 
 | 项目 | 版本 | 校验位置 |
 | --- | --- | --- |
-| Git 发布标签 | `v0.2.17` | `release.json` 的 `gitTag` 与 `git describe --tags --exact-match HEAD` |
-| 夭夭 Web | `0.2.17` | `release.json` 与 `package.json` 的 `version` |
+| Git 发布标签 | `v0.2.18` | `release.json` 的 `gitTag` 与 `git describe --tags --exact-match HEAD` |
+| 夭夭 Web | `0.2.18` | `release.json` 与 `package.json` 的 `version` |
 | Hermes Dashboard 插件 | `1.7.3` | `hermes-plugins/yaoyao/dashboard/manifest.json` 的 `version` |
 
 以下步骤以仓库根目录为工作目录。不要使用浮动分支替代发布标签。
@@ -78,12 +78,12 @@ curl --fail --silent --show-error http://127.0.0.1:9119/api/auth/providers
 ## 1. 获取并校验指定发布版本
 
 ```bash
-RELEASE_VERSION=v0.2.17
+RELEASE_VERSION=v0.2.18
 git fetch --tags
 git checkout "$RELEASE_VERSION"
 
 test "$(git describe --tags --exact-match HEAD)" = "$RELEASE_VERSION"
-test "$(node -p \"require('./package.json').version\")" = "0.2.17"
+test "$(node -p \"require('./package.json').version\")" = "0.2.18"
 test "$(node -e \"console.log(require('./hermes-plugins/yaoyao/dashboard/manifest.json').version)\")" = "1.7.3"
 npm run release:verify
 ```
@@ -247,7 +247,7 @@ Git、发布目录、命令输出或日志。配置后检查系统管理中的 A
 
 ## 7. 通过受管服务进行配套升级
 
-首次按第 6 步安装 Web `0.2.17` 后,可在左下角 Agent 菜单打开“系统更新”。
+首次按第 6 步安装 Web `0.2.18` 后,可在左下角 Agent 菜单打开“系统更新”。
 检查更新只读取固定的 `HERMES_YAOYAO_RELEASE_SOURCE`,并锁定最新 `vX.Y.Z`
 标签解析到的 Git 提交;执行升级前仍会检查插件持久目录是否安全。
 

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.2.17",
+  "version": "0.2.18",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "hermes-yaoyao",
-      "version": "0.2.17",
+      "version": "0.2.18",
       "license": "BSL-1.1",
       "dependencies": {
         "@koa/bodyparser": "^5.0.0",

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "hermes-yaoyao",
-  "version": "0.2.17",
+  "version": "0.2.18",
   "private": true,
   "license": "BSL-1.1",
   "description": "夭夭 Web:直连 Hermes 9119 的聊天、团队、文件库与产物工作台",

+ 3 - 3
release.json

@@ -1,7 +1,7 @@
 {
   "schemaVersion": 1,
-  "releaseVersion": "0.2.17",
-  "webVersion": "0.2.17",
+  "releaseVersion": "0.2.18",
+  "webVersion": "0.2.18",
   "pluginVersion": "1.7.3",
-  "gitTag": "v0.2.17"
+  "gitTag": "v0.2.18"
 }

+ 65 - 67
src/client/components/app/SettingsCenterDialog.vue

@@ -252,53 +252,54 @@ watch(() => [props.open, props.initialPage] as const, ([open, initialPage]) => {
           :class="{ 'settings-center--mobile-detail': mobileDetailOpen }"
           role="dialog"
           aria-modal="true"
-          aria-labelledby="settings-center-title"
+          aria-label="设置中心"
           tabindex="-1"
           @keydown.esc.capture.prevent.stop="handleEscape"
         >
-          <header class="settings-center__header">
-            <button v-if="mobileDetailOpen" class="mobile-back" type="button" aria-label="返回设置分类" :disabled="updateLocked" @click="backToMenu"><AppIcon name="chevron-left" :size="20" /></button>
-            <h2 id="settings-center-title">设置中心</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="设置分类">
-              <div 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="46" />
-                  <span><strong>{{ profileTitle(activeProfile) }}</strong><small>{{ activeProfile?.name || '未选择 Agent' }}</small></span>
-                  <AppIcon name="chevron-down" :size="16" />
-                </button>
-                <div v-if="profileMenuOpen" class="settings-agent-menu" role="listbox" aria-label="切换正在设置的 Agent" @keydown="handleAgentMenuKeydown">
-                  <button v-for="profile in profiles" :key="profile.name" type="button" role="option" :aria-selected="profile.name === activeProfile?.name" @click="selectProfile(profile.name)">
-                    <AgentAvatar :name="profileTitle(profile)" :avatar="profile.agentAvatar || ''" :size="28" />
-                    <span><strong>{{ profileTitle(profile) }}</strong><small>{{ profile.name }}</small></span>
-                    <AppIcon v-if="profile.name === activeProfile?.name" name="check" :size="16" />
+              <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>
+              </header>
+              <div class="settings-sidebar__scroll">
+                <div 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" />
+                    <span><strong>{{ profileTitle(activeProfile) }}</strong><small>{{ activeProfile?.name || '未选择 Agent' }}</small></span>
+                    <AppIcon name="chevron-down" :size="14" />
                   </button>
+                  <div v-if="profileMenuOpen" class="settings-agent-menu" role="listbox" aria-label="切换正在设置的 Agent" @keydown="handleAgentMenuKeydown">
+                    <button v-for="profile in profiles" :key="profile.name" type="button" role="option" :aria-selected="profile.name === activeProfile?.name" @click="selectProfile(profile.name)">
+                      <AgentAvatar :name="profileTitle(profile)" :avatar="profile.agentAvatar || ''" :size="24" />
+                      <span><strong>{{ profileTitle(profile) }}</strong><small>{{ profile.name }}</small></span>
+                      <AppIcon v-if="profile.name === activeProfile?.name" name="check" :size="14" />
+                    </button>
+                  </div>
                 </div>
-              </div>
 
-              <nav>
-                <section>
-                  <h3>当前 Agent</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>
-                <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>
-                <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>
-              </nav>
+                <nav>
+                  <section>
+                    <h3>当前 Agent</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>
+                  <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>
+                  <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>
+                </nav>
+              </div>
             </aside>
 
             <main class="settings-content" :class="{ 'settings-content--with-footer': showFixedFooter }">
               <header class="settings-content__header">
-                <h3 ref="contentTitle" tabindex="-1">{{ activeTitle }}</h3>
-                <p>{{ activeScope }}</p>
+                <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>
               </header>
               <div class="settings-content__scroll">
                 <AgentIdentityPanel
@@ -370,26 +371,26 @@ watch(() => [props.open, props.initialPage] as const, ([open, initialPage]) => {
 </template>
 
 <style scoped>
-.settings-center-layer { position: fixed; z-index: 300; inset: 0; display: grid; place-items: center; padding: 48px; background: color-mix(in srgb, #000 24%, transparent); backdrop-filter: blur(4px); }
-.settings-center { display: grid; width: min(920px, calc(100vw - 96px)); height: min(680px, calc(100dvh - 96px)); overflow: hidden; grid-template-rows: 56px minmax(0, 1fr); 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__header { display: flex; align-items: center; justify-content: space-between; padding: 0 16px 0 20px; border-bottom: 1px solid var(--line); }
-.settings-center__header h2 { margin: 0; font-size: 20px; letter-spacing: -.025em; }
+.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; min-height: 0; grid-template-columns: 248px minmax(0, 1fr); }
-.settings-sidebar { position: relative; min-height: 0; overflow-y: auto; padding: 12px 18px 14px; border-right: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 42%, var(--surface-raised)); }
-.settings-agent-selector { position: sticky; z-index: 3; top: -12px; margin: -12px 0 10px; padding: 12px 0 10px; border-bottom: 1px solid var(--line); background: color-mix(in srgb, var(--surface-soft) 42%, var(--surface-raised)); }
-.settings-agent-selector > button { display: grid; width: 100%; min-height: 52px; grid-template-columns: 46px minmax(0, 1fr) 20px; align-items: center; gap: 12px; padding: 3px 6px; border: 0; border-radius: 10px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
+.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-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: 3px; }
-.settings-agent-selector strong { overflow: hidden; font-size: 17px; text-overflow: ellipsis; white-space: nowrap; }
-.settings-agent-selector small,.settings-agent-menu small { overflow: hidden; color: var(--text-muted); font-size: 13px; text-overflow: ellipsis; white-space: nowrap; }
+.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: 46px; grid-template-columns: 28px minmax(0, 1fr) 18px; align-items: center; gap: 10px; padding: 6px 8px; border: 0; border-radius: 8px; background: transparent; color: var(--text-primary); cursor: pointer; text-align: left; }
+.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: 13px; text-overflow: ellipsis; white-space: nowrap; }
+.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; }
@@ -402,10 +403,11 @@ watch(() => [props.open, props.initialPage] as const, ([open, initialPage]) => {
 .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 { padding: 21px 30px 14px; border-bottom: 1px solid var(--line); }
-.settings-content__header h3 { margin: 0; font-size: 23px; letter-spacing: -.035em; }
-.settings-content__header p { margin: 8px 0 0; color: var(--text-secondary); font-size: 14px; }
-.settings-content__scroll { min-height: 0; overflow-y: auto; padding: 18px 30px 20px; overscroll-behavior: contain; }
+.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); }
@@ -427,27 +429,23 @@ watch(() => [props.open, props.initialPage] as const, ([open, initialPage]) => {
 .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: 24px; }
-  .settings-center { width: min(920px, calc(100vw - 48px)); height: min(680px, calc(100dvh - 48px)); }
-  .settings-center__body { grid-template-columns: 240px minmax(0, 1fr); }
-  .settings-sidebar { padding-inline: 18px; }
-  .settings-content__header { padding-inline: 30px; }
-  .settings-content__scroll { padding-inline: 30px; }
+  .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; grid-template-rows: 56px minmax(0, 1fr); border: 0; border-radius: 0; }
-  .settings-center__header { padding: 0 8px 0 16px; }
-  .settings-center__header h2 { font-size: 18px; }
-  .settings-center--mobile-detail .settings-center__header { display: grid; grid-template-columns: 44px minmax(0, 1fr) 44px; gap: 4px; padding-left: 6px; }
-  .settings-center--mobile-detail .mobile-back { display: grid; }
-  .settings-center__body { display: block; }
-  .settings-sidebar { width: 100%; height: 100%; box-sizing: border-box; border-right: 0; padding: 18px 20px max(24px, env(safe-area-inset-bottom)); }
+  .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 { padding: 22px 20px 17px; }
-  .settings-content__header h3 { font-size: 21px; }
+  .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); }

+ 4 - 1
src/client/components/app/SystemManagementPanel.vue

@@ -332,7 +332,7 @@ watch(() => [props.active, props.section] as const, ([active, section]) => {
           <button class="danger" type="button" :aria-label="`删除用户 ${user.username}`" :disabled="busy" @click="remove(user)">删除</button>
         </template>
       </div>
-      <form @submit.prevent="add">
+      <form class="user-create-form" aria-label="创建用户" @submit.prevent="add">
         <label><span>新用户名</span><input v-model="username" name="managed-username" autocomplete="off" :disabled="busy" /></label>
         <label><span>临时密码</span><input v-model="password" name="managed-temporary-password" type="password" autocomplete="new-password" :disabled="busy" /><small>至少 8 位</small></label>
         <button class="solid-button" :disabled="busy || !username.trim() || password.length < 8">创建用户</button>
@@ -451,12 +451,15 @@ watch(() => [props.active, props.section] as const, ([active, section]) => {
 form { display: grid; grid-template-columns: 1fr 1fr auto; align-items: end; gap: 10px; margin-top: 16px; }
 form > label { display: grid; min-width: 0; gap: 6px; color: var(--text-secondary); font-size: 12px; font-weight: 650; }
 form > label small { color: var(--text-muted); font-size: 11px; font-weight: 400; }
+.user-create-form { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; gap: 12px; margin-top: 18px; padding: 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-soft); }
+.user-create-form .solid-button { grid-column: 1 / -1; min-width: 104px; justify-self: end; }
 input:not([type="checkbox"]) { min-width: 0; height: 42px; padding: 0 11px; border: 1px solid var(--line); border-radius: 9px; outline: 0; background: var(--surface-raised); color: var(--text-primary); font: 13px var(--font-ui); }
 input:not([type="checkbox"]):focus { border-color: var(--line-strong); box-shadow: 0 0 0 3px var(--focus-ring); }
 .solid-button { min-height: 42px; padding: 0 14px; border: 0; border-radius: 9px; background: var(--accent); color: var(--text-on-solid); font: 600 13px var(--font-ui); }
 .solid-button:disabled,.user button:disabled { cursor: not-allowed; opacity: .5; }
 @media (max-width: 600px) {
   form { grid-template-columns: 1fr; }
+  .user-create-form { grid-template-columns: 1fr; }
   .user { flex-wrap: wrap; padding: 8px 0; }
   .user span { flex-basis: 100%; }
   .push-status { grid-template-columns: 1fr; }

+ 24 - 28
tests/e2e/workspace.spec.ts

@@ -77,8 +77,9 @@ test('navigates every 9119 workspace without blank transitions', async ({ page }
   await expect(page.getByRole('button', { name: '文件库', exact: true })).toHaveCount(0)
   await expect(page.getByRole('button', { name: '产物', exact: true })).toHaveCount(0)
   await expect(page.getByRole('button', { name: '新建团队' })).toBeVisible()
+  await page.mouse.move(1200, 20)
   await expect(page.getByRole('button', { name: '新建团队' })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
-  await expect(page.getByRole('button', { name: '新建团队' })).toHaveCSS('font-weight', '580')
+  await expect(page.getByRole('button', { name: '新建团队' })).toHaveCSS('font-weight', '650')
   await expect(page.getByRole('button', { name: '管理团队', exact: true })).toBeVisible()
   await expect(page.getByRole('button', { name: '发送消息' })).toBeVisible()
 
@@ -128,25 +129,27 @@ test('uses the active session or group title in the browser title', async ({ pag
   await expect(page).toHaveTitle('设计与工程协作 · 夭夭')
 })
 
-test('uses a floating search dialog for group rooms', async ({ page }) => {
+test('uses split search for topics, teams, and archived content', async ({ page }) => {
   await page.getByRole('button', { name: '团队' }).click()
   await expect(page.getByRole('heading', { name: '设计验收' })).toBeVisible()
 
   await page.locator('.desktop-sidebar').getByRole('button', { name: '搜索', exact: true }).click()
-  const searchDialog = page.getByRole('dialog', { name: '搜索团队' })
+  const searchDialog = page.getByRole('dialog', { name: '搜索话题、团队或归档' })
   await expect(searchDialog).toBeVisible()
-  await searchDialog.getByPlaceholder('搜索团队').fill('设计')
-  const roomResult = searchDialog.getByRole('option', { name: /设计与工程协作/ })
-  await expect(roomResult).toBeVisible()
-  await roomResult.click()
+  await expect(searchDialog.getByRole('navigation', { name: '搜索范围' })).toContainText('设计与工程协作')
+  await searchDialog.getByRole('searchbox', { name: '搜索全部' }).fill('发布检查')
+  const topicResult = searchDialog.getByRole('option', { name: /发布检查/ })
+  await expect(topicResult).toBeVisible()
+  await topicResult.click()
+  await expect(page).toHaveURL(/\/groups\/22222222-2222-4222-8222-222222222222\/88888888-8888-4888-8888-888888888888$/)
   await expect(searchDialog).toBeHidden()
 })
 
 test('shows the team animal avatar in the list and five choices during creation', async ({ page }) => {
   await page.getByRole('button', { name: '团队' }).click()
 
-  const roomRow = page.locator('[data-sidebar-id="22222222-2222-4222-8222-222222222222"]')
-  const roomAvatar = roomRow.getByRole('img', { name: '设计与工程协作团队头像' })
+  const topicRow = page.locator('[data-sidebar-id="topic:22222222-2222-4222-8222-222222222222:77777777-7777-4777-8777-777777777777"]')
+  const roomAvatar = topicRow.getByRole('img', { name: '设计验收团队头像' })
   await expect(roomAvatar).toBeVisible()
   await expect(roomAvatar.locator('.team-avatar__image')).toBeVisible()
   await expect(roomAvatar).toHaveCSS('border-radius', '50%')
@@ -608,20 +611,15 @@ test('selects existing group topics and creates a new protocol v4 topic on first
 
   await page.getByRole('button', { name: '团队' }).click()
   const sidebar = page.locator('.desktop-sidebar')
-  const roomItem = sidebar.locator(`[data-sidebar-id="${roomId}"]`)
   const existingTopic = sidebar.locator(`[data-sidebar-id="${existingTopicSidebarId}"]`)
-  await roomItem.click()
-  const backToRooms = sidebar.locator('[data-sidebar-id="group-list"]')
-  await expect(backToRooms).toContainText('返回团队列表')
   await expect(sidebar.getByRole('button', { name: '返回对话' })).toBeVisible()
   await expect(sidebar.getByRole('button', { name: '折叠侧边栏' })).toBeVisible()
-  await expect(sidebar.getByText('设计与工程协作', { exact: true })).toBeVisible()
-  await expect(sidebar.getByText('2 个 Agent', { exact: true })).toBeVisible()
-  await expect(sidebar.getByRole('button', { name: '搜索', exact: true })).toBeHidden()
-  await expect(sidebar.getByRole('button', { name: '新建团队' })).toBeHidden()
-  await expect(sidebar.getByRole('button', { name: '已归档' })).toBeHidden()
-  await expect(sidebar.getByRole('button', { name: '对话', exact: true })).toBeHidden()
-  await expect(sidebar.locator('.sidebar-footer')).toBeHidden()
+  await expect(sidebar.getByText('话题列表', { exact: true })).toBeVisible()
+  await expect(sidebar.getByRole('button', { name: '搜索', exact: true })).toBeVisible()
+  await expect(sidebar.getByRole('button', { name: '新建话题' })).toBeVisible()
+  await expect(sidebar.getByRole('button', { name: '新建团队' })).toBeVisible()
+  await expect(sidebar.getByRole('button', { name: '对话', exact: true })).toBeVisible()
+  await expect(sidebar.locator('.sidebar-footer')).toBeVisible()
   await expect(existingTopic).toContainText('发布检查')
   await expect(existingTopic).toHaveClass(/sidebar-item--topic/)
 
@@ -644,12 +642,10 @@ test('selects existing group topics and creates a new protocol v4 topic on first
   await expect(timeline.getByText('请核对发布话题的独立历史。', { exact: true })).toBeVisible()
   await expect(timeline.getByText('大家好,检查一下群聊输入框。', { exact: true })).toHaveCount(0)
 
-  await backToRooms.click()
-  await expect(sidebar.getByRole('button', { name: '搜索', exact: true })).toBeVisible()
-  await expect(sidebar.getByRole('button', { name: '新建团队' })).toBeVisible()
-  await roomItem.click({ button: 'right' })
-  const roomActions = page.getByRole('menu', { name: '团队操作' })
-  await roomActions.getByRole('menuitem', { name: '新建话题' }).click()
+  await sidebar.getByRole('button', { name: '新建话题' }).click()
+  const teamPicker = page.getByRole('dialog', { name: '选择团队' })
+  await expect(teamPicker).toBeVisible()
+  await teamPicker.locator('.topic-team-picker__choose').filter({ hasText: '设计与工程协作' }).click()
   const draftTopic = sidebar.locator('.sidebar-item--topic').filter({ hasText: '新话题' })
   await expect(draftTopic).toBeVisible()
   const draftSidebarId = await draftTopic.getAttribute('data-sidebar-id')
@@ -852,7 +848,7 @@ test('keeps the canonical logo and yaoyao-webui composer geometry', async ({ pag
   await expect(page.locator('.composer-context')).toContainText('剩余 256.0k')
   await page.getByRole('button', { name: '推理强度:默认' }).click()
   await expect(page.locator('.composer-popover--reasoning')).toBeVisible()
-  await page.getByRole('button', { name: '设置' }).click()
+  await page.getByRole('button', { name: '设置', exact: true }).click()
   await expect(page.locator('.composer-popover--settings')).toBeVisible()
   await expect(page.getByText('语音输入设置', { exact: true })).toBeVisible()
   await page.locator('.composer-shell').click()
@@ -941,7 +937,7 @@ test('uses the unified Grok-style sidebar, search trigger, and persistent collap
   await expect(page.getByRole('button', { name: '搜索', exact: true })).toBeVisible()
   await expect(page.getByRole('button', { name: '新建聊天', exact: true })).toBeVisible()
   await expect(page.getByRole('button', { name: '新建聊天', exact: true })).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)')
-  await expect(page.getByRole('button', { name: '新建聊天', exact: true })).toHaveCSS('font-weight', '580')
+  await expect(page.getByRole('button', { name: '新建聊天', exact: true })).toHaveCSS('font-weight', '650')
   await expect(sidebar.getByText('历史记录', { exact: true })).toBeVisible()
 
   const searchTrigger = page.getByRole('button', { name: '搜索', exact: true })