# 「瓮城」控制面板 — UI 设计规格与实现对照 > 版本:2026-11-01 UI 重设计(工作区改动,未发布) > 范围:只改 UI 层 `client.js` 与本文件;`src/` 运行时、协议、路由均未改动。 > 基线:改前截图的四个问题——会话标题全为「GPT」、右栏 `分身会话不可读: bad-json`、 > 僵尸任务 `adapter http-404` 埋在元信息里、顶栏 chips 与主操作层级偏弱。 --- ## 0. 本次改动清单 | 文件 | 改动 | |---|---| | `client.js` | 控制面板重构:标题降级、错误可读可重试、状态语义拆分、3:7 工作台层级、搜索/折叠/回顶、响应式、键盘可达、暗色安全的强调色用法 | | `docs/UI-瓮城入口.md` | 本规格(设计基础、组件、状态、响应式、无障碍、根因与验收) | **未改**:`src/**`、`cordis.patch.yml`、`package.json`、协议字段、API 路由。 **未提交**:改动留在工作区,由 Lead 建分支 + PR。 --- ## 1. 平台事实(未变,决定实现形态) | # | 事实 | 后果 | |---|---|---| | F1 | 左侧导航真实槽位是 `sidebar.panellist`;登记项只接收 `{id, order, label}`,图标 = 登记项组件本身 | 「瓮城」= 一个 panel 行;红门图标由插件组件渲染 | | F2 | 行点击归外壳所有(`onClick → selectPanel(id)`) | 双击只能在图标这一层挂;单击进面板 = 平台行为 | | F3 | 中列面板槽是 `main`(root keyed,登记 `{key}`) | 必须同时登记 `main.key='fang'` 与 `sidebar.panellist.id='fang'` | | F4 | 主题颜色 token 是 `--dsw-alias-*`;`--dsh-*` 只承载排版量 | 颜色全部走 alias token + 回退;红色是插件艺术色 `--fang-gate-accent` | | F5 | 客户端模块仍要求 loader 行是裸包名(`@local/dsh-fang`) | 三个 UI 入口共用同一个 `client.js` | | F6 | `window.open` 在 5–20s 派生 await 之后会脱离用户手势 | 面板内必须有「打开分身 ↗」兜底链接 | ### 三处入口(同一 Client 模块) | 入口 | 槽位 | 单击 | 双击 | |---|---|---|---| | 左侧导航「瓮城」 | `sidebar.panellist` id=`fang` order=90 | 外壳切到面板 | 图标上挂双击 → 快速派生 + 打开标签页 | | 中列面板 | `main` key=`fang` | — | — | | 输入框 dock 徽标 | `conversation.composer.dock` id=`fang-badge` | 打开面板 | 快速派生(可发现性放在 `title`) | 双击判定:400ms 窗口、6px 位移容差、带修饰键不成立、派生后 2s 冷却、进行中单飞合并。 **面板标题栏的城门图标不再挂派生手势**(避免误触);双击只发生在左侧导航图标与 dock 徽标。 --- ## 2. 设计目标(按优先级) 1. **主流程一眼可读**:左栏选会话 → 底部确认条 →「继续开发 →」派生 → 右栏出现分身项目。 2. **层级**:品牌 → 状态组 → 主动作 → 次动作;每栏「标题 / 副标题 / 计数」两级信息。 3. **状态语义**:生命周期、适配器健康、数据读取状态分别表达,不把异常藏进一行灰色元信息。 4. **错误可读可重试**:把后端 code 翻译成人能读懂的原因,保留原始诊断,就地重试;不拖垮整列。 5. **无障碍与响应式**:键盘可选择、可取消、可折叠;≤980px 上下堆叠且两栏仍各自滚动。 --- ## 3. 信息架构与线框 保留 3:7 分屏:左 = 母体项目 & 会话,右 = 分身项目 & 会话。两栏独立滚动,面板本身不滚动。 ``` ┌ 城门 瓮城 [母体 只读 | 就绪 0/8 | 运行中 2] [刷新] [+ 派生新分身] ┐ │ 母体 × 分身 · 双生控制台 │ ├ 快速派生结果条 / 状态失败条 / 操作提示(按需出现)──────────────────────────────────────────┤ │ ┌ 母体 ────────────────────────────┐ ┌ 分身 ───────────────────────────────────────────────┐ │ │ │ 项目 & 会话 · 选中会话后继续开发 30/268 │ │ 每个任务一个项目 · 会话直接打开 2 项目 · 2 异常 │ │ │ │ [搜索 标题 / 短 ID / 工作区] │ │ │ │ │ │ ▾ GPT 30 11-01 16:20 │ │ ⚠ 分身会话接口不可用(HTTP 404) [全部重试] │ │ │ │ 未命名会话 30465d81 已选中 │ │ ┌ 从会话继续 · … [运行中][适配器失联] [打开][归档] ┐ │ │ │ │ GPT · 11-01 16:20 [选择 ›] │ │ │ ⚠ 适配器失联 · 疑似僵尸分身 http-404 │ │ │ │ │ 未命名会话 a42e92ac │ │ │ ▾ 分身会话 [读取失败] [刷新] │ │ │ │ │ GPT · 11-01 15:43 [选择 ›] │ │ │ ⚠ 分身会话读取失败 · http-404 … │ │ │ │ │ … │ │ └──────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────┘ └────────────────────────────────────────────────────┘ │ ├ 已选 · 母体会话 未命名会话 30465d81 [取消] [继续开发 →] ┤ └ 双击左侧「瓮城」图标或输入框徽标 = 快速派生 · 两栏各自滚动 v2.1.0 独立门户 ↗ ┘ ``` ### 布局规则 - 桌面:`grid-template-columns: minmax(0,3fr) minmax(0,7fr)`,gap 12;两栏 `min-height:0`,内部 `overflow-y:auto` + `overscroll-behavior:contain`。 - 小屏(≤980px):单列堆叠;左栏 `height: clamp(240px,44vh,460px)`、右栏 `clamp(260px,54vh,560px)`;外层可滚动;**选中确认条插在左栏与右栏之间**,避免出现在首屏之外。 - 面板圆角 12、卡片圆角 10;列表行用发丝分隔线,不额外加卡片阴影。 --- ## 4. 设计基础 ### 4.1 颜色(只走宿主 token,红色是插件艺术色) | 角色 | 取值 | 用途 | |---|---|---| | 品牌红 | `--fang-gate-accent`(回退 `#C62828`) | CTA 填充、选中左轨、城门、焦点环、品牌标记;**不用于小号正文** | | 主文本 | `--dsw-alias-label-primary`(`#1f2328`) | 标题、行标题、主要按钮文字 | | 次文本 | `--dsw-alias-label-secondary`(`#5d6470`) | 元信息、说明、计数、次要动作 | | 三级文本 | `--dsw-alias-label-tertiary`(`#8b919b`) | 只用于装饰点/未知状态轨,**不承载可读文本** | | 边框 | `--dsw-alias-border`(`rgba(31,35,40,.16)`) | 面板、按钮、输入框 | | 发丝边框 | `--dsw-alias-border-subtle`(`rgba(31,35,40,.08)`) | 行分隔、卡片内分隔 | | 表面 | `--dsw-alias-surface`(`#fff`) | 面板、卡片、选中条、sticky 组头 | | 抬起表面 | `--dsw-alias-surface-raised`(`rgba(31,35,40,.045)`) | hover、搜索底、弱分组底 | | 成功 | `--dsw-alias-state-success`(`#146c3a`) | 就绪、适配器已握手、成功提示 | | 警告 | `--dsw-alias-state-warning`(`#8a4b08`) | 宿主缺路由、启动中、适配器待握手 | | 失败 | `--dsw-alias-state-error`(`#b3261e`) | 读取失败、适配器失联、僵尸任务 | | 信息/链接 | `--dsw-alias-state-business-primary`(`#1f5fbf`) | 生命周期「运行中」、行内链接 | **对比度(回退色实测,WCAG AA 普通文本 ≥4.5:1)** | 组合 | 比值 | |---|---| | 主文本 `#1f2328` / 白 | 15.80 | | 次文本 `#5d6470` / 白 | 5.96 | | 次文本 / raised `#f5f5f5` | 5.47 | | 次文本 / 选中洗色 `#fcf2f2` | 5.43 | | 白 / 品牌红 `#C62828` | 5.62 | | 成功 `#146c3a` / 成功洗色 `#ecf3ef` | 5.76 | | 警告 `#8a4b08` / 警告洗色 `#f6f1eb` | 6.05 | | 失败 `#b3261e` / 失败洗色 `#faf0ef` | 5.84 | | 信息 `#1f5fbf` / 信息洗色 `#eff4fb` | 5.51 | **暗色安全规则**:品牌红只做**填充/边框/轨道**(白字在红底 5.62:1);链接改用 business-primary; 选中文字用主文本 + 红色对勾;避免在暗色下用 `#C62828` 写小字。宿主 alias token 在暗色下会自适应; 回退值在亮色下有上表余量,兜底暗色预览已人工检查。 > 状态洗色(wash)用固定 `rgba()` 叠加(内联 style 兼容性优先);文字、边框、表面、焦点仍走 > `--dsw-alias-*`,暗色主题由宿主 token 决定可读性。 ### 4.2 字体与行高(全部显式,避免宿主 line-height 把行撑散) | 层级 | 字号 / 行高 | 字重 | 场景 | |---|---|---|---| | 品牌 | 19 / 24 | 600 | 「瓮城」 | | 项目标题 | 15 / 22 | 600 | 分身项目卡 | | 会话标题 | 14 / 20 | 500(选中 600) | 母体会话行;分身会话 13/20 | | 面板标题 | 13 / 18 | 600 | 母体 / 分身 | | 正文 | 12 / 18–19 | 400 | 说明、错误正文、按钮 | | 元信息 | 11.5 / 16 | 400 | 工作区、时间、行内动作 | | 辅助 | 11 / 15 | 400 | 面板副标题、页脚 | | 标签/ID | 10.5 / 14 | 500 | 状态标签、短 ID、版本 | 默认字体 `inherit`(跟随 DSH);等宽走 `--ds-font-family-code`,用于 ID、端口、版本、诊断。 ### 4.3 间距、圆角、阴影 - **间距**:4px 基准;主步进 4 / 8 / 12 / 16 / 20 / 24;密集控件允许 2px 光学增量(如 9 / 11 / 14)。 - 根容器桌面 `18px 22px 14px`,小屏 `14px 14px 18px`;面板头 `9px 14px`; 列表行 `9px 12px`(分身会话 `8px 12px`);项目卡头 `12px 14px`;项目卡列表 `12px`。 - 圆角:面板 12、卡片/选中条 10、错误块/搜索 8、按钮 7、标签 4–6、状态胶囊 999。 - 阴影:面板 `0 1px 2px rgba(0,0,0,.03)`;选中条 `0 2px 10px rgba(0,0,0,.05)`; 回顶按钮 `0 2px 8px rgba(0,0,0,.08)`。不做多层拟物阴影。 --- ## 5. 组件规格 ### 5.1 顶栏(品牌 / 状态组 / 动作层级) - 品牌:32px 圆角方框 + 红色城门(21px),`瓮城` 19/600,副标 `母体 × 分身 · 双生控制台` 11/15。 - 状态组(一个对象,三段式):`母体 只读`、`就绪 n/max`、`运行中 n`;桌面 999 胶囊、内部 1px 分隔, 小屏圆角 10 且允许换行。`就绪` 有就绪数时用成功点;`运行中` 大于 0 时用警告点并解释 「进程在跑但适配器未握手」。 - 动作:刷新=ghost(32px,含刷新图标,刷新中禁用并改文案);派生新分身=实心品牌红(36px,含 +), 是顶栏唯一一级动作。小屏动作整行、刷新自适应、CTA 最小 200px 并换行。 - 结果:品牌与状态组左侧成组,动作右侧成组;视线顺序「系统是谁 → 现在怎样 → 我能做什么」。 ### 5.2 栏头与搜索 - 每栏两行头:第一行标题 + 角色标签(右栏「每个任务一个项目」);第二行为 11px 副标题: 左「项目 & 会话 · 选中会话后继续开发」,右「项目 & 会话 · 会话直接在分身标签页打开」。 - 右上计数胶囊:左栏 `30/268 会话`(加载数/母体总数;仅 1 页时只显示数字); 右栏 `2 项目 · 2 异常`(有异常时失败色 + 点)。 - 左栏搜索框:32px 高、放大镜、清除按钮;`aria-label="搜索母体会话"`;Esc 清空; 匹配标题/短 ID/工作区/路径;只在已加载的条目内搜索(服务端 limit 上限)。 - 标题缺失时,搜索框下出现一次说明条:宿主未返回 `title/preview` 时用「未命名会话 + 短 ID」。 ### 5.3 分组头与会话行 **分组头**(`GroupHeader`) - 吸顶:`position:sticky; top:0`,实心 surface 背景,发丝上下边框。 - 按钮语义:整行 button,`aria-expanded`;chevron 跟随展开;标题 + 数量标签;第二行工作区路径; 右侧最近更新时间。展开/收起是真实状态(旧版中 chevron 只是装饰)。 - 组顺序按最近活动倒序;组内按 `updatedAt` 倒序。 **母体会话行**(58px 高,`SessionRow`) - 左轨:2px 透明/选中品牌红;选中背景为 6% 品牌洗色。 - 第一行:会话标题(省略号)+ 缺失时短 ID chip(等宽)+「首条」来源标签 +「已归档」标签。 - 第二行:工作区(`title` 暴露完整路径)+ `·` + 本地时间 `MM-DD HH:mm`。 - 右侧动作:`选择 ›`;hover 变主文本色;选中为 `✓ 已选中`(红勾 + 主文本)。动作属于整行的可点区域, 不做嵌套按钮。 - 键盘:`role=button`、`tabIndex=0`、Enter/Space 选择;`aria-pressed`;焦点环内缩 2px; `aria-label` 包含完整标题/短 ID/工作区/更新时间。 **分身会话行**(50px 高,compact) - 同一标题解析器;标题 13/20,元信息一行「更新 MM-DD HH:mm」。 - 右侧 `打开 ↗`;点击整行或键盘 Enter/Space 直接打开分身标签页;缺 URL 时动作置灰并提示。 - 不引入第二次确认(与母体两段式区分)。 **标题降级规则** | 服务端字段 | 展示 | 标签 | 说明 | |---|---|---|---| | `titleSource='session/title'` + `title` | 原生标题 | — | 真标题 | | `preview`(首条用户消息) | 预览文本 | 首条 | 不假装原生标题 | | `titleSource='first-user'` + `title` | 该标题 | 首条 | 兼容旧字段 | | `titleSource='workspace'` / 无 `title` / `title===workspaceTitle`(且无 `preview`) | `未命名会话` + 短 ID | — | **绝不把工作区名当标题** | | 无 `titleSource`,但 `title` 存在且不等于工作区 | 该标题 | — | 兼容极旧宿主 | | 全部未命名且条目 ≥3 | 列表上方说明条 | — | 解释降级与重启后可显示原生标题 | ### 5.4 分身项目卡(右栏) - 容器:1px 边框 + 3px 左侧状态轨(就绪绿 / 运行中蓝 / 适配器失联红 / 启动中黄 / 失败红); 异常时边框偏红;圆角 10、surface 背景。 - 卡头: - 第一行:项目标题 15/22/600(长标题省略 + `title` 全量)+ 生命周期胶囊 + 适配器健康胶囊。 - 第二行:任务 ID(等宽,末 12 位,title 全量)+ 端口(等宽数字)+ 创建时间。 - 右侧:[打开](有 URL 时锚点,28px)+ [归档](quiet,二次确认)。 - 异常时附加红色 `Callout`:`适配器失联 · 疑似僵尸分身`、错误码 `http-404`、解释 「进程在跑但适配器未握手;仍可尝试打开,确认不需要时归档回收」+ 可折叠诊断 (taskId / status / adapter.error / port / pid)。**不提供假的重试握手按钮**—— 后端没有该路由,只有真实存在的 打开 / 归档。 - `分身会话` 子头:折叠按钮(`aria-expanded`)+ 数量/读取失败标签 +「刷新中…」提示 + 右对齐刷新按钮。 - 会话区状态机: - **读取中**:状态块「正在读取分身会话…」,说明只读元数据;`aria-busy`。 - **失败**:完整 `Callout`(code、人话原因、诊断、重试),保留「上次成功读取」的旧行并标注可能过期; 若是已知宿主缺路由(全列 404),降级为单行 `InlineNote` + 顶部统一说明,减少重复红块。 - **空**:虚线块「该分身还没有会话」+ 刷新 / 打开分身;`sessions-root-missing` 视为正常空态, 但显示目录降级码。 - **有数据**:紧凑行列表;`degraded` 数组以「目录降级:…」弱提示;若 API 的 `total` 大于 返回条数(`limit=20`),底部提示「仅显示最近 n / total 个分身会话」。 - 已归档任务:不在活跃项目里占位;底部独立可折叠「已归档 · n」,只显示标题/短 ID/已归档标签/有 URL 时的打开。 - 小屏:卡头改为纵向;标题占满行,状态胶囊下一行,动作独立右对齐;会话行保持紧凑。 ### 5.5 状态与健康徽章 **生命周期**(独立胶囊,颜色只表达生命周期) | `status` | 文案 | tone | 说明 | |---|---|---|---| | `ready` | 就绪 | success | 进程 + 适配器都已就绪 | | `running` | 运行中 | info | 进程活着,但**适配器未握手**(可能是僵尸) | | `provisioning` | 启动中 | warning | 不拉会话;会话区显示启动说明 | | `failed` | 已停止 | danger | 进程不在;仍尝试读会话(可抢救) | | `archived` | 已归档 | neutral | 只在已归档区 | | 其他 | 原值/未知 | neutral | 显示「当前不可读」而不是假加载 | **适配器健康**(第二枚胶囊,与生命周期分开) - `adapter.ok` → `适配器就绪`(success)。 - 有 `adapter.error` → `适配器失联`(danger;卡片红轨 + 异常 Callout)。 - 无握手且无错误 → `适配器未就绪`(warning)。 - 顶栏 `运行中 n` 在 n>0 时用警告点;右栏头 `n 异常` 用失败色;徽标显示 `瓮城 · n 异常`。 - **异常信号不只靠颜色**:文案「失联/异常」、警示图标、右侧计数、卡片红轨同时存在。 ### 5.6 错误展示:Callout vs InlineNote - **Callout**:多行问题(读取失败、路由缺失、派生失败)。结构 = 3px 色轨 + 警示图标 + 标题 + 错误码 chip + 动作 + 正文 + `
诊断信息`。正文用次文本;错误码等宽。 - **InlineNote**:已知单一原因(如旧宿主缺 `/tasks/sessions`)的单行条:图标 + 标题 + code + 一句原因;重试由所在子头的刷新按钮承担,避免每卡重复一个大红块。 - 所有「诊断信息」保留 URL、HTTP 状态、Content-Type、解析错误、响应片段(截断 200 字符); 文案中不暴露 token(API 诊断只含路径)。 **错误码 → 人话** | code | 展示含义 | |---|---| | `bad-json` | 返回不是 JSON(旧宿主/代理错误页);保留片段 | | `empty-body` | 空响应,宿主可能重启中 | | `http-404`(分身会话) | 宿主未提供 `/fang/api/tasks/sessions`(v2.2.0 起);重启母体后再试;分身仍可打开 | | `http-404`(其他) | 路由/对象不在注册表 | | `unknown-task` | 任务已不在当前母体注册表(重启/归档) | | `network` | 请求未完成(宿主未启动/离线/中断) | | `session-catalog-unavailable` | 会话目录服务未加载 | | `sessions-root-missing` | 正常空态(新分身/无会话) | | `index-not-found` / `index-read-failed` | 分身索引降级,标题/分组可能不完整 | | `quota-exceeded` | 配额满,先归档 | | `session-seed-failed` | 摘要种子失败,派生 fail-closed 中止 | | `handler-threw` | 宿主处理异常,详见诊断 | | 其他 `http-nnn` | `接口返回 HTTP nnn` | ### 5.7 选中确认条(两段式) - 位置:桌面在两栏下方通栏;小屏插在左栏与右栏之间(选中后立即可见,不必滚到页尾)。 - 结构:红 3px 左轨 + 白色表面 + `✓ 已选 · 母体会话`(次文本标签、红勾)+ 标题(含短 ID)+ 一句说明「派生 1 个分身并注入 ≤32KiB 摘要种子;母体会话保持只读」。 - 动作:`取消`(quiet,Esc 同效)+ `继续开发 →`(品牌红实心,派生中禁用并改文案)。 - 小屏:说明/标题占满整行,按钮行右对齐,主按钮 `flex:1` 拉伸;触控高度 32px。 - 语义:`role=status`、`aria-live=polite`;派生成功清空选中。 ### 5.8 结果条、空态、加载、回顶 - **快速派生结果**:pending 显示已等待秒数;成功显示「分身已就绪 / 分身已启动」+ 端口 + 任务 + 「适配器待握手」警告 + 打开链接;失败显示 code / 原因 / 诊断 / 重试 / 知道了。 - **空态**:母体空态给出「开新对话后刷新」;右栏空态给城门徽记 + 「派生新分身 / 刷新」; 搜索无结果给「清除搜索」。全部使用虚线弱容器,不与错误红块混淆。 - **加载**:状态点 + 文案;列表容器 `aria-busy`;不会先用假空态闪一下。 - **回顶**:列表滚动 >240px 时在面板右下出现 30px 圆形按钮;`prefers-reduced-motion` 下用即时滚动。 - **行/面板独立滚动**:`overscroll-behavior:contain`,内层滚动不带动整个页面。 --- ## 6. 响应式规格 | 断点 | 行为 | |---|---| | >980px | 3:7 两列;顶栏一行(品牌 + 状态组 + 动作);选中条通栏在底部;面板高 = 剩余视口 | | ≤980px | 单列堆叠;顶栏允许换行(动作整行、CTA 至少 200px);左/右面板固定 clamp 高度、各自滚动;选中条夹在两栏之间;卡片头纵排、动作右对齐 | | 430px 实测 | 无横向溢出(`scrollWidth == clientWidth`,视口宽度随滚动条略有差异);选中条可完整显示;任务卡标题/胶囊/动作不互相挤出 | - 面板高度用 `clamp()`,避免极短视口里列表只剩一行;外层滚动承载整页。 - 触摸目标:行 50–58px;按钮 28/32/36px(≥ WCAG 2.5.8 的 24px);搜索清除 24px; 「诊断信息」`min-height:24px`。 --- ## 7. 无障碍 ### 7.1 键盘 | 按键 | 行为 | |---|---| | Tab / Shift+Tab | 品牌动作 → 搜索 → 分组头 → 会话行 → 卡片动作 → 会话行 → 选中条 → 页脚 | | Enter / Space | 母体会话行 = 选中;分身会话行 = 打开;按钮/折叠 = 激活 | | Esc | 取消选中;搜索有内容时先清空搜索 | | 原生 details | Enter/Space 展开诊断;读屏语义由 `` 提供 | ### 7.2 语义与焦点 - 标题层级:`h1 瓮城` → `h2 母体/分身` → `h3 项目标题`。 - 行 `role=button` + `tabIndex=0` + `aria-pressed`(母体选择);折叠按钮 `aria-expanded`; `aria-busy` 标在加载中的列表/会话体上。 - 焦点环:2px 品牌红,offset 2;行内用内缩 -2,避免被面板 `overflow:hidden` 裁剪。 - Live region:选中条、派生结果、操作提示 `role=status` / `aria-live=polite`。 - 图标全部 `aria-hidden`;图标按钮/仅符号按钮都有 `aria-label`。 - 状态不只靠颜色:文案 + 图标/点 + 数量。 ### 7.3 其他 - 对比度见 §4.1;小号可读文本不使用 `label-tertiary`。 - 不播放循环动画;平滑滚动尊重 `prefers-reduced-motion`;无闪烁/自动弹层。 - 搜索有可访问名称(`aria-label`)与可见 placeholder、清除按钮;错误/空态有文字说明,不只图形。 - 文本缩放:布局以 flex-wrap / minHeight 为主,按钮固定高度为 28–36px,行高不裁剪文字; 浏览器整体缩放(Electron/Chromium)下保持可用。 --- ## 8. 数据契约与 UI 推导(未改协议) | 方法 | 路径 | UI 用途 | |---|---|---| | GET | `/fang/api/status` | 顶栏角色/配额/运行中;宿主版本;徽标 | | GET | `/fang/api/tasks` | 右栏项目列表;归档分区;异常计数 | | GET | `/fang/api/sessions` | 左栏母体会话;`total/returned/limit/degraded` 用于计数与说明 | | GET | `/fang/api/tasks/sessions?taskId=&limit=20` | 每个分身自己的会话;失败只影响该卡 | | POST | `/fang/api/derive` | 快速派生 / 从会话继续(lineage 种子) | | POST | `/fang/api/tasks/archive` | 归档回收(二次确认) | **UI 推导(全部在 client 层)** - 标题来源 → `sessionTitleInfo`;未命名用短 ID;不透传工作区名。 - 异常 = `status==='running' && !adapter.ok`;右栏头/徽标/顶栏用同一判定。 - 会话拉取:`ready/running/failed` 才拉;`provisioning` 不拉;`archived` 不拉。 - 刷新:初始 + 每 5s(页面 hidden 时跳过)+ 手动;单飞锁避免重叠;失败保留上次数据并标 stale。 轮询拉取分身会话用静默模式(不闪「刷新中」),只有卡片内的显式刷新才显示忙碌。 - 每个分身的会话请求有独立序号,旧响应不会覆盖新响应;失败只更新该任务。 - 面板派生/归档成功 → 全量 refresh;快速派生结果进共享 store,面板在下一次轮询时同步。 --- ## 9. 实测 Bug 与修复 > 现场:2026-11-01 16:40 左右,宿主 `/fang/api/status` 报告 `version=2.1.0`,`tasks.total=2`, > `ready=0, running=2, max=8`;两个任务的 `adapter.error` 均为 `http-404`。 > `/fang/api/sessions` 返回 50(总 268)条,字段为 `sessionId/workspaceTitle/...`, > **没有 `title`、`titleSource`、`preview`**。 ### B1. 所有会话标题都是「GPT」 - **现象**:左栏每一行标题都是 `GPT`;50 行无法区分。 - **根因**:宿主仍在 v2.1.0,未返回 `title/titleSource/preview`;旧 `sessionLabel()` 在无标题时 回退到 `workspaceTitle`,把工作区名当成了会话标题(工作区恰好叫 GPT)。 这不是截图渲染 bug,而是「降级表达缺失」。 - **修复**:`sessionTitleInfo()` 优先看 `titleSource`;`session/title` / `first-user` 才算标题来源; 无标题统一「未命名会话」+ 短 ID chip;不把 `workspaceTitle` 当标题;列表上方在 全部未命名且 ≥3 条时给一次来源说明;搜索按短 ID/工作区也可定位;tooltip 带完整原因。 - **验证**:用现场字段(无 title/preview)渲染,标题为「未命名会话 + 短 ID」; 原生标题/首条预览/工作区回退三种单测全部通过。 ### B2. `分身会话不可读: bad-json` - **现象**:右栏每个项目下只有一行灰色 `分身会话不可读:bad-json`,无原因、无重试。 - **根因**:`/fang/api/tasks/sessions` 在 v2.1.0 宿主上返回 **HTTP 404 + 空 body**; 旧 `api()` 一律 `res.json()`,解析失败就把任何错误压成 `body.error='bad-json'`, HTTP 状态、Content-Type、响应片段全部丢失——于是「旧宿主缺路由」被误报成「JSON 坏了」。 - **修复**: 1. `api()` 保留 status / content-type / 解析错误 / 响应片段(截断 200 字符); 2. 空 4xx → `http-404`;200 非 JSON → `bad-json`;空 200 → `empty-body`; 3. 分身会话错误按任务独立重试,不拖垮整列;保留上次成功读取的旧行并标注; 4. 当所有活跃任务的分身会话请求都返回 `http-404` 时,右栏顶部给统一 `Callout`: 「分身会话接口不可用(HTTP 404)… v2.2.0 起加入;重启母体后再试」+ 全部重试; 单卡降级为一行 `InlineNote`,避免重复大红块; 5. `详情` 里显示 URL/HTTP/解析错误,便于判断是旧宿主、代理页还是任务已归档。 - **验证**:现场 404 探针;静态树/冒烟渲染断言出现 `HTTP 404`、原因、重试、诊断; 其它错误(`unknown-task`、`network`、`handler-threw`)也走同一映射。 ### B3. 僵尸任务 `adapter http-404` 不显眼 - **现象**:任务元信息行末尾 `adapter http-404`,与端口/时间同色同字号;状态仍是裸 `running`。 - **根因**:旧 UI 把生命周期与适配器健康塞进同一条 meta,且没有异常 tone;用户很难一眼看出 「进程在跑但控制通道已断」= 僵尸。 - **修复**:拆成两枚胶囊(运行中 / 适配器失联);异常卡加红左轨 + 红边 + 异常 Callout + 右栏头 `n 异常` + 顶栏运行中警告点 + 徽标 `瓮城 · n 异常`; 快速派生成功时若适配器未握手,显示「分身已启动 + 适配器待握手」而不是「已就绪」。 动作只保留后端真实存在的打开/归档,不伪造握手重试。 - **验证**:用现场两条 `running + http-404` 任务渲染,异常计数 2、每卡两枚胶囊与异常块均出现。 ### B4. 组头缺折叠、12 条静默截断、刷新全禁 - **现象**:截图里的组头带 chevron;但当前工作区 `client.js` 的组头是纯 `div`,没有展开/收起处理器, chevron 只是观感。列表 `slice(0,12)` 静默截断,组头写 50 却只渲染 12 条且无说明; 「刷新」在任何操作(派生/归档)期间都被禁用,状态含混。 - **根因**:组头未接线;`slice(0,12)` 静默截断;`disabled={!!busy}` 把不同操作的忙碌混在一起。 - **修复**:组头改为真实 button + `aria-expanded`;列表展示全部已加载条目并给 `n/total` 与 「仅加载最近 n / 共 total」说明;新增搜索;刷新只受自身 `refreshing` 控制,派生/归档各自忙态。 - **验证**:键盘可折叠/展开;50 条不再静默消失;刷新与归档/派生互不误禁。 ### B5. 小屏选中条掉到首屏之外 - **现象**:堆叠布局下选中条在右栏之后,选中会话后看不到「继续开发」。 - **修复**:≤980px 把选中条插在左栏与右栏之间;标题占满行,按钮行右对齐、主按钮拉伸。 - **验证**:430px 视口预览;选中条在左栏之后出现且可完整点击,`scrollWidth == clientWidth`(无横向溢出)。 ### B6. 强调色小字在暗色下的对比风险 - **修复**:品牌红只承担填充/边框/焦点环/左轨;正文与标签改用主/次文本;行内链接改用 business-primary;选中态用红勾 + 主文本,不把 `#C62828` 当 10.5px 正文。 - **验证**:亮/暗两套主题静态预览;回退色对比表见 §4.1。 --- ## 10. 验收与测试记录 | 检查 | 结果 | |---|---| | `node --check client.js` | 通过(exit 0) | | 许可证头 / SPDX(`test/licensing.test.mjs`) | 通过 | | 假 React 冒烟渲染(初态、现场数据、僵尸卡、健康卡、徽标、侧栏) | 全部通过 | | 亮色 1568×1045 静态预览(真实组件树 + 现场形状) | 通过;3:7、层级、错误态、选中条 | | 暗色 1568×1045 静态预览 | 通过;语义 token 可读,品牌红只做非文本/填充 | | 小屏 430×1400 / 选中态 | 通过;堆叠、独立滚动、选中条位置正确、无横向溢出 | | 现场 GET 探针(status / tasks / sessions / tasks-sessions) | 复现并确认 B1/B2/B3 根因 | | 对比度抽检 | 见 §4.1,全部 ≥4.5:1(装饰性三级文本除外,已不承载可读文本) | > 说明:视觉预览使用真实 `FangPanel` 组件树渲染为静态 HTML 后截图,不是另画稿; > DSH 面板本身需要登录态,本次未在已登录的宿主页内做点击回放。真实主题 token 由宿主提供, > 预览使用与回退值一致的主题。 ### 交接测量 - 面板内边距:桌面 18/22/14,小屏 14。 - 面板头 9/14(两行 18 + 15 + 1 gap);搜索行 8/10 + 32 高;列表行 9/12、min 58; 分身行 8/12、min 50;分组头 8/12(两行 16 + 14)。 - 项目卡:头 12/14;标题 15/22;meta 11.5/16;卡间距 10;卡外列表 padding 12。 - 顶栏:32 图标、19/24 标题、11/15 副标;状态胶囊 28 高;CTA 36 高、刷新 32 高。 - 选中条:11/14 内边距、红 3px 左轨、标题 14/20、说明 12/18、按钮 32。 - 颜色/字号/间距的完整 token 见 §4。 --- ## 11. 已知边界与下一步 1. **客户端需要页面刷新**:新 `client.js` 在页面加载时装载;宿主侧 v2.1 模块升级需重启母体 (ESM 缓存)。看不到新 UI 时先刷新页面。 2. **分身会话路由**:`/fang/api/tasks/sessions` 自面板 v2.2.0 起提供;现场宿主 v2.1.0 返回 404, UI 会解释并给重试,但必须**重启母体加载新版**才会真正可用。 3. **会话标题**:原生标题依赖宿主 v2.1.1+ 返回 `title/titleSource/preview`;v2.1.0 只能显示 「未命名会话 + 短 ID」。UI 不会再用工作区名冒充标题。 4. **会话上限**:现场 `/fang/api/sessions` 只返回最近 50 条(母体共 268 条)。搜索只覆盖已加载 条目;面板明确显示 `50/268` 与截断说明,不制造「全部可见」的错觉。 5. **无握手重试路由**:适配器失联时 UI 只提供真实的打开/归档,不伪造「重试握手」;需要恢复时 走母体重启/重新派生流程。 6. **归档任务不拉会话**:home 被回收后无意义;失败任务仍会尝试读取,便于抢救。 7. **双击热区**:仍只有左侧导航图标(18px 级别)和 dock 徽标;面板内标题栏城门图标不再挂手势。 8. **主题**:颜色走 `--dsw-alias-*`;插件红是固定艺术色,暗色下只用于填充/边框/焦点环。 若宿主允许 `--fang-gate-accent` 覆盖,会自动跟随。若宿主未提供 `--dsw-alias-state-*`, 回退语义色按亮色对比度验证;暗色应依赖宿主提供浅色 aliases(本次暗色预览使用模拟的同名 token)。 9. **小屏**:两栏各自滚动是刻意的;外层页面在堆叠时也可滚动,选中条安排在左栏后,避免首屏丢失。 --- ## 12. 变更记录 | 日期 | 变更 | |---|---| | 2026-11-01 | UI 重设计:标题降级、错误可读可重试、状态/健康拆分、顶栏层级、搜索/折叠/回顶、暗色安全强调色、≤980px 堆叠与选中条移位;仅 `client.js` 与本文档,未提交、未改 src | ## 数据契约(v2.4.0) - 会话行显示「创建」与「更新」两个时间:`createdAt` 来自会话日志 header,`updatedAt` 来自日志 mtime,两者不可混用; - `empty:true`(没有 user/message)显示「空会话」标签,且「继续开发」禁用; - 派生成功提示会说明历史会话是否导入:`已导入历史会话(N MiB)` / `历史过大仅带交接摘要` / `历史未导入`; - 母体会话在分身的出现方式:日志复制到 `/sessions///`,并登记到分身 `storages/workspace.json`; - 空会话、导入失败、宿主版本过旧(无 /tasks/sessions)都走显式降级文案,不伪造数据。