会话总结与决策记录(2026-08-21)
September 16, 2026 · View on GitHub
给后续 DSH 会话的交接文档:新会话继续完善本项目前,先读本文件 + README。 2026-08-22 更新:0.2.0 模型余额已接入,见文末「八、续作会话(2026-08-22)」; 其中 8.7 记录了「CSS 全局名冲突致 dsh 启动崩溃」事故与修复,改 client 半部前必读。
一、本项目是什么
dsh-dock(中文名:功能坞)—— DeepSeek Harness 功能坞插件。一张管理面板统一注册、开关所有小功能(模型余额、Token 用量记录、任务动画,以及未来任意新功能)。已发布 npm:dsh-dock@0.1.0(maintainer: wycto),0.2.0(模型余额 + 侧栏入口按钮 + 功能弹层)代码完成待发布。
二、本会话做了什么
- 架构咨询结论:个人工具集用"单中枢 + 注册表 + 独立功能模块"(即当前实现),而非拆成互不相识的多个插件;只有信任级别不同或需要独立生命周期的功能才拆单独包。三个既有功能包(
@wycto/dsh-balance-panel/dsh-token-usage/dsh-task-pulse)后续按路线图吸收或注册进中枢。 - 命名决策:无作用域名
dsh-dock(初选dsh-hub已被他人占位;作用域备选@wycto/dsh-hub保留)。2026-08-21 已发布 npm 占用(0.1.0,latest)。 - 基础框架落地:继承本地已安装原型
feature-hub(静态 bundle 路线),正式化为可发布 npm 包。 - 本项目初始化:本仓库从
/Users/weiyi/develop/test/dsh-dock(发布源,2 commit)复制代码并整理 README + 本文档。
三、关键决策与原因(不要推翻)
| 决策 | 原因 |
|---|---|
静态 bundle 路线(cordis.patch.yml + dsh.bundle.patch + exports["./client"]) | 本部署的模型网关会把对象类型工具参数序列化为字符串,cordis_define 动态插件路径被阻塞;静态包才是一致、常驻、长期使用的形态 |
Client half 手写 lazy-CJS 工厂(window.__ModuleLoader__.load) | 无需构建器,改完直接生效(profile 里是 link 依赖,重启 dsh web 即加载新代码) |
| 每功能 = 注册表一条 + 视图一个 + 独立开关(内存态) | 与 feature-hub 原型一致;个人工具集足够,拒绝过度设计 |
| Host↔Client 数据通道用 webServer 回环路由 + 同源 fetch,不走 typert | 见 8.3:@wycto/dsh-balance-panel@0.1.1 同部署实测可用,无需 typert/connection RPC |
余额策略表吸收 dsh-balance-panel 实现(外人视角即"复用",非重写) | 同作者 MIT 代码,官方计费接口,已被生产验证;避免重复造轮子 |
npm 包名 dsh-dock(无作用域) | 用户拍板;已发布即占用,勿改 |
四、当前状态快照(2026-08-22 更新)
- npm:
dsh-dock@0.1.0latest(待 0.2.0 发布后 latest 更新) - 代码:0.2.0 模型余额已接入并本地验证通过(本仓库
main,待发布) - 运行环境:web profile 已换为
dsh-dock(link 本仓库,隔离实例验证 ✅);线上dsh web(端口 3080)仍是旧组合,用户重启后生效(重启会中断会话) - 代码源:本仓库(gitea
main)+ 发布源副本/Users/weiyi/develop/test/dsh-dock - 发布脚本:
scripts/publish.sh(含登录检查、--cache 绕坑)
五、下一步(按顺序)
2026-08-22 更新:前两项已完成,最新清单见「八、续作会话」8.5。
- ✅ 本地替换验证(profile 已替换并在隔离实例验证通过;用户重启
dsh web即生效)。 - ✅ 0.2.0 模型余额(2026-08-22 完成代码 + 本地验证,待发布)。
- 0.3.0 Token 用量:
hostSetups.tokenlog监听事件记账 + 统计视图。 - 0.4.0 任务动画:纯 Client 模块,可注册对话区 slot。
- 0.5.0 持久化:开关状态落盘。
- GitHub 发版:建
wycto/dsh-dock(GitHub)→package.jsonrepository 改实际地址 → 发布 → tag 发 Release。
六、环境备忘(踩过的坑)
- npm 缓存 EPERM:
~/.npm有 root 属主历史文件。绕过:CACHE_DIR=/tmp/dsh-dock-npm-cache ./scripts/publish.sh;根治:sudo chown -R 501:20 ~/.npm。 - registry 读取一致延迟:
npm publish成功后立刻 curl 可能 404,等几秒重试即 200,非失败。 - 沙箱:gitea 目录在会话工作区外,本会话已由用户开放全权限写入。
- git 身份:本仓库需设置
user.name/user.email(本机无全局身份),用 wycto。 typert 联通(已推翻,见 8.3):静态 bundle 下 Client↔Host 的私有 RPC 原假设走 typert 服务;实测采用 webServer 回环路由 + 同源 fetch 即可。
七、命名与生态备忘
- 现有功能包家族:
@wycto/dsh-balance-panel、@wycto/dsh-token-usage、@wycto/dsh-task-pulse - 面板包:
dsh-dock;后续新功能包建议@wycto/dsh-<功能名>或吸收为 dock 内模块 dsh-hub无作用域名已被他人占位(0.0.1),勿用
八、续作会话(2026-08-22)
8.1 本会话做了什么
- 0.2.0 模型余额接入完成(代码 + README + 本文档;发布与本地安装另行确认):
index.js:新增hostSetups.balance,在回环 webServer 注册GET /dsh-dock/balance路由; Provider 枚举(llm.listConfigurableProviders+settings.describe(redactSecrets)+agentDefaultModel.currentSelection)、 余额策略表、凭证解析全部沿用@wycto/dsh-balance-panel@0.1.1(MIT,同作者)——策略覆盖 DeepSeek / StepFun / Kimi Coding / OpenRouter / MiniMax / xAI,另含qwen-token-plan-*等登录跳转与控制台链接。- 补上
export const inject = ['webServer'](0.1.0 框架从未声明 inject;balance 是第一个真实 Host 功能, 实测发现 apply 在 webServer 就绪前执行导致路由注册失败,补齐后与@wycto/dsh-balance-panel一致)。 client.js:balance 占位移除planned,新增真实视图(fetch('/dsh-dock/balance'),5 分钟自动刷新 + 手动刷新, 每 Provider 卡片带独立主题色、余额/配额/登录跳转/不支持等状态),开关默认启用。package.json:版本 0.2.0,新增依赖@deepseek-ai/dsh-credentials@^0.1.0-rc.6(与 web profile 已装版本一致)。
- 环境验证:Node v26(全局 fetch + AbortSignal.timeout 可用);
webServer.register返回 disposer;@deepseek-ai/dsh-credentials在 web profile node_modules 已就位(0.1.0-rc.6)。 - 本地验证全绿(详见 8.6):
- 冒烟测试:以真实部署配置桩驱动
apply(),Provider 分类正确; - Client SSR:以真实 react/react-dom 渲染整个面板(五个功能卡片 + 余额视图初始态 + 规划占位)全过;
- 真实实例验证:用
DSH_HOME=/tmp/dsh-verify-home隔离起一个同组合dsh web(端口 3999),GET /dsh-dock/balance返回 200,分类正确(qwen-token-plan-cn→需登录带百炼链接、fangzhou/jiyuanlvdong→不支持、 deepseek 系→未配置密钥不发网络请求);/dsh-balance-panel(旧包)仍 200,两包并存无冲突。
- 冒烟测试:以真实部署配置桩驱动
8.2 未做(留给后续会话/用户)
- 重启线上实例看面板:web profile 已替换为
dsh-dock(link 本仓库)并验证通过(8.6), 但当前跑着的dsh web(PID 66740,端口 3080)还是旧组合——需用户手动重启dsh web才会加载 dsh-dock; 重启会中断当前 DSH 会话,故本会话没有动它。 - npm 发布 0.2.0:
npm publish需用户确认(wycto 账号),用CACHE_DIR=/tmp/dsh-dock-npm-cache ./scripts/publish.sh。 ⚠️ 发布前注意:package.json的dependencies只在非 link 安装(npm 安装)时生效; 本地 link 模式需要仓库内 shim(见 8.4/8.6),那是 gitignored 的,不随包发布。
8.3 重要决策更正:Client↔Host 联通不走 typert,用 webServer 回环路由
旧文档(第六节)假设"静态 bundle 下私有 RPC 走 typert"。实测并经 @wycto/dsh-balance-panel@0.1.1
(同部署已安装可用)验证:Host 在回环 webServer 注册同源 HTTP 路由、Client 直接 fetch 即可,
无需 typert、无需 connection RPC、任何回环部署都能工作。0.2.0 采纳该路线:
- 路由路径
GET /dsh-dock/balance(与余额面板包的/dsh-balance-panel错开,两包并存不冲突); - 数据是纯 JSON 归一化视图(generatedAt / default / providers[]),不是内部对象;
- 密钥只在 Host 进程内
credentials.resolve(credentialRef(apiKeyEnv))取用,绝不下发浏览器。
8.4 环境备忘(延续)
- git 身份:本仓库仍需 wycto 的 user.name/user.email(本机无全局身份)。
- 本部署 settings(~/.dsh/settings.yaml)Provider 清单见 8.1 第 3 条;新增 Provider 只要 profile 配了
baseURL/apiKeyEnv就会被自动枚举。 - link 模式依赖坑:profile 里
dsh-dock: link:本仓库,Node 按真实路径解析@deepseek-ai/dsh-credentials, 仓库内必须有node_modules/@deepseek-ai/dsh-credentials(-> symlink profile 已装版本,gitignored)。 否则插件行 import 失败(boot 不报错但功能静默缺失)。npm 真实安装后不需要 shim。
8.5 下一步(按顺序)
- v0.2.0 追加(本次会话):中文名定为「功能坞」;新增侧栏入口按钮
(
sidebar.footer.actioniddsh-dock,order 1,靠右端 = 设置旁) + 功能弹层(仿 dsh 设置:居中模态、左导航 + 右内容区) (shell.overlayiddsh-dock-panel,order 21); CSS 由页内<style>改为 apply 时全局注入(ensureCss/data-plugin-css="dsh-dock")。 首页总揽(2026-08-22 追加):导航首项「首页」为默认选中页,网格卡片总揽全部子功能 (状态徽章 + 运行概要 + 快捷开关,点卡片跳对应功能页);为此把余额数据提升为模块级共享快照balanceStore(首页概要与余额视图共用一份请求/5 分钟刷新),心跳改用模块级loadedAt运行时长基准。 位置对齐:设置 button 在宽栏是满宽 42px 行(margin:4px -2px,行带 50px)、窄栏 36px 居中(margin:8px 0 10px,行带 54px)→ 入口按钮用transform: translateY(46px)(宽)/44px(窄)+zIndex:1下移进设置行右端空白区, 与设置按钮同底对齐(transform 不动布局、不遮齿轮图标)。 注意:设置面板的 open/select 是ui-settings-general内部 useState,无公开 API—— 入口按钮只能打开自己的弹层,无法编程式打开设置页并选中 section(若未来需要,得给ui-settings-general加服务或事件)。 ⚠️ 2026-08-22 修复:弹层 JSX 链尾部缺一个右括号(client.js曾处于语法非法状态, 页面能跑是 HMR 缓存的旧有效版本);已补齐并以node --check+ SSR 冒烟把关。 状态:用户已重启dsh web,静态版生效(动态预览随之清空);重启后曾发生启动崩溃事故, 根因与修复见 8.7。 - 用户在页面确认:入口按钮与设置按钮同底对齐不再悬空;功能弹层默认打开「首页」总揽 (卡片网格:状态/概要/快捷开关,点击进入模块页),下方菜单为各子功能;设置页「功能坞」面板有样式。
- 确认后:
CACHE_DIR=/tmp/dsh-dock-npm-cache ./scripts/publish.sh发布 0.2.0 → 本仓库提交/推送。 - 0.3.0 Token 用量记录:
hostSetups.tokenlog监听 LLM API 事件记账 + 面板统计视图,参考@wycto/dsh-token-usage。 - 0.4.0 任务动画:纯 Client 模块。
- 0.5.0 持久化 + 双侧注册表打通:开关状态落盘;Host 侧
setEnabled现在只在 load 时按defaultEnabled执行一次,届时接上 Client 面板开关同步(可把数据通道升级为同一路由体系下的 POST 控制接口,或沿用 8.3 的 fetch 模式)。
8.6 本地验证怎么做(可复用)
- 组合层:
dsh web --dump-config→ 应看到# == dsh-dock / - id: dsh-dock行。 - Host 冒烟:
/tmp/dock-smoke/smoke.mjs(stub ctx + 真实部署配置形状)→ 路由输出分类断言全绿。 - Client SSR:
/tmp/dock-smoke/client-ssr.mjs(真实 react + react-dom 渲染整个面板)→ 五卡片全渲染。 - 真实实例(不碰线上):
DSH_HOME=/tmp/dsh-verify-home拷贝 profile(node_modules 里dsh-dock改回绝对链接指向本仓库)+ settings.yaml,起dsh web --port 3999, curl/dsh-dock/balance断言 200 与分类。⚠️ 别用真实 home 起第二实例,避免与线上进程争存储。
8.7 事故复盘:CSS.join is not a function 导致 dsh 整页启动崩溃(务必读)
- 现象:重启
dsh web后页面报Failed to load plugins / dsh-dock / failed to apply loader entry <rev> (dsh-dock): CSS.join is not a function,整个 GUI 起不来。 - 根因:bundle 的样式数组被命名为
CSS,与浏览器全局window.CSS(命名空间对象,无.join) 同名。在浏览器执行环境里标识符解析歧义,ensureCss()里的CSS.join("\n")实际调到了全局 命名空间 → 插件 apply 抛错 → 该 entry 加载失败 → 整页启动失败。 - 为什么本地验证没拦住:SSR 冒烟里
typeof document === "undefined"让ensureCss()提前 return,CSS.join那行根本没执行;node --check只查语法。即:只在浏览器端炸的 环境相关 bug,纯 Node 冒烟查不出。 - 修复(已落地):
- 变量改名
DOCK_CSS(唯一命名,任何作用域都解析不到全局); - 恢复
tag.textContent = DOCK_CSS.join("\n")正确注入(注意:事故后他人先用textContent = CSS(数组直接赋值)止血,那会让 CSS 被逗号拼接成碎样式,已一并修正); ensureCss()整体 try/catch +Array.isArray防御:注入永不抛错,坏了只降级不炸页。
- 变量改名
- 验证升级(防复发):
client-ssr.mjs增加「浏览器模拟」段:注入毒化的全局CSS(无 join 的命名空间)+ 假document,让ensureCss()真实执行,断言样式按行 join、 幂等、且带data-plugin-css。今后任何把样式数组命名为 CSS/作用域错位的改动,测试当场红。 - 硬规矩(勿违反):bundle 顶层变量一律避开浏览器全局名(
CSS、window、document、fetch、AbortSignal等直接用但自定义变量不要占这些名字);改 client 半部后 SSR 冒烟 + 浏览器模拟段必须全绿,且真实dsh web启动验收前不允许发布/提交收尾。
8.9 续作会话(2026-08-22 下午):0.3.0 模型设置 + 弹层窗口化
- 0.3.0 模型设置(集成官方链路,不改内核):
- 官方链路勘察结论(勿重复探索):会话模型选择器(
ui-model-selection,slotconversation.input.model) 按model.reasoning.efforts展示强度档;档位源自 Provider 配置—— pi-ai 每模型reasoningEfforts(键=档位,值=wire 值;off: null= 支持关闭不发参数;false= 不支持思考; 档位全集 off/minimal/low/medium/high/xhigh/max); deepseek 官方连接级thinking(enabled/disabled)+reasoningEffort默认档(off/low/high/max,全模型共享)。 - 输入模态:官方 schema 仅收
text/image(pi-aiinput、deepseekinputModalities); schemastery 保留未知字段(实测)→「视频/音频/文档」以dockTags标注随官方配置持久化,不参与请求路由。 - Host(index.js):
GET/POST /dsh-dock/models——GET 枚举目录(deepseek 官方始终可编辑,schema 默认兜底; 其余按"已配置"口径);POST 经settings.mutate(ns, ops, revision)写回(revision 乐观锁,冲突 409)。 - Client(client.js):
modelconfig模块(FEATURES 首位 = 导航第二项,紧跟「首页」): Provider chips + 模型行编辑(id/名称/上下文/最大输出/输入类型/标注/思考强度三态)+ 添加/删除 + 保存/重新拉取;modelsStore共享快照(首页总揽概要 "N 个 Provider · M 个模型")。 - 验证:Host 冒烟
/tmp/dock-smoke/models-smoke.mjs(读分类/写回 ops/拒绝用例/409)全绿; 隔离实例(/tmp/dsh-verify-home3,端口 3999,profile link 仓库 +@deepseek-ai/dsh-credentials软链) 浏览器实测:GET 5 Provider;编辑 qwenglm-5.2为自定义档(off/low/high)+ 视频标注 → 保存 → 隔离 settings.yaml 出现reasoningEfforts: {off: null, low: low, high: high}+dockTags: [video]✅。
- 官方链路勘察结论(勿重复探索):会话模型选择器(
- 弹层窗口化:默认 ;标题栏 ─ ▢ ✕ 三键
(最小化折叠内容、最大化 inset 10px 铺满、双击标题栏切换);标题栏 pointer 拖动(视口钳制)、
右下角 16px 手柄缩放(≥640×420);
lastGeom页面生命周期内记忆几何。实测拖动/缩放/最大化/最小化/还原全过。 - 版本:package.json 0.3.0;路线图顺延(0.4.0 tokenlog、0.5.0 动画、0.6.0 持久化)。
⚠️ 线上
dsh web(3080)需用户手动重启才会加载新 Host 半部(/dsh-dock/models 路由在 Host 进程内注册)。
8.10 事故复盘:开思考后 qwen 请求 400「developer is not one of …」(2026-08-22 晚,已修复)
- 现象:用户给 qwen 路由模型开思考档后,选强度发消息即 400:
developer is not one of ['system','assistant','user','tool','function'](百炼端点)。 且只要模型reasoning=true,不选强度也 400(system 一直被改写为 developer 角色)。 - 根因链(关键代码均已核实):
- pi-ai
openai-completions.js:787:useDeveloperRole = model.reasoning && compat.supportsDeveloperRole; - 非内置目录模型无 catalog compat → 检测默认
supportsDeveloperRole: true(标准端点); qwen3.8-max/deepseek-v4-*-0731/0813不在 pi-ai 内置目录(glm-5.2在,自带{thinkingFormat: qwen, supportsDeveloperRole: false}兜底,所以它没事);- 我们的模型设置让用户给非目录模型开了
reasoningEfforts→reasoning=true→ 触发 2+3。 另:qwen 路由是纯目录路由(用户层只有 models,api/baseURL 靠内置目录),修复时不能依赖 profile.api。
- pi-ai
- 修复(index.js
piAiModelWrite):- raw round-trip:GET 带出原条目
raw,写回基于 raw 合并——未知字段(用户自配 compat/description 等)不再丢失(旧版保存会把它们清掉,这次也顺带修了); - 思考 compat 兜底:凡启用 custom 档位,显式写
compat.supportsDeveloperRole: false(system 角色全端点通用);目录路由(无 api)视作 openai 兼容;路由 id/baseURL 命中qwen|dashscope|aliyuncs再补thinkingFormat: 'qwen'(与目录内模型一致,实测 schema 接受)。
- raw round-trip:GET 带出原条目
- 存量治愈:直接给
~/.dsh/settings.yamlqwen 路由 4 个模型插入compat: {thinkingFormat: qwen, supportsDeveloperRole: false}(备份.bak-dock-heal); settings.yaml 被 chokidar 监听(watch:true)热生效。 - 端到端验证(隔离实例 3999 + 治愈后配置 + 修复代码 + 拷贝凭证):
qwen3.8-max · High 发送成功,出现思考块且正常回复,无 400。
(坑:隔离 home 缺
.credentials.yaml会先报 MISSING_CREDENTIAL——从 ~/.dsh 拷.credentials.yaml即可。) - 遗留提醒:线上 3080 跑的还是旧写回代码——在面板重复保存 qwen 模型会把 compat 再次清掉,
需重启
dsh web加载新 Host 代码后才能安全使用面板保存。
8.8 数据通道(确认可行,勿改)
- 静态版本:
GET /dsh-dock/balance(webServer 路由 + 同源 fetch);动态预览:harness.handlehost.call。两者等价,静态为主。
8.10 续作会话(2026-08-22 晚):模型设置一键批量操作
- 需求:模型的输入支持(文本/图片 + 标注 视频/音频/文档)与思考强度档,支持一键全勾选和取消勾选。
- 实现(client.js
ModelsView):- 每个模型行:输入行尾部加「全选 / 取消全选」;强度行加「全选 / 取消全选」 (强度全选/取消 = 切 custom 并把全部档置 true/false);
- Provider 区工具栏(
dkm-toolbar):输入全选 / 输入取消全选 / 强度全选 / 强度取消全选, 作用于该 Provider 全部模型草稿;官方 deepseek Provider 强度为 Provider 级共享,隐藏强度批量按钮; - 语义安全:输入取消全选 =
input: []= 继承目录默认(Host 写回时删除 input 字段, 见 index.js 写回逻辑,不会写坏配置)。
- 样式:
dkm-mini/dkm-toolbar。验证:node --check+ SSR(含弹层整树)+ Host 冒烟全绿。 - ⚠️ 8.9 的遗留提醒仍有效:若线上进程未重启加载新 Host 写回代码,面板保存可能清掉存量
compat 治愈字段——保存前先确认已重启过
dsh web。
8.11 图片理解代理(2026-08-22 晚,v0.3.0 内追加)
- 需求:纯文本模型也能"看图"——收图时自动调用配置的视觉模型识别,识别文本替换图片; 多模态模型不受影响;视觉模型从模型目录(多模态模型)里点选。
- 官方现状:纯文本模型收图时运行时把图片替换为
[image omitted …]占位(projectImagesForTextModel, 发生在LlmRuntime.adapterStream,判定依据 prepareCall 返回的modelInfo.inputModalities);没有视觉代理机制。 - 拦截点勘察(勿重复探索):
llm/stream是 cordis waterfall,但next()不收参数、请求对象 frozen——监听器无法改写请求(只读);llm.registerAdapter对已有 provider 抛DUPLICATE_ADAPTER——不能包装官方适配器;- 主对话走
llm.prepareCall→preparedCall.stream(request),其余走llm.stream,两路都汇于streamWithRegistration; - 结论:方法级包装
llm.stream+llm.prepareCall(原函数存llm.__dockOrigStream,own-property 覆盖, dispose 时 delete 恢复原型方法)——唯一的请求级拦截层。dsh 升级若改这两个方法需回归验证。
- 实现:
- 自有 settings 命名空间
dsh-dock(settings.register+ schemastery):visionProxy{enabled,provider,model}, 写走settings.mutate(revision 乐观锁),读走settings.get(内存 resolved 值,热生效); transformForVisionProxy:目标是目录中输入类型不含 image 的模型且请求带图(含 tool-result 嵌套)→ 逐图调用视觉模型(BlockAssembler聚合文本;maxTokens 4096——视觉模型带思考时小限额会被思考吃光,实测), 图片块替换为[图片内容(由视觉模型 p/m 识别):…];识别失败降级为说明文本;总兜底异常不改写不阻断;- 递归保护:识别调用走
__dockOrigStream(天然绕开包装)+dockVisionProxy标记双保险;options.purpose存在(内部辅助调用)不代理;视觉模型自身不代理; - 目录判定用
readModelDirectory的 10s TTL 缓存(避免每请求全量 settings.describe); - GET /dsh-dock/models 附带 visionProxy 与 dsh-dock revision;POST 支持
{visionProxy, revisions}分支。
- 自有 settings 命名空间
- Client:模型设置页顶部「图片理解代理」面板——启用开关 + 视觉模型下拉 (候选 = 目录中输入类型含 image 的全部模型,跨 Provider;当前值不在候选时保留"(当前)"选项)+ 独立保存按钮。
- 验证:Host 冒烟(改写/多模态不改写/关闭不代理/visionProxy 读写/缺模型 400)全绿; 隔离实例 UI 保存 → settings.yaml 持久化 → GET 反映 ✓;真实 API 验证 deepseek-v4-flash-vision-exp 识别小图正确(答「红色」)。⚠️ in-app browser 无文件选择器,完整"发图→识别→回复"流待用户真实浏览器验证。
- 依赖新增:@deepseek-ai/dsh-llm、@deepseek-ai/dsh-settings、@deepseek-ai/schemastery (package.json 声明 + 仓库 node_modules 软链 shim,同 dsh-credentials 模式)。
8.12 事故复盘:图片代理包装致「全模型 stream is not async iterable」(已修复)
- 现象:重启加载图片理解代理后,所有模型每轮请求报
本轮运行失败 stream is not async iterable。 - 根因:
prepareCall包装层用了async (options) => …—— 返回 Promise 而非 AsyncIterable。 消费方(agent-loop)const stream = preparedCall.stream(request); for await (const chunk of stream)的of表达式不会先 await Promise(Node 语义:直接取 Symbol.asyncIterator),Promise 上没有 该符号 → 全量失败。(llm.stream包装用的是 IIFE async generator,同步返回可迭代,没炸; 只有 prepareCall 路径炸——主对话全走这条路。) - 修复:两层包装统一为
(options) => (async function* () { … })()—— 同步返回 async generator。 - 防复发:Host 冒烟新增「包装契约」断言——
llm.stream(...)与prepareCall().stream(...)都必须同步返回 AsyncIterable(非 Promise、有 Symbol.asyncIterator、可迭代)。 - 教训:包装官方函数必须逐条保契约(返回类型同步性也在内);冒烟当时只测了 llm.stream 路径, 漏了 prepareCall 主路径——以后每个被包装的入口都要有契约断言。
8.13 事故复盘:误勾「图片」输入致模型瞎折腾 14 分钟 + PyYAML 二次事故(已修复)
- 用户现象:给 deepseek-v4-flash-0731 发图问「图中有什么」,模型 87 步 / 14 分钟用 bash+tesseract+swift OCR 自己折腾图片文件,而非走视觉模型。
- 根因 1(配置语义):此前「批量输入全选」把 qwen 路由 4 个模型全勾了「图片」—— 其中 glm-5.2 / deepseek-v4-flash-0731 / deepseek-v4-pro-0813 在 pi-ai 内置目录里是纯文本 (GLM-5.x 全系纯文本;qwen 编程套餐目录里多模态的只有 kimi-k2.5+、qwen3.6+/3.7-plus/3.8-max-preview)。 代理规则「多模态模型自己识别」→ 跳过;图片原样发端点 → 端点不认 → 模型只拿到附件文本引用 → 工具瞎找。
- 修复 1:治愈 settings(4 处 input 删除、留空继承目录=纯文本;qwen3.8-max 真多模态保留)。 语义已在面板写明:勾「图片」= 端点原生支持、原图直发;不确定勿勾,交给代理。
- 根因 2(判定源不一致):代理此前用插件目录副本判多模态——条目留空时副本显示路由默认
(纯文本),但运行时解析链是
entry.input ?? 内置目录 ?? 路由默认:kimi-k2.7(qwen 路由)条目 留空却靠目录继承 [text,image],会被误拦。 修复 2:判定改用llm.resolveModelInfo(provider, model)(dsh-llm 运行时公开方法,与官方 投影同源);旧宿主无此方法/单次解析失败时回退插件目录。GET 目录逐模型附runtimeInput, 面板「图片理解代理」区展示「原图直发(多模态)/ 走视觉代理(纯文本)」分组。 - 提速:识别调用先试
reasoningEffort: 'off'(关思考求快);报 UNSUPPORTED_REASONING_EFFORT 则回退默认档重试一次。 - 二次事故(自查自纠):治愈脚本用 PyYAML load/dump 转储 settings.yaml——PyYAML 是 YAML 1.1,
把
off:键解析成布尔 False 再写回成false: null(键名腐蚀);且全新启动时 llm-pi-ai schema 校验不过 → 全部 pi-ai Provider 消失(热加载进程不重启则无感,极具迷惑性)。 补救:从备份做纯文本手术(只删 input 行、不动其他字节)。 教训:绝不用 PyYAML 转储用户 settings.yaml(YAML 1.1 布尔陷阱 off/yes/on/no);要改就文本级 手术,或让 settings 服务自己写(settings.mutate)。 - 验证:双实例(3080 线上 / 3999 隔离)全新启动后 5 Provider 齐全;runtime 判定 治愈模型=text(走代理)、qwen3.8-max=text,image(直发+当视觉模型);Host 冒烟新增 5 断言 (runtimeInput 附带 / 目录继承多模态不误拦 / 解析失败回退 / off 被拒自动重试 / off 先行尝试)全绿。
- GLM-5.3(fangzhou 自定义路由):未进内置目录 → 运行时默认纯文本 → 收图自动走视觉代理 ✓。
8.14 终局复盘:图片理解代理「配了还是不能识别」三层根因(已修复,真实全链路验证通过)
用户反馈配好了代理仍不能识图(模型抱怨"sha256 不是文件路径")。隔离实例 + API 注图复现, 探针矩阵二分定位,共三层根因(一次比一次深):
- 官方带图准入拦截(首层):apiproxy 的
session.prompt收到图片先查ctx.llm.resolveModelInfo(provider, model)的 inputModalities,纯文本模型直接拒绝MODEL_DOES_NOT_SUPPORT_IMAGES——请求根本进不到 llm.stream 包装层。 修复:安装时同时包装resolveModelInfo(虚拟多模态):代理启用且目标≠视觉模型时, 纯文本模型的 inputModalities 对外补 'image'(浅拷贝,不改 truth);改写判定继续用原函数。 副作用勘察:其余调用方(模型选择器目录只用 reasoning 字段;mcp-client/tool-fs 的图片 声明检查——放行反而正确,工具产图也走代理);官方内部投影不走此公共方法。 BlockAssembler.finish是 getter,恒真值(最深的根因):finish未收到 finish chunk 时返回{kind:'stop'}——if (assembler.finish) break会在第一个 chunk 后就 break, 识别永远只消费 1 个块(block-start)→ 空文本 → "视觉模型没有返回文本"。 ~350ms 秒败 + 位置相关曾误导出"端点限流"假说(探针 A-E 用c.type==='finish'恰好成功, F-J 用asm.finish恰好失败,与调用次序重合)。修复:if (chunk.type === 'finish') break。 教训:消费官方对象先核对属性语义(getter/方法/默认值),冒烟的 fake 流第一个 chunk 就带文本所以测不出该 bug。- 识别调用通道:改走原
prepareCall().stream()(与 agent-loop 带图请求同通道,被验证 路径);options 以prepared.config(resolved)为底构造,否则callConfigEquals拒绝。 不带 sessionId(llm/stream 的会话检查点监听对带 sessionId 的调用先做检查点)。
识别结果缓存(防真实限流):attachmentId+视觉模型 → 文本,TTL 10 分钟、上限 64 条。
agent 每轮重试/多步都带历史图片重发,逐图重识别既慢又易撞端点节流;失败不缓存。
导出 __clearDescribeCache() 供冒烟隔离。
验证(隔离实例 3999 真实全链路,API 注图):
- deepseek-official/deepseek-v4-flash-vision-exp 视觉模型:识别 168 字,主模型 deepseek-v4-flash-0731 正确答出"蓝底+黄方块+绿长条"(与测试图一致),无工具折腾;
- qwen-token-plan-cn/qwen3.8-max 视觉模型(用户实际配置):识别 174 字,同样正确。 Host 冒烟新增:虚拟多模态(启用宣称/停用保真/多模态不重复加)、识别缓存命中断言。
8.15 v0.4.0:模块化架构 + 用量记录(2026-08-22 深夜)
需求:①新功能【用量记录】放第二个菜单,功能与页面参照 /Users/weiyi/develop/gitea/wycto/dsh-token-usage
(仅作参照,不动那个仓库与 npm 包);②dsh-dock 像dsh 一样由一个个独立功能模块组成,任何模块可拎出单独打包发布,又能装回面板。
架构落地(feature 模块化):
- 仓库重构:
features/<id>/host.js(宿主半部,纯 ESM 零构建)+features/<id>/view.js(x)(客户端视图)+src/client.jsx(外壳组装)+src/host-core.js(共享内核);index.js只 import 组装(1359 行 → ~90 行)。 - 客户端放弃零构建(本版唯一推翻的历史决策,见三、表格 8.7 条上下文):esbuild bundle 成单文件
client.js(react 系外部化走运行时 seed,@wycto/dsh-token-usage同款线上已验证模式)。理由:650 行 JSX 视图手写 createElement 不可维护、真实源码文件无从模块化。宿主半部保持零构建。改客户端后必须node scripts/build-client.mjs。 - 模块契约:视图导出
{ id, name, order, accent, description, css, View, HomeStat?, Chip? }(order 定菜单次序,当前:modelconfig=100/tokenlog=110/balance=120/animation=130/heartbeat=140/theme=150);宿主导出{ id, name, description, defaultEnabled, setup(ctx)→disposer };视图收到{ ctx, feature, params }props(params 来自导航总线)。 - 会话区 chips(0.4.0 追加):外壳注册
conversation.input.left(模型选择器左侧工具行,官方文档指定可点击小控件的座位),渲染各已启用且「会话页显示」功能的Chip组件;余额 chip 显示当前选中 Provider 余额(agentDefaultModel 快照),点击 openPanel('balance', {provider}) 定位并高亮行;用量 chip 显示当前会话总 Token(query sessionId,10s 刷新),点击 openPanel('tokenlog', {sessionId}) 按会话筛选。开关状态经 shared.js 总线(面板导航 + 功能开关 + chip 显隐,localStorage 持久化dsh-dock/chips/v1),避免外壳↔模块循环依赖。 - dockBridge 回装通道:
client.js导出dockBridge.register(def);独立功能包(scripts/extract-feature.mjs生成的骨架)在浏览器端ctx.modules.import('dsh-dock')成功→注册进功能坞(独立入口隐藏),失败→自己独立面板。跨插件 import 是 DSH client-modules 官方机制(懒 CJS 注册表 + boot 图,已读dsh-client-modules源码验证),非 hack。外部视图有 FeatureBoundary 错误边界 + 「外部」徽章 + package 标注。 - 提取脚手架
scripts/extract-feature.mjs <id>:镜像仓库布局生成独立包(宿主入口/双形态客户端入口/构建脚本/patch/package.json),脚手架性质、发布前需裁剪实测。
用量记录(tokenlog):宿主移植 dsh-token-usage/lib/index.js(session/event 实时 + 启动全量扫描、sessionId:seq 去重、turn/end 两遍回填、峰谷定价 + 官网价目 24h 抓取 + dsh-dock-tokenlog 命名空间覆盖),RPC POST /dsh-dock/tokenlog/query|export|scan;视图移植其 client v6(筛选/KPI/分组/明细/CSV/5s 自刷新/localStorage 暂存),CSS 前缀 dtok-,dock 内自适应(无 overlay 壳,全屏用弹窗最大化)。顺带修了原版分组按钮一次旧维度过期查询(runQuery 带 dimOverride)。
验证:node --check 全绿;构建产物含 ModuleLoader 壳/exports.dockBridge/仅 react 系外部依赖(bundle 纯度断言,8.7/8.12 教训延续);/tmp/dock-smoke-v040/smoke.cjs 全绿——假 react(手搓 hooks 派发)渲染整棵弹层树断言菜单次序(首页/用量记录/模型设置…)、外部桥注册排序与徽章、错误边界;宿主假 ctx 装配断言三路由注册、tokenlog 摄取→查询→分组→CSV 闭环(含 token 桶/派生字段/llmMs/状态回填/金额非零)。浏览器实测见后续补充。
注意:
- 冒烟假 ctx 的
effect(fn)必须立即执行 fn(cordis 语义),否则 tokenlog 路由注册不到——曾在此误判路由缺失。 - Node 26 有实验性全局 localStorage,冒烟里
typeof localStorage不再是 undefined(try/catch 兜底无碍)。 - visionproxy 依赖 modelconfig 的
readModelDirectory(模块间 import),提取时两个模块要一起拎。
8.16 v0.5.0:任务动画(2026-08-23)
需求:参照 /Users/weiyi/develop/gitea/wycto/dsh-task-pulse 做任务动画功能,但动效不要花哨、要克制高级;支持单独开启动画 / 单独开启通知;所有配置持久化。
实现(features/animation/ 模块,占位转正):
- Host(features/animation/host.js):会话级任务追踪移植自 task-pulse(agent/status 驱动开始/结束、session/event 补回合/步骤/工具/Token/结束原因/首条输入与末条摘要、agent/disposed 兜底;readTitle 异步回填标题),去掉钉钉推送。RPC
POST /dsh-dock/animation/status|config;配置经settings.get/mutate(DOCK_NS)持久化到 settings.yamldsh-dock.animation段(schema 在 host-core.js DockConfig 扩展:animationEnabled/effectMode/notifyEnabled/notifyOnComplete/notifyOnError/notifyStayMs/systemNotify)。config 增量合并、逐字段类型校验(未知 effectMode 忽略、stayMs 钳 0~600000)。 - Client(features/animation/view.jsx):
- 新模块契约 Overlay:功能描述符可挂全局浮层组件(props {ctx, feature}),外壳新增 shell.overlay 注册
dsh-dock-feature-overlays(order 22)统一渲染已启用功能的 Overlay(FeatureBoundary 包裹;dockBridge 外部功能同样支持)——与 Chip(会话区小控件)互补,Overlay 是常驻整页 UI。 - 动效(全部走 dsw 主题变量、暗/亮自适应):任务进行中右下角玻璃拟态状态徽标(任务数+计时,点击 openPanel("animation"));三选一动效模式 flow=顶部 2px 流光细线(默认)/breathe=徽标圆点呼吸/ring=圆点细环匀速旋转;任务结束徽标与细线自动消失,完成瞬间一缕一次性流光掠过(成功绿/异常红,onAnimationEnd 清场)。
- 通知:右上角 toast 卡片栈(标题/模型/耗时区间/回合步骤工具/Token/摘要/错误),完成与异常可分别开关、停留时长 4s~30s 或常驻、最多叠 4 张、out 动画后卸载;可选浏览器系统通知(仅 document.hidden 时推送,开关点击时 requestPermission——用户手势)。结束检测靠轮询 diff(2s 活跃/6s 空闲/15s 出错自适应,visibilitychange 立即刷新)。
- 面板页:动画/通知两组独立开关 + 模式选择卡(缩微实时预览)+ 进行中/最近完成列表;每个开关即时 RPC 保存(乐观更新,回包经 animationStore.applyConfig 对齐)。共享 animationStore(浮层轮询驱动,View/HomeStat 只读)。
- 新模块契约 Overlay:功能描述符可挂全局浮层组件(props {ctx, feature}),外壳新增 shell.overlay 注册
- 功能开关持久化(0.5.0 路线图项顺手落地):shared.js
dsh-dock/features/v1localStorage(与 chips 显隐同模式);Host↔Client 开关双向同步仍未做(面板停用只影响浏览器侧,Host 侧 defaultEnabled 为准——见 index.js 注释)。
验证:node --check 全绿;构建产物仅 react 系外部依赖;冒烟 /tmp/dock-smoke-v050/smoke.cjs 全绿(bundle 契约/5 slot/导航次序/浮层渲染、动画追踪闭环 running→事件→idle→status、config 合并→mutate 落盘→status 反映、非法值钳制、tokenlog 双监听共存无回归);隔离实例(/tmp/dsh-verify-home3,端口 3998)真实 RPC:status 默认配置 ✓、config 写入 settings.yaml animation 段 ✓、status 反映新值 ✓、/plugins/dsh-dock/client.js 服务的就是新构建(字节数一致)✓。
注意:
- 冒烟假 react 曾把 useEffect(fn) 的 fn 当 useState 初始化函数立即执行(v040 遗留),撞上 overlay effect 里的 document 才暴露;已改为 effect 只存不执行(React 语义)。
- 会话标题是异步回填:毫秒级结束的任务可能落到 firstPrompt 兜底标题(task-pulse 同款行为,可接受)。
- 线上 3080 旧宿主对 /dsh-dock/animation 返回 405(无路由);用户需手动重启 dsh web 后动画路由与新 client bundle(rev 模块映射在 boot 时生成)才生效。
- v040 冒烟脚本 7 项"失败"均为预期变化(slot 4→5、动画转正无规划徽章、单监听器假 ctx 被 animation 覆盖 tokenlog),非产品回归;v050 冒烟已断言双监听共存。
8.17 事故复盘:任务动画页排版全乱(2026-08-23,已修复 + 防复发断言)
- 现象:用户反馈任务动画页排版错乱——模式选择卡变成一行挤在一起的纯文本、无卡片边框、第三张卡溢出内容区、设置行间距全无。
- 根因:
features/animation/view.jsx定义了css常量,但 feature 描述符导出时漏挂css字段——外壳ensureCss()只按f.css收集模块样式,动画模块的 28043-19507≈8.5KB 样式整体缺失,所有dkan-类裸奔(.dkan-mode退化为 inline-block、.dkan-sec/.dkan-row退化为 block)。 - 为什么冒烟没拦住:v050 冒烟只断言了视图渲染与 RPC 闭环,没断言注入样式内容——又是 8.7 教训的重演(浏览器端才炸的问题,纯 Node 冒烟查不出)。
- 定位手段(可复用):浏览器实测 + 只读
evaluate量 computed style:style[data-plugin-css="dsh-dock"]里includes('.dkan-mode')为 false、标签尾部还是 balance 样式 → 直接锁定「css 没进 fullDockCss」。 - 修复:feature 描述符补
css,字段;顺带修掉系统通知说明文案的重复括号(permNote 已含「仅页面后台时推送」又拼了一次)。 - 防复发:v050 冒烟新增 A2b——假 document 捕获注入样式,断言含
.dkan-mode{/.dkan-badge{(动画)与.dtok-/.dkb-(存量),任何模块忘挂 css 当场红。 - 验证:重建后浏览器实测——CSS 注入 28043 字符、三张模式卡 flex 并排各 269×93 带边框、sec/row/head 全部恢复 flex;浏览器点击「轨道光环」→ settings.yaml 即时出现
effectMode: ring(交互→RPC→落盘闭环)。
8.18 版本策略(2026-08-23 用户指示)
- 内部调试一律用小版本递增(当前 0.4.2);等用户明确说发布时再升到目标大版本(任务动画对应 0.5.0)。
package.json与src/client.jsx的 DOCK_VERSION 保持同步 0.4.2。
8.19 钉钉群机器人推送(2026-08-23,0.4.2 内追加)
需求:任务动画的通知支持对接钉钉群机器人,参照 /Users/weiyi/develop/gitea/wycto/dsh-task-pulse(宿主侧 webhook 推送同款)。
实现:
- 配置(schema 扩展
dsh-dock.animation):dingtalkEnabled(默认 false)+dingtalkWebhook(默认 '')。config RPC 校验:非空 Webhook 必须以 http(s):// 开头否则 400(顺带修了路由 catch 一律 500 的问题,改按 e.statusCode 映射)。 - 宿主推送(features/animation/host.js):
finishSession归档后pushDingtalkIfNeeded(record)异步直发(不阻塞)——宿主侧发送,浏览器关着也能推;事件筛选跟随notifyOnComplete/notifyOnError(完成推/异常推与页面通知共享语义,通道各自开关)。markdown 消息移植 task-pulse 版式(任务/模型/耗时区间/回合步骤工具/Token/结果/摘要/署名),标题含「任务」二字——钉钉自定义关键词填「任务」或「dsh」即可命中。sendDingtalk判定成功 = HTTP 200 且业务 errcode===0(比 task-pulse 只看 res.ok 更严)。 - test RPC(
POST /dsh-dock/animation/test):用已保存 Webhook 发测试消息,返回{sent, error}(连通失败也是 200 + sent:false,UI 内联展示错误,不当 HTTP 错误抛)。 - 客户端(view.jsx):通知区之后新增「钉钉推送」卡片——开关 + Webhook 输入(草稿「保存」按钮 + 「发送测试消息」按钮 + 内联测试结果)+ 机器人创建指引。同步保护:配置轮询回包不再覆盖乐观值/输入草稿——
pendingSavesRef(保存进行中跳过同步)+editingWebhookRef(输入中跳过同步;保存成功后手动对齐一次,因同步 effect 在编辑期被跳过不会再触发)。 - 验证:冒烟 B4(fetch 桩)——推送格式(msgtype=markdown/标题含任务完成/含任务与模型与摘要/署名)、开关门控(关推送不再发)、事件门控(notifyOnComplete=false 完成不推)、errcode≠0 与 HTTP 非 200 两态 sent:false、非法 Webhook 400;隔离实例真实 RPC——400 状态码映射 ✓、Webhook 落盘 ✓、拒连端口 test 优雅返回 sent:false("fetch failed") ✓;浏览器实测——钉钉区渲染/输入框/按钮 ✓、点击开关落盘 ✓。
8.20 动画增强:错位修复 + 速度驱动 + 环屏巡航 + 桌面机器人(2026-08-23,0.4.2 内追加)
用户反馈:①徽标/浮层与 dsh 自身 UI 错位;②动画不明显;③要更多动效(绕屏幕转圈、速度随任务);④桌面机器人(多显示器工位、思考/写码/查资料三态同步、要逼真);后追加:⑤机器人要侧身/背侧视角;⑥机器人可自由拖到屏幕任意位置防遮挡。
实现:
- 错位根因与修复:徽标固定
bottom:20px压在 dsh 输入卡(--dsh-composer-height,约 152px)与 Details 区上。修复:徽标/机器人默认停泊抬到bottom:calc(var(--dsh-composer-height,152px) + 20px);功能坞面板打开时环境动效整体隐藏(subscribePanel 驱动 ambientOn,避免与弹窗重叠);钉钉 Webhook 行nowrap防按钮换行错位。 - 速度驱动:overlay 每次轮询按 Token 吞吐(Δtokens/Δt)算 speed=clamp(0.7+t/45, 0.7, 3),经 CSS 变量
--dkan-speed下发;全部动效时长用calc(base / var(--dkan-speed))——流光/呼吸/旋转/巡航/机器人敲击全随任务忙闲变速。 - 新动效:
orbit环屏巡航:12px 光点(发光+拖影光晕)沿屏幕四边转整圈(keyframes top/left 四段)。robot桌面伙伴:背侧视角纯 CSS 场景(176×108)——横贯书桌、三台显示器(侧屏 rotateY ±24° 透视内倾)、机器人见后脑勺+双耳+天线+椅背+双肩前臂。data-phase三态:think=屏幕调暗+歪头+思考泡泡;code=双臂高频交替敲击+中屏代码行滚动;search=头部左右扫视+侧屏滚动。整卡 pointer 拖拽(≥4px 位移阈值区分点击,拖后位置写 localStoragedsh-dock/anim/robot-pos/v1,挂载恢复时钳回视口内),默认停泊右下输入卡上方。
- 阶段追踪(host):
tool/call事件带data.name(dsh-agent-loop appendToolCall 实证:turn/step/callId/name/arguments)。分类:web|search|fetch|grep|glob|read|ls→search,其余(edit/bash…)→code;step/start 与 assistant/message→think。status RPC active 条目新增 phase/phaseAt/lastActivityAt;面板「运行状态」行显示阶段标签。 - 可见度增强:流光细线 2px→3px、底色 16%→30%、扫光段加白端+辉光;呼吸/环加大加亮。
验证:冒烟全绿(新模式合法值、phase 三态断言 step/start→think / webfetch→search / str_replace_editor→code、新 CSS 注入断言 .dkan-orbit/.dkan-botcard/.dkan-bot-scene[)。隔离实例 + 真实任务 E2E:40s bash 任务运行中实测——host 侧 phase 实时流转(bash 期 code → 收尾 think);浏览器侧五种模式元素逐一出现(orbit/line/badge+dot/badge+ring/botcard 各=1);机器人卡片背侧结构齐全(臂x2 屏x3 椅x1 耳x2)、caption「1 个任务 · 00:07 · 编写代码」;cua.drag 拖到 (328,95) 成功、刷新页面后新挂载从 localStorage 恢复同位置;错误边界 0 触发。
注意:
- 会话视图的输入框 placeholder 从 "Describe what you want to build"(新会话)变为 "Message the agent"(进入会话后),自动化测试按角色名定位。
- 机器人阶段是"最近一次事件"的粗粒度推断(事件驱动,无流式细节)——assistant/message 后短暂 think、bash 长跑期稳定 code,符合直觉即可,不是精确的 token 级流式状态。
- dsh 输入卡高度走
--dsh-composer-height(152px 回退值),dsh 升级改高度时停泊位自适应。
8.21 3D 立体机器人 + 流级阶段同步(2026-08-23,0.4.2 内追加)
用户反馈:①机器人要 3D 立体、侧身面对镜头;②同步 bug——正文已经在输出,机器人还显示"思考中"。
同步 bug 根因与修复:旧阶段只靠事件级推导(step/start/assistant/message/tool/call),消息粒度太粗——模型流式输出期间没有任何事件更新阶段。修复:接 assistant/chunk 流级事件(dsh-agent-loop 每 token append 一条 {turn, step, chunk},chunk.type 为 reasoning-delta/text-delta 等,dsh-llm invariant.js 实证):reasoning-delta→think、text-delta→write(新增阶段"输出中");assistant/message 落盘后不再回跳 think(维持 chunk 末态,避免输完还闪思考)。
3D 实现(纯 CSS,无三方库):Box3 组件 = 六面长方体(前/后/左/右/顶/底,backface-visibility:hidden + 面级明暗渐变出光感);世界层统一 rotateX(-15deg) rotateY(-30deg) 取 3/4 视角(俯视 + 侧身面向镜头);场景 = 书桌+桌腿、三台薄盒显示器(组内扇形微转、屏幕贴前面板)、机器人(椅背/椅座/躯干/头组/双臂肩+肘两级关节/键盘)。阶段驱动:think=仰头(rotateZ)+泡泡+屏幕调暗;write/code=低头+肘部高频敲击+中屏代码滚动;search=头部 rotateY 左右扫视+侧屏滚动。速度仍走 --dkan-speed。
验证:冒烟补 chunk 断言(reasoning→think / text→write / message 不回跳)全绿;隔离实例真实任务每秒采样阶段时间线:——bash 期 code、输出流期 write 精确对应;3D 结构体检(15 个长方体/3 屏/2 肘/2 眼、world matrix3d 生效)。 注意:环境动效在功能坞面板打开时自动隐藏(8.20 防重叠设计),浏览器验证机器人必须先关面板;面板打开期间观察不到卡片是预期行为。
8.22 机器人换 3D 动漫人物 + 修身体被桌遮挡(2026-08-23,0.4.2 内追加)
用户反馈:①机器人只见头不见身子;②换成 3D 动漫真人角色;③动作要随任务切换(思考完要有写代码动作)。
只见头的根因:8.21 的机器人组 translateZ(-6px) 与书桌同深度,躯干/椅/手臂埋进桌体被遮挡,只有头探出桌面。修复:人物组改 translateZ(30px) 坐到桌前近镜头侧(侧身面对屏幕、背对镜头偏侧),桌子收窄到右侧,键盘 z=14 放桌前缘手边——头/躯干/腿/椅/手臂全部可见。
换人:机器人 → 动漫人物(仍纯 CSS 长方体):后发+顶发(棕)、脸(肤色+深色眼睛+脖颈)、卫衣躯干、坐姿腿(大腿前伸+小腿垂下)、双臂带手(肤色手块)、椅子。配色走 dk3-skin/dk3-hair/dk3-hood/dk3-pants 面级渐变。动作语义不变(think 仰头+泡泡 / write+code 低头敲键盘 / search 扫视),阶段仍由 assistant/chunk 流级同步。
验证:结构体检(人物组/头/发×2/眼×2/躯干/腿×2/臂×2/手×2/椅,人物 matrix3d z=30);真实任务时间线 ——思考→写代码→输出逐段切换。
8.23 人物居中布局修正(2026-08-23,0.4.2 内追加)
- 用户反馈:人物坐在桌子左边缘外,要坐到桌子中间。
- 修正:场景加宽 200→220px;书桌 200 宽居中(x=110 中心);人物组 left 16→96(中心 x≈118 ≈ 桌中心);三屏收到人物右侧扇形(x=128/166/196,ry ±20°);键盘 x=126 在人物手边桌前缘。
- 几何验证(面板预览卡实测):人物中心与桌中心偏差 8px(居中);人物 bottom 落在桌沿下(坐姿正确,腿在桌面下方);三屏在人物右侧、键盘在手边。
- 注意:本节验证时隔离实例的模型配额已耗尽(429 insufficient_quota,周配额 08-28 重置)——真实任务 E2E 已不可行,布局验证改走面板预览卡(静态渲染同款 RobotScene)+ getBoundingClientRect 几何测量;阶段流转逻辑未改动(8.22 已验证)。
8.24 手机接力正式接入 + 局域网电脑直连(0.0.0.0)(2026-08-28,未发布)
需求:用户希望局域网内其他电脑能访问完整 DSH(会话记录、工作区文件夹选择等全部交互与 127.0.0.1 一致),并明确要求"以 0.0.0.0 启动",实现放在 dsh-dock 的【手机接力】里;主实例(3080)不得挂掉,另开新端口测试。
关键事实(实证自 dsh 0.1.1-rc.2 源码):
- CLI 拒绝
--host 0.0.0.0(dsh-web-app/lib/startup.js显式program.error,理由:浏览器表层无登录认证,直开会把代码执行能力交给同网任何人); - 但
webServer行 schema 本身支持0.0.0.0(dsh-host-webserver),合成配置里该行 id =webserver,host 来自ctx.webStartup.host ?? '127.0.0.1'; - 绑定 0.0.0.0 后
dsh-web-app的resolveLanTrust自动把全部局域网 IPv4 派生进/api信任白名单;目录选择器directory-picker-auto按bindHost !== '127.0.0.1'自动切成 browse 模式(浏览器内浏览/新建目录,host.listDirectory/host.createDirectory)——这正是局域网电脑选工作区文件夹所需;host.pickDirectory(本机原生对话框)属于PRIVILEGED_METHODS,任何 LAN 访问都 403,与真 0.0.0.0 绑定行为一致。
实现(不复制会话,同资料共享):
features/mobile-relay/host.js新增lan/lan/start/lan/stopRPC:写一条- id: webserver, config: {host: '0.0.0.0', port: N}补丁到~/.dsh/dsh-dock-lan/lan-N.patch.yml,用process.argv[1](当前 dsh 入口)spawnnode <入口> --profile web --patch <补丁> --port N --no-open,stdout/stderr 进lan-N.log;端口预检(0.0.0.0 绑定探测)、就绪轮询(25s 超时带日志尾部报错);- 子实例环境标记
DSH_DOCK_LAN_CHILD=1+DSH_DOCK_LAN_PARENT_PID:子实例内lan/start拒绝(防递归嵌套);每 5s 检查process.ppid,主实例退出即自退出(孤儿保护); features/mobile-relay/view.jsx新增LanDirectCard(两种视图分支都渲染):端口输入(默认 3082)、状态徽标、地址列表 + 二维码 + 复制、一键启停、安全警示;- 接线:
index.jsFEATURES +src/client.jsxBUILTIN_FEATURES(order 80)+ 重建 client.js(版本号未提升,待发布时再定)。
集成测试实录(主实例 3080 全程存活):
- 实例 A(0.0.0.0:3082,手动补丁同机制启动):loopback/LAN IP 均 200;
Host: 172.18.98.20:3082下host.describe/host.listDirectory/session.list全部通过信任篱笆;host.pickDirectoryLAN 403(预期特权钉死);pickDirectoryloopback 报composed picker serves "browse"(证明 browse 已挂载);host.listDirectory返回目录树、host.createDirectory可建目录; - RPC 链:A 上
lan/start {port:3083}→ 子实例 B 1s 内就绪,LAN IP 可访问,lan状态child:true;B 上lan/start返回 400 拒绝;B 的session.list与主实例同一份;Alan/stop→ B 进程退出、端口释放;停止后可重开;杀掉 A → B 在 5s 内自退出(孤儿保护生效); - 收尾:3082/3083 均无残留监听,
http://127.0.0.1:3080/保持 200。 - 遗留注意:macOS 防火墙首次放行 node 入站可能弹窗(本次测试 172.18.98.20 直连成功,未遇拦截);直连子实例与主实例同时写同一份
~/.dsh会话文件属预期共享语义,避免两端同时编辑同一会话。
8.25 局域网直连 crypto.randomUUID 修复(2026-08-28,未发布内追加)
用户反馈:局域网电脑打开直连实例后左侧无会话记录、不能打开工作目录,控制台报 crypto.randomUUID is not a function。
根因:局域网 http://<IP> 不是浏览器安全上下文(只有 HTTPS 或 localhost 才是)。部分浏览器在非安全上下文只暴露 crypto.getRandomValues() 而省略 crypto.randomUUID()。DSH 的 connection 客户端在生成消息 ID(MessageId(crypto.randomUUID()))与 RPC ID(RpcId(crypto.randomUUID()))时直接调用它——缺失则整个客户端引导失败,表现为会话列表空、工作区不可用等"全面瘫痪"。手机接力网关此前已用代理注入 compat.js 解决同问题,但直连子实例是裸 DSH 服务、没有代理层,所以漏了这一步。
修复:直连子实例(isLanChildInstance() 为真)在 webServer.tapIndex 注册一个 transform,向 index.html 的 <head> 之后内联注入同款 UUID v4 兜底脚本(LAN_COMPAT_JS),先于任何 DSH 引导脚本执行;幂等(marker 防重注);本机回环下 randomUUID 已存在→脚本自我短路零副作用。仅子实例注册,主实例 HTML 完全不受影响。
验证:带子实例标记启动 0.0.0.0 实例,index.html 的 <head> 后即见 <script data-dsh-lan-compat>(loopback 与 LAN IP 均注入);VM 模拟非安全上下文(有 getRandomValues 无 randomUUID)跑注入脚本→randomUUID 被定义且产出合法 UUID v4;重启用户现役子实例后 session.list@LAN 正常返回 59 条会话,主实例 3080 保持 200。
2026-08-28 · v0.8.1 联调修复:手机接力/局域网直连三个链路级问题
用户反馈:手机端接力打开没有历史会话记录、不能选择文件夹工作。在 3480 测试实例(源码安装 pnpm dsh web,插件经 ~/.dsh/profiles/web 以 file: 依赖挂载)逐项复现与修复:
1) 手机端不能选文件夹(架构):首版接力网关上游指到主实例(127.0.0.1)→ 主实例目录选择器为 native(directory-picker-auto 按 bindHost 解析,win32+回环 → native),手机端 host.listDirectory 被 DSH 拒绝(directory-picker-unavailable,实测复现)。修复:手机接力与局域网直连共用同一个 0.0.0.0 子实例(ensureLanChild 幂等复用),子实例补丁钉死 browse 组合(停用 directory-picker-auto,直接挂 -browse 后端 + ui-directory-picker-browse 前端面);网关按路径分流:/dsh-dock/mobile-relay/* 配对 RPC 转发回主实例(配对状态/任务摘要在主进程),其余流量转发子实例。
2) 信任链路(会话列表):子实例绑定 0.0.0.0 后 DSH 派生局域网 IP 信任(web-app resolveLanTrust → connection 行 trustedHosts),信任篱笆(api-request-trust.ts)校验请求自带 Host/Origin 一致性而非回环名——网关代理头从「改写 Host/Origin 为回环 + 剥 cookie」改为原样透传(改写反而令 Origin 校验失败 403;GET 无 content-type 的 HTML 请求仅去 accept-encoding 以便注入 compat)。实测手机链路 session.list 返回与主实例同一份历史会话。
3) 源码安装下子实例 spawn 崩溃:dshEntry() 取到 .ts 入口(tsx 运行),裸 spawn 无法执行——透传 process.execArgv(--import tsx/esm);子命令参数顺序:web --patch <path> --port <port>(commander passThroughOptions 在第一个未知旗标后全部透传,--patch 放 --port 后会报 unknown option '--patch');就绪超时 25s → 60s(tsx 冷启动实测 ~18s)。
其它:无设备配对 90s 僵尸回收(否则卡住 lan/stop 满配对窗口);lan/stop 守卫只看有设备在线的配对;DSH_DOCK_ENTRY/DSH_DOCK_LAN_PORT 测试桩(宿主单测覆盖「接力拉起共用子实例/有配对拒绝 lan/stop/end 后网关关/lan/stop 后子实例退」)。
验证(3480 实例全链路):LAN 直连 host.listDirectory(浏览 F:\workspace)+ session.list ✓;手机 connect→join(经网关分流回主实例)→status ✓;经网关 session.list/host.listDirectory ✓;end→网关关、lan/stop→子实例退 ✓。两个单测(gateway/host)在透传语义下通过。
部署注意:profile 里的插件是 file: 快照——同版本号 pnpm add 会跳过,改代码后须 pnpm remove dsh-dock && pnpm add file:F:/workspace/wycto/gitea/dsh-dock;仓库 checkout 无 node_modules,跑测试需 junction qrcode/@deepseek-ai/schemastery(已建,gitignore 内)。
2026-08-28(晚)· 架构定稿:服务器模式(单实例 0.0.0.0)
用户明确目标形态:"dsh 装在服务器上,任何设备访问都一样,包括正在进行的任务"。此前共用子实例方案的根本缺陷暴露:主实例与子实例是两个进程,agent 运行在各自进程里——3082 上继续的会话,127.0.0.1 看不到运行状态;反向亦然。任务同步跨进程不可行,唯一正解是单实例。
重构:删除整个子实例机制(spawn/补丁生成/孤儿保护/测试桩),改为「服务器模式」——插件把 - id: webserver / config: {host:'0.0.0.0', port} 写进用户 profile 补丁层(~/.dsh/profiles/web/cordis.patch.yml;js-yaml + 与 include 插件同款 !!js Type 无损往返,幂等 upsert),重启后主实例直接绑定 0.0.0.0:绑定即派生局域网信任、目录选择器自动 browse(不再需要钉死补丁);手机接力网关上游改回主实例本身(配对 RPC 分流也不再需要)。tapIndex 兼容注入改为无条件注册(回环自短路)。
附带结论:浏览式选目录本来就支持任意路径——顶部面包屑/铅笔是"点击可编辑路径",直接输入 F:\workspace\xxx 即可跨盘;"只有 C 盘"是没发现这个入口,不是缺陷。
验证(3480):lan/start 写补丁→重启→0.0.0.0:3480 LISTENING、127.0.0.1 与 10.31.1.4 均 200、compat 注入 OK;LAN IP 浏览选目录(列出 F:\workspace\wycto)+ session.list OK;手机 connect→join→status、经网关 session.list/host.listDirectory OK;lan/stop 补丁还原 []。两个单测(网关透传语义 / 服务器模式补丁往返+幂等)通过。
部署:profile 快照 file: 依赖同版本跳过——更新代码后必须 pnpm remove dsh-dock && pnpm add file:<仓库>;仓库 checkout 需 junction qrcode/js-yaml/@deepseek-ai/schemastery 到 profile node_modules(均已建,gitignore 覆盖 node_modules/)。新增 npm 依赖:js-yaml ^4.1.0。
2026-08-28(夜)· 定稿:远程访问 = 账号密码登录网关(主实例回仅本机)
用户要求"除 127.0.0.1 外的访问都要弹账号密码,能登录/退出"。最终架构:删除扫码访客与配对机制,网关升级为账号认证前门——登录页(纯静态 HTML+CSS,表单原生 POST,无内联脚本)→ 验证通过发 HttpOnly 会话 Cookie(7 天滑动)→ 代理到主实例;/__dsh_auth/logout 注销;失败按 IP 限速(10 次/10 分钟)。账号存 dsh-dock settings 命名空间(sha256+盐,timingSafeEqual 比对),auth/set 改密即 revokeAllSessions 全端下线。开启入口同时:写 browse 选择器钉死补丁(重启生效)+ 自动移除旧服务器模式 0.0.0.0 行(迁移)。主实例回到仅本机 = 结构上不存在免登录直连;单实例 = 任务进度全端一致。
踩坑:Mimosa 安全钩子对 <script> 字符串构造极敏感(loginPage 的静态路径拼接也被拦)——页面一律用无脚本纯 HTML(错误态走服务端 303 + 独立静态页,自动跳转用 meta refresh);window.__DSH_REMOTE__ 标记注入被拦后改用 /__dsh_auth/health 探针检测远程态。测试注意:node fetch 默认跟随 302(门禁断言须 redirect:'manual')且默认 Accept 不含 text/html(须显式带)。
验证:网关单测(门禁 302/401、登录页、错/对密码、Host 改写、Cookie 透传、WS、退出、revokeAllSessions)+ 宿主单测(未设账号 400、开启后凭据登录全链路、auth/set 改密旧会话作废、补丁幂等、关停清理)全绿;3480 实测 LAN IP:未登录 302 → 登录页 → 401/200 → 会话代理 → 退出 302。共享 3480 端口与用户活动实例相撞导致两轮误判("启动慢"实为 tsx 冷启动 + 端口被残留实例占用)——测试实例用独立端口或先清场。
2026-09-04 · v0.9.4 手机端抽屉按钮:侧栏「只收不放」修复
用户反馈:远程访问手机端只有默认会话视图,侧边栏不显示、也没有任何按钮能把会话抽屉拉出来。
根因(实证自 dsh-client-ui-layout/sidebar bundle):窄屏(<700px)下侧栏显隐由 layout store 的 narrowExpanded 驱动(toggleSidebar 翻转它),原生「打开侧边栏」按钮渲染在侧栏窄轨(rail)里;而 v0.8 的手机适配 CSS 把 [class$="frame"] 栅格压成 0px minmax(0,1fr) 0px——窄轨连同里面唯一的展开按钮一起被裁掉,侧栏从此只收不放。
修复(features/mobile-relay/host.js 注入层,本机与远程网关同层生效):
- 悬浮抽屉按钮
.dsh-mobile-drawer-btn:行为脚本创建,窄屏 + 侧栏收起 + 无设置弹窗时才显示;停泊输入卡上方左下(bottom: calc(var(--dsh-composer-height,152px) + 14px),与任务动画徽标右侧停泊镜像),点击转发原生button[aria-label="打开侧边栏"]toggle。 - 抽屉遮罩
.dsh-mobile-scrim:抽屉展开时显示(z 序:按钮 70 < 遮罩 75 < 侧栏 80 < overlayLayer 90),点击经全局捕获 handler 收起——单一路径,不给同一事件两次 toggle 的机会(遮罩自身不挂 click,避免捕获/冒泡双触发把抽屉又顶开)。 - 显隐同步:MutationObserver(
data-sidebar-collapsed属性翻转即时)+ 1.5s 低速轮询(覆盖路由重渲边角);注入点在<head>后 body 未解析,DOM 创建推迟到 DOMContentLoaded。全局捕获 handler 排除.dsh-mobile-drawer-btn(否则展开动作会被"点外部收起"规则当场撤销)。
验证:host 单测新增 tapIndex 注入断言(抽屉按钮/遮罩/UUID 兜底 + 幂等)全绿;网关单测无回归;注入 JS 独立 node --check;手写最小 DOM 仿真(/tmp/dsh-inspect/behave-sim.mjs)跑通窄屏全交互(收起显按钮→点击展开→外部点击收起→选会话 300ms 自动收起)与宽屏零副作用。
部署注意:注入层在宿主进程(tapIndex 注册于插件加载时)——需重启 dsh web 后手机端刷新生效;client bundle 仅版本号变化。
2026-09-04(续)· 抽屉按钮改走客户端 Overlay:刷新即生效,免重启
转折:用户反馈"刷新了还是一样"。实证:宿主 tapIndex 注入在进程启动时注册(旧进程仍是旧注入,刷新无效),但 client bundle 的 rev 是按内容 hash 每请求实时算的(boot entries 里的 dsh-dock rev 随磁盘 client.js 变化)——客户端改动浏览器刷新即可生效,无需重启 dsh web。
落地:抽屉按钮 + 遮罩 + 行为逻辑复制为 MobileRelayOverlay(features/mobile-relay/view.jsx 的 feature.Overlay,渲染返回 null、effect 里直接操作 document.body 下的 DOM——必须逃出 overlayLayer z90 堆叠上下文才能压住侧栏抽屉 z80);抽屉 CSS 挂进模块 css(dock ensureCss 注入,同样刷新即生效)。
双版互斥:window.__dshDockMobileDrawer 先到先得——宿主注入脚本在 先跑(重启后总是宿主版接管,设 "host");客户端 Overlay 看到旗标自动让位(自己设 "client",HMR 卸载时清理)。与旧版宿主行为脚本(无按钮只有自动收起)并存安全:收起链路同一事件内第二次 collapse() 因 aria-label 已翻转而空转。
注意:本机有个外部 watch 进程会在源文件变更后自动重建 client.js(字节数两次在无手动构建下变化、时间戳紧随编辑)——手动构建后若发现产物被"覆盖重建"属正常现象,以最新产物为准。macOS BSD grep 不支持 BRE 的 \| 交替,多模式用 grep -E。
2026-09-04(续2)· 抽屉把手改贴左边缘 + 趣味游戏浮标上方
用户反馈:把手停在输入卡上方左下,遮挡内容、不直观。
改法(客户端 Overlay 与宿主注入两版同步改):
- 视觉改为贴左边缘的抽屉把手(width 30 / height 48、右圆角、无左边框、玻璃底,与趣味游戏浮标 dgfab 同款边缘吸附语言),图标换通用抽屉符号(汉堡线
M4 7h16M4 12h16M4 17h10)。 place():动态定位在.dgfab正上方(fabTop - btnH - 8);浮标太靠上(top<64,会压顶栏标题)改放它下面;浮标隐藏(dgfab-hide)或不存在时兜底左侧中部 38% 视高。sync 每 1.5s 复测(拖拽浮标最迟一个周期跟上)+ window resize 立即复测。- 顶部左侧方案否决理由:手机端顶栏左上是会话标题,按钮必遮挡。
验证:注入 JS node --check、DOM 仿真全交互绿(仿真器 window 桩补 addEventListener/innerHeight)、host/网关单测绿、线上 3080 bundle(rev 54a2fc8ddd1e)与仓库一致。刷新即生效。
2026-09-09 · v0.9.9 点「任务动画」整个面板界面消失(waitingCount 跨块引用)
用户反馈:点击【任务动画】界面就没有了,而侧栏【功能坞】仍是选中态。
现象 → 机制(实证自 @deepseek-ai/dsh-client-ui-renderer 的 SlotErrorBoundary):宿主把每个插槽条目包在错误边界里,条目内任何渲染抛错都被换成 <div data-slot-error="…"> 空节点("one registrant crashing must not take down siblings")。所以「弹层整块消失 + 侧栏入口仍高亮」= 插槽内组件渲染抛错,而不是面板自己关闭。
根因:features/animation/view.jsx 的 const waitingCount = active.reduce(…) 写在 if (!cfg) {…} else {…} 的 else 块内,但「运行状态」区块的 rows.push(…) 在 块外(if (saveErr) 之后)引用它 → ReferenceError: waitingCount is not defined。该行 v0.9.3(需确认提醒)引入,之后任务动画页一打开就崩(首帧即走那条 push,与 cfg 是否加载无关)。esbuild 的产物线索:声明被重命名为 waitingCount2、引用仍叫 waitingCount——同一绑定必然同步改名,名字不一致就说明源码里它们不是同一个变量。
修复:
- 计数提到函数体作用域(
active/recent旁边),两条渲染路径共用;注释写明踩坑经过。 - 内置视图也包
FeatureBoundary(此前只有外部包视图包):视图 / HomeStat / Chip / Overlay 全覆盖,带key(切功能时重建边界,否则上一个功能的错误态会串到新页面)与label;边界补componentDidCatch把错误console.error出来(原来只吞不报)。
反馈回路(本次的关键):新增 scripts/test-client-views.mjs(npm run test:client,无外部依赖,~130ms)。做法是在 vm 沙箱里加载构建产物 client.js(与浏览器同一份代码),用极简 React 替身(含 hook 按渲染路径缓存、多趟渲染、错误边界语义)逐个启用 8 个内置功能渲染设置页面板。它先以 waitingCount is not defined 变红 → 修复后全绿;另含两条隔离断言(外部视图崩溃、内置视图崩溃都只降级为一行提示)。新功能视图接入后请跑一次。
验证:npm run test:client 全绿(8 视图 + 2 隔离);client.js 重建后 waitingCount 声明与引用同为函数体作用域;无 [DEBUG-] 残留。
部署:纯客户端改动——浏览器刷新即生效(boot entries 的 rev 按 client.js 内容实时计算),无需重启 dsh web。
环境备忘:本机 npx esbuild 找不到包(/tmp/npm-cache 在 Windows 下无效);可用 C:\Users\wzy60\AppData\Local\npm-cache\_npx\beb367dfa21eb3f5\node_modules\.bin 前置到 PATH 后再跑 node scripts/build-client.mjs。
2026-09-09 · 通知从「任务动画」拆出,独立成【任务通知】菜单项
需求:把【任务动画】里的通知部分独立出来做一个左侧菜单,同样支持启动停止。
拆法(features/notify/,宿主 + 客户端两半,功能坞左侧菜单独立项,order: 140,accent 琥珀 #f59e0b):
- 搬走:通知事件(完成 / 异常 / 需确认)、提醒方式(停留时长 / 系统通知 / 提示音 6 音效试听)、钉钉推送、飞书推送,外加一个「运行状态」(等待确认项 + 最近完成列表);提示音库、Toast 组件、
.dkan-toast*/.dkan-row*/.dkan-sound*等样式全部改前缀为dknt-,两个模块样式自洽(各自可单独提取发布)。 - 留在动画:运行动画开关 + 19 种模式 + 桌面伙伴大小 + 运行状态;浮层只做动效(流光/彩带/货运舰/徽标),不再有卡片栈。
关键设计:
- 宿主侧任务追踪抽成共享模块
src/task-track.js,用引用计数共享一份实例:两个功能都开也只订阅一次session/event(每 token 一条,不能订阅两份),两个都关立刻退订。API:acquireTaskTracker(ctx)→{ tracker: { snapshot(now), onFinish(fn), dispose() }, release() }。通知模块用onFinish接任务结束做群机器人推送。 - 配置分段:
DockConfig.animation只剩动画字段,新增notify段。/dsh-dock/animation/*与/dsh-dock/notify/*各写自己的段,互不污染(宿主机测试有断言)。 - 一次性迁移:
migrateNotifyConfig(ctx)在index.js的 settings 注册回调里跑(早于任何面板保存——animation 保存整段写回会丢掉旧通知字段),把老animation段的通知字段搬进notify段并置migratedFromAnimation: true,只写一次。schemastery 不剥离未知键,所以旧字段即使不在 schema 里也读得到。 - 模块启停 = 功能坞开关:notify 不再有第二个总开关(原
notifyEnabled去掉),停用功能即卸载浮层 + 注销宿主路由与推送订阅。沿用 v0.9.5 默认关闭:升级后需在功能坞里手动开启「任务通知」。
验证:npm run test:client 全绿(9 视图 + 2 隔离 + 新增「弹层左侧菜单含任务动画/任务通知」断言);新增 npm run test:host(scripts/test-task-notify-host.mjs)断言共享追踪只有一份订阅且停用后归零、双路由配置隔离、Webhook 校验与测试消息 400、钉钉推送跟随 notifyOnComplete/notifyOnError、迁移字段与幂等。client.js 重建后 dkan-toasts / dkm-miniswitch 在产物里为 0 处。
部署:宿主半部改了(新增路由 + 迁移),必须重启 dsh web 才生效;重启后到功能坞开启「任务通知」,旧的通知配置(含 Webhook)会自动迁移过来。
顺手修掉的提取脚本老毛病(验证「模块可单独提取发布」时发现,scripts/extract-feature.mjs 此前跑任何模块都会中途崩):① 生成的客户端入口硬编码 view.js,.jsx 模块直接 import 失败;② outDir/scripts/ 没创建就写 build-client.mjs → ENOENT;③ 生成 package.json 的 description 引用了脚本里根本不存在的 feature 变量 → ReferenceError。现按 view.jsx/view.js 自动选择、显式 mkdir、description 用 featureId;实测 notify(.jsx)与 balance(.js)都产出完整骨架。同时把 src/task-track.js 一并复制进独立包(notify 宿主依赖它)。
2026-09-09(续)· 用户实测「不得行」:开关只落在浏览器、宿主从未 setup
现象:重启 dsh web 后打开【任务通知】,页面报「宿主进程是旧版本(没有任务通知路由)」,首页卡片也报「通知状态不可用」。
排查(先排除「没重启」):netstat 查到 3080 由 PID 57640 监听、启动于 12:51:05,晚于本次改动(12:48);~/.dsh/settings.yaml 里 notify 段已迁移(migratedFromAnimation: true、钉钉 Webhook 完好)→ 宿主新代码确实在跑、迁移也跑了。但同一份文件里 features 只有 animation: true——宿主侧从未收到 notify 的开关,fNotify.setup() 从没执行,路由自然不存在。面板那句「宿主进程是旧版本」是误报。
根因(客户端开关同步的缺口):toggleFeature 只在点击时推宿主,syncFeatureStateFromHost 启动时只对「本浏览器没表态过」的 id 跟随宿主。于是「本浏览器已表态 true、宿主从未记录过」这种组合永远补不上:点开关时宿主还是旧进程 → POST 404 → 只留在 localStorage;重启后宿主按 defaultEnabled=false 不 setup → 路由 404 → 面板误报。用户当时的手动绕过(开关关掉再打开)让 POST 打到了新宿主,features.notify 才落盘。
修法(src/shared.js):启动同步时若某功能在宿主 persisted 里根本没有记录,就补推一次本浏览器的选择(true/false 都推);宿主显式记过 false 的绝不覆盖——多设备(手机 + 桌面)场景下不能拿本浏览器的旧值去顶别人的新决定。404/405 提示同步改成「宿主进程可能是旧版本,或该功能在宿主侧未启用(重启 dsh web,或把开关关掉再打开)」。
验证:新增用例「开关补推」(三种组合:宿主无记录→补推 true;宿主记过 false→不推;本地 false 且宿主无记录→补推 false),npm run test:client 全绿;线上实测 GET /dsh-dock/features 返回 notify enabled=true、POST /dsh-dock/notify/status 200 且带回迁移后的钉钉 Webhook、POST /dsh-dock/animation/status 200。教训:功能拆成新菜单项时,宿主侧「开关落地」必须与客户端状态对齐,否则表现为「面板好好的、功能全 404」。
2026-09-09(续2)· 开发/发版流程落地:git 修正 + 流程文档 + dsh-dock 开发预设
用户指正:开发阶段提交备注不要写「发版」;不要同步代码到发版库 github;只有用户明确说「发版」时才改包版本、发 npm、写发版备注、推 github 并打 tag。
处置(当轮全部完成):
- 撤销 github:
git push github +ea08c46:main --force-with-lease=refs/heads/main:df14934→ 发版库回到上一个发版提交(开发提交不再出现在 github)。 - 改写本地提交:
功能:通知从任务动画拆出成【任务通知】独立菜单项(各自启停)+ 开关补推修复(正文保留需求/根因/改法/验证),并 force-with-lease 同步到 origin。 - 回退版本号:
package.json与src/client.jsx的DOCK_VERSION都回到0.9.9(重建client.js后产物里也是 0.9.9);CHANGELOG.md顶部段改成## 未发布(下一版)— 开发中;清掉代码注释里的v0.10.0字样(版本号未定,写死会在改号时全变错)。 - 新增
docs/workflow.md:开发/发版流程的唯一真源(远端角色、开发阶段 6 条铁律、每次改动必跑的检查、发版 12 步、模块架构约定、环境坑、预设维护)。 - 新建用户预设
dsh-dock 插件开发(~/.dsh/.agent-presets/dsh-dock/):standard的副本 + persona 里注入开发/发版铁律 + 预设自带技能skills/dsh-dock-release/SKILL.md;standingKeyFor('dsh-dock')挂载校验通过。
教训:跨会话的流程约定必须落到文件里(本文件 + docs/workflow.md + 预设 persona),只存在于会话记忆里的约定,下一次会话就会漂回旧习惯。
2026-09-09(续3)· 运行状态从「任务动画」拆出,独立成【运行状态】菜单
需求:把【任务动画】下面的【运行状态】也独立成一个左侧面板菜单出来。
拆法(features/runstate/,宿主 + 客户端两半,左侧独立菜单项 order: 150,accent 天蓝 #38bdf8):
- 搬走:进行中任务(阶段标签 / 耗时 / 回合 / 步骤 / 工具 / Token)、等待确认横幅 + 每张卡的等待工具与说明、最近完成(最多 10 条,含结束原因与结束时间)。
- 新模块只读:宿主半部只有
POST /dsh-dock/runstate/status(返回共享追踪快照),没有 settings 配置段、不写盘;客户端没有常驻浮层,只在面板打开时轮询(有任务 2s / 空闲 8s / 出错 15s),关掉面板即零开销。首页概要卡片首次进入拉一次。 - 【任务动画】只剩动画(运行动画开关 + 19 种模式 + 桌面伙伴大小),【任务通知】只剩四组通知设置;两页各留一行指路。首页概要各归其位:运行状态报任务(进行中 / 等待确认 / 空闲·最近完成)、动画报当前动效模式与开关、通知报事件 + 提醒方式 + 已开启的推送通道。
为什么两个旧面板的运行状态都删:用户确认「两处都删,只留新菜单」——上一次拆分已经让 animation 与 notify 各有一份运行状态,再加新菜单就是三处重复;现在运行状态只有一个归属。
共享追踪:src/task-track.js 仍按引用计数共享一份实例,三个功能都开也只订阅一次 session/event,全部停用才退订(宿主测试有断言)。
清理:动画页的 TaskRow / END_LABELS / fmtDur / fmtTime 与 dkan-task*、dkan-tag*、dkan-refresh 样式,通知页的 NotifyTaskRow / phaseLabel 与 dknt-task*、dknt-tag*、dknt-refresh、dknt-warn 样式全部删掉;新模块用 dkrs- 前缀自成一套(同样可单独提取发布)。
测试:
npm run test:client:新增运行状态视图用例(10 个内置视图全绿);「左侧菜单」断言改为三个独立菜单项;内置视图错误隔离用例改用运行状态视图当靶子——动画视图已不再做active.reduce,原来那个 Proxy 触发器不再会崩。npm run test:host(脚本由test-task-notify-host.mjs更名test-task-host.mjs,npm 入口不变):断言三个模块共用一份订阅、逐个停用后引用计数正确、/dsh-dock/runstate只有只读 status(config404 且不写 settings)、完成记录三处共享。
部署:宿主新增路由 → 必须重启 dsh web 才生效;重启后在功能坞开启「运行状态」(新功能默认关闭,开关会经 /dsh-dock/features 持久化到宿主)。纯客户端部分刷新页面即可。
教训:拆功能时「重复区块」要一并处置——留着旧区块比多写几行代码更糟,用户看到的是三份一样的东西。
2026-09-09(续4)· 模型设置整页 404:宿主/客户端功能 id 劈叉(models vs modelconfig)
用户反馈:功能坞【模型设置】页报「模型目录拉取失败:模型目录接口 HTTP 404」,重试无效。
反馈回路(本次一分钟定位的关键):插件无条件注册的 GET /dsh-dock/features 就是现成探针——GET /dsh-dock/models 404、features 表里 models enabled:false 且 persisted 没有它;再对比 POST:{id:"modelconfig"} → 404「未知功能」,{id:"models"} → 200 且路由立刻活了。全程 HTTP 探测,零代码插桩。
根因:客户端视图 id modelconfig(features/modelconfig/view.js)≠ 宿主功能 id models(features/modelconfig/host.js)——唯一不一致的模块(其余模块目录名 = 视图 id = 宿主 id 三方一致)。面板开关与 v0.9.7 的补推都 POST 客户端 id → 宿主 404 被客户端 .catch(()=>{}) 静默吞掉 → 宿主半部从不 setup → 路由不存在。v0.9.5 之前宿主默认开启所以从未暴露;用户浏览器 localStorage 还留着旧「已启用」标记,所以页面能进、看到的是 404 报错而非「已停用」提示。404 之所以无声:fetch 对 HTTP 404 是正常 resolve,.catch 根本接不住。
修法:
- 宿主 id 改为
modelconfig(features/modelconfig/host.js);index.js功能清单注释同步。 src/host-core.js新增migrateModelsFeatureId(ctx):开关表旧键features.models→features.modelconfig(幂等早退、新键已存在不覆盖),settings 注册回调里与migrateNotifyConfig并列调用;index.js的persistedFeatureMap()读取侧做兼容别名——异步迁移落盘前,启动开关表与/dsh-dock/features响应第一时间就是新口径。src/shared.js开关补推失败改console.warn(查res.ok;网络错误仍静默——宿主旧版本/不可达时本地开关照常生效的设计不变)。- 回归测试:
test-client-views.mjs新增「双半部 id 一致」用例(7 个双半部模块:host.js 真导入 + view 源码正则抽 id);test-task-host.mjs新增迁移用例(迁一次 / 幂等 / 新键已存在不覆盖)。 - 顺手修构建(上个会话 587 行备忘的根治版):
build-client.mjsnpx 缓存/tmp/npm-cache→os.tmpdir()(Windows 下 npx 解析不到 esbuild),esbuild 来源三级解析(DSH_DOCK_ESBUILD环境变量 →node_modules/esbuild本地安装 → npx 兜底);本次沙箱内 npm 缓存目录不可写(_cacacheEPERM)且 npm install 会因存量 peer 冲突 ERESOLVE,用DSH_DOCK_ESBUILD指到系统 npx 缓存里现成的 esbuild 完成重建。
验证:npm run test:client 全绿(11 用例含新增 id 一致);npm run test:host 全绿(含新迁移用例);线上 3080 应急开启后 GET /dsh-dock/models 200(13KB 目录)。宿主半部改动 → 重启 dsh web 生效:重启后迁移把 features.models:true 换成 features.modelconfig:true,开关状态无缝延续;不重启当前实例也已恢复(应急开启时路由已注册)。
教训:「目录名 = 视图 id = 宿主 id」的功能模块三方一致是开关同步的隐形契约。v0.9.7 的补推修复修的是时机(宿主旧进程时 POST 404),没查id 本身对不对得上——补推把错误 id 原样再推一遍,照样 404。跨半部契约必须用测试锁死,不能靠约定自觉。
2026-09-09(续5)· 发版 v0.10.0;github 发版库改为「每次发版一条提交」
发版:v0.10.0 已发布(两处版本号 → CHANGELOG 改名 → 重建产物 → 全量测试 → 发版提交 2560206 → tag v0.10.0 推两个远端 → npm 上线,registry latest 已返回 0.10.0)。
用户指正:发版同步 github 时不要把开发提交分批推上去——把代码同步成一条提交:主标题写「发版」,body 汇总开发库提交内容(相当于把开发库提交记录总结一次提交)。
改法:
- 新增
scripts/push-github-release.sh:发版时用git commit-tree把 main 整棵树压成一条「发版 vX.Y.Z」提交(父提交 = 上一次发版提交,message 文件复用发版提交那份),快进推到github/main;本地release分支跟踪github/main(勿推 origin)。 docs/workflow.md:§1 远端表补「github main 只有发版提交 / release 分支不推 origin / 两库树同史不同」;§3 第 7-10 步改为 提交+推 origin → 脚本一条同步 github → tag(指向开发库发版提交)→ npm(Git Bash 路径);§5 补「本机bash解析到 WSL,跑scripts/*.sh用"D:\Program Files\Git\bin\bash.exe"」。- 预设
dsh-dock 插件开发三处同步:persona 铁律(github 一条发版提交同步)、skills/dsh-dock-release/SKILL.md(铁律 3 + 发版步骤 7-9 + 环境坑 Git Bash)、preset.yml描述。
github 历史重写(用户拍板「重写为纯发版历史」):git commit-tree 'main^{tree}'(无 -p)造 v0.10.0 根提交 0bc4547(message 汇总本版内容),git push github +refs/heads/release:main --force-with-lease=refs/heads/main:2560206。github main 从此只有发版提交;旧开发提交不再出现在 main 线上(历史版本 tag 仍指向原提交,可访问);根提交树与 origin/main 完全一致(82bc8337)。origin 未动,完整开发史保留。
验证:git fetch github main 后 git log 仅 1 条且树 = main 树;本地 release = github/main;origin/main 仍在 2560206;预设 preset.yml 直解、agent.cordis.yml 把 DSH 专有 !!js 标签替换成普通标量后结构解析通过(plain js-yaml 不认 !!js 属预期);bash -n 语法检查通过。
教训:tag 指向开发库的发版提交,在 github 上它不在 main 线上但树一致——这是约定不是事故:发版库看发版日志,开发库看开发史,npm 主页展示的是发版库。
2026-09-09(续6)· github 历史重写翻车修正:历史记录必须保留,改为「上一发版点 + 每版一条」
用户反馈:按「重写为纯发版历史」执行后,github Commits 页只剩 v0.10.0 一条,0.9.x 及更早的记录全部不可见——「搞错了吧……我0.9.x版本的记录呢?」
根因:把「每次发版一条提交」执行成了「抹掉全部历史、只留一条根提交」。用户要的是今后不再分批推开发提交,不是删掉历史;历史可见性是底线,重写公开仓库历史必须默认保留全部旧记录。
修正:
git commit-tree '2560206^{tree}' -p ea08c46造 v0.10.0 汇总提交ef60b3f(树 = v0.10.0 发布树82bc8337,父提交 = v0.9.9 发版提交),--force-with-lease推回 github main。- 效果:github main 恢复 95 条——0.9.x 及更早完整历史全部可见;v0.10.0 循环的 4 条开发提交收进一条「发版 v0.10.0」汇总提交(符合新规矩);此后每次发版在
ef60b3f之上快进追加一条(scripts/push-github-release.sh逻辑不变)。 - tag v0.10.0 仍指向原发版提交 2560206(其开发史经 tag 仍可达);本地
release分支跟随到ef60b3f。
流程文档同步:workflow.md §1「github 的 main 只有发版提交」改为「github 不收分批的开发提交——既有历史原样保留、不重写不删,每次发版只多一条汇总提交」。
教训:「总结成一条」指的是增量(本次发版的开发提交收成一条),不是替换(把历史压掉)。涉及改写远端历史的操作,保留多少历史必须与用户逐字对齐;写进文档的规矩要写成「不删历史」的明文,不能只写「只推一条」。
2026-09-10 · 「模型列表加载慢」定位结论:宿主空转,与 dsh-dock 无关 + v0.10.1 文档/截图
现象
升级到 dsh 0.1.5-rc.1 后,原生「设置 → 模型」列表「很慢很慢甚至出不来」;随后侧边栏 / 文件面板也出现 client api: workspaceFiles/list failed: Failed to fetch。
排查与根因(不是本插件的兼容问题)
- 插件宿主接口全是毫秒级:
/dsh-dock/models10ms、/dsh-dock/features3ms;RPC 侧llm/listProviders12ms、llm/listConfigurableProviders26ms、settings/describe8ms。 - 宿主进程在空转:长时间运行的
dsh web持续吃满约一整颗核(20 秒耗 20+ CPU 秒)。给该进程挂 Node inspector 采样,热点全在 dsh 自带包:existsSync(文件系统探测)→@deepseek-ai/dsh-agent-presets的packageInstalled → rowResolves → unresolvableRows;异步栈为@deepseek-ai/dsh-commands的CommandRuntime.layers → notifyChange叠加@deepseek-ai/cordis的Fiber._reload。 - 自我维持的反馈回路:
CommandRuntime构造期注册命令 → 每次注册 emitcommands/change(宿主侧notifyChange实测 24 次/秒,cordisPresetTreefiber_reload约 115 次/秒)→ 浏览器端官方dsh-client-ui-commands收到commands/change就invalidateAll()重拉/api/commands/list(实测约 35 次/秒)→ 重拉又 resolve agent / resume session → 再次 emit,闭环。浏览器同源连接池被这个高频请求占满,其它请求(workspaceFiles/list等)便排队或Failed to fetch——这才是「模型列表出不来、侧边栏失败」的直接原因。 - 与 dsh-dock 无关的证据:a) 停用插件全部功能(宿主侧 + 浏览器 localStorage + chips)后循环照旧(6 秒 198 次);b) 把
~/.dsh/.agent-presets/dsh-dock目录移走后宿主照样满载;c) 新起 dsh 实例(带 / 不带本插件)均完全空闲,循环不出现;d) 循环栈里没有一帧本插件代码。 - 兼容性核对:插件用到的宿主 API 在 rc.1 全部存在——
llm.listConfigurableProviders/resolveModelInfo/prepareCall/registerAdapter、BlockAssembler(@deepseek-ai/dsh-llm)、credentialRef(@deepseek-ai/dsh-credentials)。
处置
重启 dsh web 即清空该进程态(所有新实例均未复现);或先关掉全部 dsh 页面标签再重开——实测无浏览器连接后宿主自身就回到空闲(0.09 CPU 秒/10 秒)。本次未改任何插件代码:症状是宿主进程态问题,不属于本仓库可修范围。
顺带发现(待跟进,非本版改动)
~/.dsh/.agent-presets/dsh-dock/agent.cordis.yml是standard预设的旧副本:比 rc.1 的 shippedstandard少一行@deepseek-ai/dsh-tool-present(rc.1 新增)。仍可解析、不是本次循环的原因,但已与新版脱节;后续可用新版standard重做该副本并重新贴回 persona 铁律。
排查方法备忘(踩过的坑)
git tag | tail会骗人:tag 是字典序排序,v0.10.0排在v0.9.x前面,用tail -10/tail -6看「最新 tag」会把v0.10.0漏掉、误判成「tag 缺失」。核对版本 tag 一律用git tag | sort -V | tail,或按具体版本git ls-remote --tags <remote> | grep v0.10。本次一度据此误报「v0.10.0 tag 缺失」,实际本地 /origin/github三处都有(annotated tag6b0af30→ 提交2560206)。tab.screenshot({ clip })在本环境会平铺错位:截指定区域时会得到重复平铺的错图,只能整窗截图再视需要裁(见下)。
本次发布内容(v0.10.1)
- 截图全部重拍(10 张,统一 1280×720):新增模型余额 / 任务通知 / 运行状态 / 趣味游戏 / 设置→功能坞;用量记录 / 模型设置 / 任务动画 / 会话区小控件按 v0.10.0 拆分后的界面更新。任务通知页的钉钉 / 飞书 Webhook 已脱敏——这一步是必须的:仓库内文本文件(README、
view.jsx的 placeholder、测试脚本)本就只用…/x/test占位、没有真实密钥,截图若不脱敏就会成为首次把真实群机器人地址写进公开仓库。 - README:功能一览补【趣味游戏】;「手机接力」更正为菜单项实际名称【远程访问】;「各功能使用」下补齐截图(文件内「界面 / 安装 / 使用方法」整段有重复副本,两处同步)。
- 版本号两处对齐
0.10.1(package.json+src/client.jsx的DOCK_VERSION),npm run build:client重建client.js(页脚版本号随产物刷新)。
截图做法(可复用)
本机 dsh 开了认证(/api 与根页都需要 dsh-auth-* Cookie)。截图时用一个注入 Cookie 的反向代理(对本机 127.0.0.1:3080 上游,用 ~/.dsh/.credentials.yaml 里 client-connection/browser-session 的 secret 现签一张 Cookie)起在本地端口,浏览器打开代理地址即可驱动真实 UI;WebSocket upgrade 也要一并转发。注意 tab.screenshot({ clip }) 在本环境会出现平铺错位,一律用整窗截图(1280×720)。
2026-09-10(续)· 「切模型卡住 → 模型操作失败」根因:dsh-dock agent 预设未跟上 dsh 0.1.5-rc.1 的 schema 漂移
现象
历史会话(原本用已删除的模型)切到别的模型时卡住、切不动,最后弹:
模型操作失败: gateway/internal: resume failed for session "session-f1f3959e-…":
RemoteError: agent-presets: preset "dsh-dock" failed to mount: failed to apply loader entry
persona (@deepseek-ai/dsh-persona): invalid config: - $.prefix missing required value
(at prefix) (C:\Users\wzy60\.dsh\.agent-presets\dsh-dock\agent.cordis.yml)
根因
dsh-dock agent 预设(~/.dsh/.agent-presets/dsh-dock/agent.cordis.yml)是 shipped standard
在 2026-09-09 的副本。dsh 升到 0.1.5-rc.1 后 @deepseek-ai/dsh-persona 的 Config schema 改了:
- 旧:
config.text: |(一整段,自带{{model}}/{{cwd}}占位) - 新:
config.prefix(z.string().required())+config.suffix,text字段已删除
预设里那行还是 config.text → prefix 缺失 → 预设挂载失败。因为预设是 standing mount,
所有绑定该预设的会话都无法 resume(切模型、打开历史会话都会走 resume → 撞上挂载失败),
所以表现为「卡住 + 模型操作失败」的通用报错,而不是「persona 配置错」。
注意:这跟 dsh-dock 插件无关,是 agent 预设在 DSH_HOME 下的本地配置漂移;但预设名字叫
dsh-dock,报错里写的是preset "dsh-dock",很容易误以为是插件坏了。
改法
把预设与新版 standard 重新对齐(diff 忽略注释空行后只剩 persona 正文一处差异):
persona.config:text: |→suffix: Your working directory is {{cwd}}.+prefix: |-+ 原有正文(首行You are a coding agent powered by the {{model}} model., 其余 dsh-dock 铁律原样保留)。- 补上 rc.1 新增的末尾行
- id: present/name: '@deepseek-ai/dsh-tool-present'。
验证(关键:只查 roster 不够)
- 单独用
Config(config)校验 persona 行:✅prefix1495 字节、suffix正确(旧行报$.prefix missing required value)。 agentPresets/list:dsh-dock ✅ ok——但这一步在修复前也是 ok,因为 roster 只校验 YAML 形状、不校验 config schema,真正失败的是 mount。以后不能只靠它。- 真机 mount(修复前失败、修复后通过):
session/create带agentPreset: "dsh-dock"→ ✅ 返回agentPreset=dsh-dock;- 对报错里那个会话
session-f1f3959e-4d6b-49ed-928c-dc678723bd45执行session/selectModel(deepseek-official / deepseek-flash)→ ✅ 切模型成功。 - 注意
/api的 RPC 参数形状:会话类接口要包一层args.request(如{"args":{"request":{…}}}),selectModel用错形状会报missing "request"。
沉淀
docs/workflow.md§6 新增「升级核对(dsh 换版本后必做)」:diff 对齐 + 两处已知漂移表 + 真机 mount 验证命令。- 配套技能
dsh-dock-release/SKILL.md新增「dsh 升级后必做:agent 预设对齐」一节(含 0.1.5-rc.1 踩坑记录),并顺手修正了环境坑(esbuild 用DSH_DOCK_ESBUILD显式指定; Git Bash 参数要 Windows 路径;git tag | tail的字典序坑)。 - 教训:凡是「预设 / 插件 schema 跨版本漂移」,症状往往是下游操作卡死 + 笼统报错
(resume failed → 模型操作失败),根因却在配置行;排查时先看报错里的完整原文——
这次原文把文件名与字段名(
persona、$.prefix)都点出来了。
2026-09-10(续2)· README 截图引用整理
- README 的截图引用移除 1 张不再适用的示例图(保留 9 张,各自对应功能页);
screenshots/目录 与package.json的files保持整洁一致。 - 顺带修订发版提交的信息措辞:公开历史里的提交信息只描述改动内容,不展开事件经过/排查细节, 避免在仓库历史中留下不必要的注释;细节留在私有记录里。
2026-09-10(续3)· 用量记录支持自定义模型单价(费用按自填单价计费)
需求
费用"不准确"——因为单价是抓来的。用户要求:支持配置每个模型的单价、持久保存,并用保存的单价 算费用;并征询"单价在哪里设置比较好"。
根因(两处,改动前先查清)
- 原有的 settings 覆盖机制从未生效。
features/tokenlog/host.js头部与loadPricingConfig()读的是dsh-dock-tokenlog命名空间,但该 namespace 从未注册。 查 dsh 源码packages/settings/settings/src/index.ts确认:get(ns)只返回this.registrations.get(ns)?.resolved,write()在registration === undefined时直接throw new Error('settings namespace "..." is not registered')——未注册命名空间读写都不通。 所以文档里"改 settings.yaml 即可覆盖单价/汇率"的说法与代码行为不符。 - 抓取价优先级最高。原
resolvePricing()顺序是「官网抓取 → settings 覆盖+内置 → 兜底」, 即便用户能配上,也会被抓取价压掉。这正是"费用不准又改不动"的直接原因。
改法
- 配置位置:随插件一起注册的
dsh-dock命名空间下新增tokenlog段 (src/host-core.js的DockConfig,schema 化:usdCnyRate/fetchOfficial/pricingUrl/pricingFetchIntervalHours/pricing[]/fallback)。选它而非新建命名空间,是为了复用 既有的「功能配置写在自己段里」约定(notify/animation/visionProxy 同款),web 端只跟/dsh-dock/tokenlog/*打交道。 - 优先级反转:
resolvePricing()改为 自定义单价 > 官网同步价 > 内置价 > 兜底价,并返回 来源标签(custom/official/builtin/fallback)。 - 改价即时生效:费用不再只在采集时算死,而是在
query/export时经withCost()按当前 单价重算(浅拷贝记录),因此改价无需重扫历史。KPI 卡/分组/明细/CSV 一并采用新价。 - 面板:用量记录页筛选行加「单价设置」按钮,展开
PricingEditor——按模型逐条配单价 (支持峰谷时段)、兜底单价、汇率、官网同步开关;模型名可用已用/已配置模型下拉,也可手输子串。 明细中走兜底价的金额标「兜底」,详情弹窗显示「计价来源」。 - 路由:新增
pricing(读)与setpricing(写,校验后落盘);setpricing校验失败按statusCode返回 4xx(路由 catch 块改为按e.statusCode定状态码)。 - 安全:
pricingUrl出站前assertAllowedFetchUrl()校验——仅 http/https,拒绝 localhost/.local/.internal、环回、10./172.16-31./192.168./169.254./组播保留、IPv6 环回与 链路本地(满足 Mimosa 生成前安全约束)。 - 测试:新增
scripts/test-tokenlog-host.mjs(接入npm run test:host),覆盖优先级、 即时重算、参数校验 4xx 不落盘、pricingUrl 出站拦截。
验证
npm run build:client→npm run test:client全绿(10 个功能视图 + 错误隔离 + 菜单 + id 一致性)。npm run test:host全绿(task/tokenlog 两个文件)。- 测试桩 fetch 的 HTML 需 >500 字节,否则被
fetchOfficialPricing()的 "SPA shell too small" 早退、拿不到官网价——首次跑就踩到,fixture 里补了填充文本。 - 顺带发现并修掉一个测试假通过:
scripts/test-client-views.mjs的 fetch 桩从未返回/dsh-dock/tokenlog/query,TokenLogView渲染时读data.counts.matching直接抛错、被FeatureBoundary降级成一行提示;而该用例的 marker 是「用量」,恰好在功能卡片的描述文字 里命中,于是「✓ 用量记录渲染正常」长期是假的(视图其实一次都没真正渲染出来)。修法:补上 query 桩数据,marker 改成只有真实视图才有的「调用明细」,并追加明细表标题/单价入口按钮/金额 等断言。新增的「单价设置」面板因默认收起、测试桩无点击模拟,其内部渲染是用临时把默认改为 展开跑一遍确认的(表格/峰谷列/来源标注/内置价列表/KPI 金额均正常渲染),确认后已改回收起。
沉淀
docs/workflow.md§2 检查表补注:test:host现由两个用例文件组成;新增宿主功能照此追加并串进package.json。- 教训:读未注册的 settings 命名空间永远是 undefined。写"可被 settings 覆盖"的配置前,
先确认该 namespace 有
settings.register(),否则文档与行为会长期背离。 - 教训:marker 断言要选"只有真实渲染才出现"的字符串。用功能名/描述里的词做 marker,会在 视图抛错降级成提示时照样命中,把假通过藏起来;发现可疑的"渲染正常"时,先把渲染出的 HTML 打出来看一眼。
2026-09-10(续4)· 单价设置改为独立子弹窗 + 分时段价支持多段
需求
用户看到首版「单价设置」后提了三点:① 不要内嵌在用量页里,改成独立子弹窗;② 子弹窗要能 最大化、拖动、缩放;③ 峰谷时段要能多段(举例 DeepSeek——它有高峰/优惠多个时段, 单段表达不了)。
改法
- 独立子弹窗:
PriceModal覆盖层(position:fixed,z-index:2100,高于功能坞弹层自身的 200),带标题栏拖动、右下角缩放手柄、最大化/还原、Esc 关闭、点遮罩关闭。拖动/缩放用 window 级 pointermove 监听,指针移出元素也不丢;最大化态禁用缩放手柄。原来内嵌的PricingEditor保留, 作为子弹窗内容(embedded分支)。 - 分时段多段:数据模型从单段
peak:{start,end,...}改为peaks:[{start,end,input,output, cacheRead,cacheWrite}, ...](src/host-core.js的 schema 与features/tokenlog/host.js)。- 匹配:
pickPeakSegment()按顺序命中第一段;start<end普通区间、start>end跨零点 (如23~7)、start===end全天;用小数小时(getHours()+getMinutes()/60)以便支持8.5 = 08:30这类半点边界(DeepSeek 刊例就是 08:30 分界)。命中哪段用哪段,段外回落基准价。 - 兼容:
normalizePricing()读旧数据时把peak归一为单元素peaks,老配置不用手工迁移。 - 校验:每段 start/end ∈ [0,24]、各价 ≥ 0,非法 4xx 不落盘。
- 匹配:
- UI:每个模型一张卡片(
dtok-prow),卡片内「基准价」+ 可增删的多个「时段」行(dtok-pseg), 时段行显示0~8.5(跨零点)之类的实时摘要;内置价表的分时段列改成列出全部段。 - 可测性:
TokenLogView支持params.openPricing深链直接展开子弹窗(DockPanel也改为 透传props.params),使客户端渲染回归测试能覆盖到子弹窗内容,而不必模拟点击。
验证
npm run test:host:新增多段用例——03:00/08:15 命中优惠段、08:45 段外回落基准、10:00 命中 高价段;跨零点段覆盖凌晨 02:00 与深夜 23:30、段外回落;旧单段peak读取归一;非法时段 4xx 不落盘。全绿。npm run test:client:tokenlog 用例带params.openPricing,断言「分时段价」「添加时段」 「基准输入」「跨零点」等子弹窗内容;另加用例 8 用带document桩的沙箱断言子弹窗确实createPortal到document.body(防祖先backdrop-filter/transform 改变 fixed 包含块)。全绿。- 真机浏览器验证(拖拽/缩放/最大化是 CSS 与指针行为,单元测试的 React 替身覆盖不到):本机
dsh 有认证,起了一个注入 `dsh-auth-*$ \text{Cookie} 的反向代理(临时脚本放仓库外,验完即删),用浏览器
实际打开用量记录 →「单价设置」,确认:
- 覆盖层铺满视口(1280 \times 720,\text{x}/\text{y}=0),弹窗 980 \times 688 居中;最大化 → 1264 \times 704(贴边 \text{margin} 8), 还原回 980 \times 688;
- 拖动标题栏 位移 (\text{x}+200, \text{y}+100);右下角手柄缩放 980 \times 688 → 840 \times 598;
- 分时段:点两次「+ 添加时段」得到两段行,填入 $23
714`,行内摘要实时显示 「23与97(跨零点)」** 与 **「914」;四个时段价输入框带 输入/输出/缓存命中/缓存写入 占位提示。 - 说明:没有点「保存单价」——当时跑着的宿主进程是上一个修订版(
/dsh-dock/tokenlog/pricing还没有peaks字段),保存会被旧宿主丢掉分时段;真机验证只覆盖界面行为,数据往返由宿主测试覆盖。
- 过程中发现并修掉一个自身引入的回归:
DockPanel原本未声明props形参,透传参数后全部功能 视图都报props is not defined(测试立刻抓到)——补上props形参即恢复。
2026-09-14 · v0.11.2:用量 chip 点击即出当前会话结果(挂载查询竞态)+ chip 宽度放宽
现象
- 用户反馈:从会话输入区「⛁ 用量」chip 打开用量记录,面板显示的是全量/旧条件数据 (会话下拉框已选中当前会话、KPI 却是 1.2 万条全量),必须再点一次「查询」才按会话出结果。
- 用户反馈:配置单价后费用段「· ¥0.11」出现,用量/余额两个 chip 的数值被省略号截尾 (「¥0....」「余额 3014....」),小数位看不到。
根因
- 竞态:
TokenLogView挂载时两条初始查询并发——挂载 effect 先scan(慢)再按 上次暂存条件查(不含本次带入的会话);navSession effect 立即按params.sessionId查(快)。 慢链路的结果最后到达覆盖快链路的会话结果。截图佐证:下拉框已是当前会话、统计仍是全量。 - 截断:chip 行宽度上限 189px(≈25cqw)是 v0.10.x 实测收窄的值,当时「4~6M 用量 + 余额」 恰好放得下;配置单价后每 chip 多出「· ¥0.xx」一段,合计超出约两个字符。两个 chip 的文本 精度本来就够(费用/余额均 2 位小数),纯粹是宽度上限截断。
改法
features/tokenlog/view.jsx:sessionId 初值直接用 navSession(优先于暂存条件);挂载查询里navSession覆盖暂存的会话条件——初始只跑这一条查询;navSession effect 改为只处理 「面板已打开时再次点击」,以navAt(chip 点击时间戳)判同跳过首次。chip 的openPanel带navAt: Date.now(),同一会话再点也重查。src/client.jsx:.dockchip-row上限 189px/25cqw → 216px/28cqw,注释里记录新测算 (两 chip 合计约 204px)与回退方案(若再挤换行,优先压用量 chip 的 ⛁ 前缀,别再收上限)。
验证
npm run build:client+npm run test:client全绿(10 个视图渲染 + 错误隔离/portal 等检查)。- 真机验证(dsh web 3080 link 模式,注入 Cookie 反代 + 内置浏览器):
- 点「会话用量」chip → 面板直接打开用量记录页,秒出「11 / 12354 条」(当前会话 11 条),
会话下拉框选中
session-6fd8c951-…,无需点「查询」; - chip 完整显示「⛁ 525.5K · ¥0.11」「余额 3008.89」,模型选择器与发送键同行不换行。
- 点「会话用量」chip → 面板直接打开用量记录页,秒出「11 / 12354 条」(当前会话 11 条),
会话下拉框选中
- 顺手修正截图记忆里的反代做法:转发请求的
Host头必须保持代理 authority(3990), 不能改写成上游 3080——上游按 Host 校验 Cookie,改写必 401;dsh 页面 chip 的 Playwright click 会因 React 周期刷新超时,改用 evaluate 原生el.click()。
2026-09-15 · 仓库文档卫生:README 去重 + 行尾统一 LF + 修正过时的远程访问口径
现象
README.md里「界面 / 安装 / 使用方法」整段存在两份近似副本(L38–194 与 L195–351,其中 133 行完全相同), 正文等于讲两遍;两份已互相漂移,docs/workflow.md甚至为此留了一条「两处都要改」的操作注记。git status长期显示 49 个文件「已修改」,git diff合计 16422 增 / 16422 删,看着像全仓库重写。- 功能一览表【远程访问】那一行仍以「局域网电脑直连:另起一个 0.0.0.0 绑定的完整 DSH 子实例」为卖点。
根因
- 重复副本:某次编辑是「追加而不是替换」,此后两份各自被改过几次。差异集中在「远程访问」段 (旧副本是 4 步骤版,新副本是登录网关版)与结尾(旧副本重复「会话区随身小控件」,新副本是「常见问题」)。
- 假 diff:仓库位于
/mnt/f(Windows 盘),Windows 侧编辑器把工作区文件全写成 CRLF,而仓库既没有.gitattributes、core.autocrlf也未配置,于是行尾差异全部暴露成内容改动。git diff --ignore-cr-at-eol为空可证。 - 过时口径:远程访问经历过「主实例+子实例」→「服务器模式 0.0.0.0」→「账号登录网关」两次重构,
前两者均已删除;表格行当年只改了名字,括号里的旧卖点没跟着删,与同文件「各功能使用」段及
features/mobile-relay/(页面明写「不存在免登录的直连路径」)相矛盾。
改法
README.md:保留较新的一份副本(含「常见问题」),删掉旧副本。删之前先把旧副本独有的 3 处信息并回: 异地组网(Tailscale/ZeroTier 用组网 IP 访问同一入口)、登录会话 Cookie 7 天滑动过期、 网关与主服务是同一台机器上的两个监听、不能共用一个端口;另给「心跳监视 / 主题信息」补空行。README.md功能一览表【远程访问】:去掉 0.0.0.0 子实例直连的说法,改为「主实例保持仅监听127.0.0.1, 结构上不存在绕开登录的直连路径」。- 新增
.gitattributes:* text=auto eol=lf+ 二进制资源binary;git checkout -- .把 49 个文件还原成 LF。 docs/workflow.md§2.6:删除「README 存在重复副本、两处都要改」的注记,改为「单一副本,改一处即可」。
验证
- 清理前
git diff --ignore-cr-at-eol --numstat | wc -l= 0 → 确认 49 个文件确为纯行尾差异,还原后git status干净。 - 去重后做差集比对(原文件 vs 新文件的行集合):只多出 8 行「消失」行,逐条核对均为被新副本改写、
等价覆盖或仅缩进/编号变化的旧行,无信息丢失;
## 界面/## 安装各只剩 1 处。 .gitattributes落地后git ls-files --eol全库均为i/lf w/lf,无需重新规范化;git add -A -n只列出本次的目标文件,没有冒出额外文件。- 仅文档与仓库元文件改动,未触碰
features/、src/、index.js:按docs/workflow.md的改动-测试对照表, 本次不需要跑build:client/test:client/test:host。
部署
- 纯文档与仓库卫生改动,插件代码零改动、版本号未动(仍为 0.11.2),已发布的 npm 包不受影响
(
docs/、.gitattributes都不在package.json的files白名单内);用户侧无需任何操作。 - 按仓库约定只推
origin(开发库),github远端仍只在发版时同步。
2026-09-16 · 发版 v0.11.3 前的全量测试:分时价用例第二次时间耦合(21:00 后必挂)
现象
发版前跑 npm run test:host(当时 22:53),test-tokenlog-host.mjs 挂在内置价断言:
AssertionError [ERR_ASSERTION]: 内置价 1.5/4.5 生效
at scripts/test-tokenlog-host.mjs:196
根因
内置表 deepseek-v4-flash / deepseek-v4 带 914 时高峰分时价,而 14 峰段,
峰价 3/9 取代基准价 1.5/4.5,断言必失败。用例 6(export 结构化明细)用的是同一个时间戳,只是没断言金额。resolvePricing() 是按调用时刻的
本地小时数选段(pickPeakSegment)。v0.11.1 为「上午跑挂」改过一次:把时间戳从 Date.now() 换成
「倒退 12 小时」——那只是把失败窗口从上午挪到了晚上:21:00 之后 now - 12h 正好落进 9
改法
- 文件头新增
atHour(h, m)=new Date(2026, 0, 15, h, m, 0, 0).getTime():写死「本地时刻」, 不随运行时刻漂; - 用例 1 / 用例 6 的时间戳由
Date.now() - 12h改为atHour(20)——20:00 在所有内置峰段 (914)与本文件自定义峰段(00:008:30、914、237)之外。
验证
npm run test:host全绿(tokenlog host: ok …,共两个用例文件);- 额外用
TZ=UTC与TZ=America/New_York各跑一遍同样全绿:new Date(y, m, d, 20, 0)按本地时间解释, 写死的是「本地 20:00」,与运行时刻、时区均无关。
部署
- 仅测试文件改动,插件运行代码零改动(
features/、src/、index.js未动),用户侧无需操作。 - 顺带把本机 agent 记忆目录
.workbuddy/加入.gitignore(与.mimosa/同类,属本机产物,不进包)。
2026-09-16(续)· 发版 v0.11.3:查询等待动画 + CSV 中文表头 + 开发机工具链
本版内容(自上版 v0.11.2 以来在开发库累积的全部提交)
- 用量记录点「查询」有等待动画(03fa1a5):状态行
⟳ 加载中...、按钮「查询中…」禁用、 数据区 sticky banner 轮流蹦跳换文案(>5 秒转安抚语气)、旧数据压暗不可点、结果到位闪 「✓ 数据已更新」;5 秒静默轮询不置 loading;遵守prefers-reduced-motion。 - 用量记录 CSV 导出改中文表头(5d64756):export 路由改回结构化明细,前端按界面「调用明细」表 同列序同取值生成 14 列 CSV,末尾追加合计行、前置 UTF-8 BOM;旧宿主进程自动回退原形式。
- 宿主删掉任务开始/结束的例行终端日志(b3b51ad):运行状态以「运行状态」页为准,终端只留报错。
- 文档与仓库卫生(e3b9f5f、0f43952):README 去重(397 → 241 行)+ 修正过时的远程访问口径 +
.gitattributes统一 LF。 - 开发机 link 安装 + 改码自动刷新(15b34e1、c3e5377、158120e、31dde8e):
dev-link-deps.mjs/watch-client.mjs/npm run dev:link/npm run dev:client+ workflow 4b 节。 - esbuild 解析兼容 WSL / 本地命中(eea8e2a、a1fe734):
.exe直执 +wslpath路径桥、本地命中改经 node 跑 JS 入口、win32 走 shell、失败包装报错信息。 - 修复(97270a5):
test-tokenlog-host.mjs分时价用例写死本地 20:00,21:00 后跑不再必挂(上一节)。
发版动作(按 docs/workflow.md §3)
- 版本号两处改 0.11.3(
package.json+src/client.jsx的DOCK_VERSION);CHANGELOG.md的「未发布(下一版)— 开发中」整段改名为## v0.11.3 — 2026-09-16。 npm run build:client重建client.js;npm run test:client(10 视图 + 6 项隔离/一致性断言)与npm run test:host(task/animation/notify/runstate + tokenlog)全绿,无跳过。- 复查 README /
package.jsondescription:无写死的版本号需改;功能一览表未受影响。 - 发版提交(开发库)→ 推
origin;tagv0.11.3推origin与github。 - 发版库
github:scripts/push-github-release.sh把 main 整棵树压成一条「发版 v0.11.3」提交 (父提交 = 上一发版提交48d2293f= v0.11.2),快进推github/main——不重写、不删既有历史。 ./scripts/publish.sh发布 npmdsh-dock@0.11.3(npm pack --dry-run预览 + 登录态检查 +npm publish)。- 本地发版库检出对齐:
/Users/weiyi/develop/github/wycto/dsh-dock的 main 长期停在 v0.9.0, 且自 v0.10.0 那次「github 历史重写再修正」起就与远端分叉(本地 10 条 / 远端 101 条互不包含)。 本次不 force、不丢历史:先把旧 main 存成backup/main-pre-v0.11.3,再fetch+reset --hard到origin/main,使其与github/main完全一致。