Dizzy-DSH 开发方案
August 24, 2026 · View on GitHub
本文档是 Dizzy-DSH 的完整开发指南:架构、平面规则、双半区开发流程、 关键技术机制、常见坑与验证手段。所有结论均来自本仓库实际踩坑验证, 而非纸面推测。
1. 架构总览
Dizzy-DSH 是一个 DSH bundle 层插件合集仓库:"克隆即装",无需 npm 发布, 重启后依然生效。每个功能 = 一个独立子包插件(与 third-party 收录的插件 同构):独立 host 插件、独立 client bundle、独立挂载/卸载,互不引用; 主包只是聚合根(依赖声明 + patch 层)。
用户机器
├── DSH 安装(dsh CLI / web 服务)
├── profile: ~/.dsh/profiles/web/
│ ├── package.json # dependencies 含 dizzy-dsh(file: 仓库路径)
│ │ # bundles 列表含 dizzy-dsh(自动加入)
│ └── node_modules/
│ ├── dizzy-dsh → Junction → store 快照(file: 安装时生成)
│ ├── dizzy-dsh-balance / dizzy-dsh-usage-card /
│ │ dizzy-dsh-agent-instructions / dizzy-dsh-kimi-webbridge
│ └── dsh-better-sidebar / dsh-subscription-auth / dsh-gui-customization / @anionex/...
└── 仓库(本目录)
├── package.json # 聚合根:main + dsh.bundle + file: 依赖(plugins/* + 第三方)
├── cordis.patch.yml # bundle 插件层(insert 条目,全部插件在此挂载)
├── index.js # 聚合根空插件(无功能)
├── plugins/ # 自有插件合集
│ ├── balance/ # dizzy-dsh-balance:package.json + index.js(Host) + client.js(UI)
│ ├── usage-card/ # dizzy-dsh-usage-card:同上
│ ├── agent-instructions/ # dizzy-dsh-agent-instructions:package.json + index.js + prompts/
│ └── kimi-webbridge/ # dizzy-dsh-kimi-webbridge:Host 工具集
└── third-party/ # 收录的第三方插件(git subtree 跟随上游)
├── dsh-subscription-auth/ # OAuth 订阅登录(上游快照 + 本地补丁)
├── dsh-gui-customization/ # 界面设定/时装工坊(上游插件包子目录,sparse 覆盖)
└── … # genui / notification / vision-toolkit / anchored-standard(subtree)
安装与生命周期
# 一条命令安装全部插件(自有子包 + 收录的第三方插件,见 §7.5)
dsh plugin --profile web add file:<仓库绝对路径>
dsh plugin转发给 pnpm 在 profile 目录执行安装。必须用file:而非link::link:协议下 pnpm 不解析目标包的dependencies(目标目录 自管依赖,已实测:link 安装的包其依赖一律不进 lockfile/node_modules), 而file:会递归安装完整依赖树(registry 依赖、file: 依赖全部解析, 经 profile 的nodeLinker: hoisted提升到顶层 node_modules)- 主插件
package.json的dependencies声明自有子包 (file:./plugins/*)与收录的第三方插件(dsh-better-sidebar@0.15.0走 registry、其余含dsh-subscription-auth走仓库快照)——file:安装时自动全部带上 - 安装成功后 reconcile(
plugin-9h8shc4d.js的reconcilePlugins): 遍历 profile 顶层dependencies,凡是解析到的包声明了dsh.bundle.patch就自动加入dsh.profile.bundles层列表(顶层只会有 主插件一个,子包与第三方由主插件 patch 挂载,见第 4 步; 子包不带dsh.bundle.patch,add 时会提示 "plain dependency",属预期) - 下次启动 dsh web 时,loader 读取主插件 bundle 的
cordis.patch.yml, 其中的- insert:条目列出全部插件行(自有子包 + 收录的第三方), entry 的 name 是包名,从 profile 顶层 node_modules 按包加载:- Host 半区:
import('<包名>')→ 包main(index.js)→ 挂载插件 - Client 半区:
client-modules按 entry 的包名扫描dsh.client声明 +exports["./client"]→ 把该包的 client bundle 注入浏览器
- Host 半区:
- 首次安装若遇
ERR_PNPM_IGNORED_BUILDS(node-pty/protobufjs 构建脚本): pnpm 默认禁止并以非零码退出导致 reconcile 不执行。在 profile 的pnpm-workspace.yaml的allowBuilds里将两者设为true后重跑 (pnpm v11 会自动生成占位node-pty: set this to true or false)
卸载:dsh plugin --profile web remove dizzy-dsh(子包与第三方随依赖一起移除)
更新:cd <仓库> && git pull 后强制同步 file: 快照 —— pnpm 对
file: 依赖只检测 package.json 是否变化,仓库内其他文件(如
plugins/agent-instructions/prompts/agent-instructions.md)改了不会同步到
node_modules 的安装副本(已实测复现:改 prompt 文件后 dsh plugin add
报 "Already up to date",system 里仍是旧内容)。强制同步(主包与每个子包
都要删):
Remove-Item ~/.dsh/profiles/web/node_modules/dizzy-dsh -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/dizzy-dsh-balance -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/dizzy-dsh-usage-card -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/dizzy-dsh-agent-instructions -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/dizzy-dsh-kimi-webbridge -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/dsh-subscription-auth -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/dsh-gui-customization -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/dsh-plugin-memory-tencentdb -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/@tencentdb-agent-memory -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/@anionex -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/@dsh-external -Recurse -Force
Remove-Item ~/.dsh/profiles/web/node_modules/@omdsh-dev -Recurse -Force
dsh plugin --profile web add file:<仓库绝对路径>
注入内容动态读取,同步后下一轮对话即生效;改了插件代码才需重启
2. 平面规则(决定能力放哪)
| 能力 | 位置 | 说明 |
|---|---|---|
| 模型工具、定时任务、数据服务、HTTP 路由 | 对应子包 Host 半区(bundle 层) | 进程级,全会话共享 |
| 浏览器 UI(徽章、用量视图) | 对应子包 Client 半区(dsh.client) | 随 bundle 持久加载 |
| 每会话独立的配置(prompt、persona、技能集) | agent preset(~/.dsh/.agent-presets/) | 每会话独立挂载/卸载 |
| 临时扩展、原型验证 | 动态插件(cordis_define) | 进程级,重启即失 |
判断准则
- 能力需要跨会话共享或被浏览器访问 → 子包插件(bundle 层)
- 能力只属于"某个会话的 agent 行为" → agent preset
- 能力只是临时调试/验证 → 动态插件(用完即弃)
⚠️ 动态插件的最大局限:进程重启后全部丢失(这正是早期余额徽章 重启消失的原因)。任何"希望长期存在"的插件都必须固化为子包。
3. 双半区开发模型
每个子包插件的形态是"一个包,双半区"(以 usage-card 为例):
浏览器 (client.js) Host (index.js)
───────────────── ─────────────────
用量视图(「用量」Tab) 会话日志聚合(每日 token)
│ fetch GET /dizzy/usage │
│◀─────────────────────────────── ├─ 扫描 ~/.dsh/sessions(增量)
│ └─ webServer 路由 /dizzy/usage
核心原则
- 密钥只活在 Host:浏览器拿不到 API key / 凭据,所有敏感操作留在 Host 半区,Client 通过同源 HTTP 路由中转。
- Client 免构建:
client.js是window.__ModuleLoader__.load({ id, factory })工厂格式,factory内的require("react")由平台 seed 提供, 不需要 TypeScript 编译或 bundler 打包,改完即生效。 - 一功能一子包:每个功能 =
plugins/<name>/独立包(host + client + 自己的 package.json),主包只聚合;不把新功能塞进既有子包。 - 改动即提交:
file:是安装时快照语义,仓库即线上代码;git pull后 删除 profile 里node_modules/dizzy-dsh*副本并重新dsh plugin add file:<仓库>同步,再重启。
4. Host 半区开发(plugins//index.js)
插件骨架
export default {
name: 'dizzy-dsh',
inject: ['credentials', 'timer', 'tools', 'webServer'], // 按需声明
apply(ctx) {
// ── 初始化 / 定时任务 ─────────────────────────────
const refresh = async () => { /* ... */ }
refresh()
const stopTimer = ctx.interval(refresh, 60000) // 60s 一次
// ── HTTP 路由(供 Client 半区取数)─────────────────
const stopRoute = ctx.webServer.register({
kind: 'exact', // exact | prefixes
path: '/dizzy/balance',
handler: async (req, res) => {
res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' })
res.end(JSON.stringify(cache))
},
})
// ── 模型可见工具 ─────────────────────────────────
const disposeTool = ctx.tools.register({
name: 'balance_check',
description: '...',
parameters: { type: 'object', properties: {}, additionalProperties: false },
output: {
schema: { type: 'string' },
render(_args, value) { return [{ type: 'text', text: String(value) }] },
},
async execute() { return '结果' },
})
// ── 系统提示词注入(可选)────────────────────────────
const disposePrompt = ctx.systemPrompt.section({
name: 'dizzy-dsh:agent-instructions', // 唯一名,重复注册抛错
order: -50, // 负数 → 在 persona(0)之前渲染
text: () => readFileSync(PROMPT_FILE, 'utf8'), // 每次组装动态读取
})
// ── 清理:停止/卸载时执行 ─────────────────────────
return () => { disposePrompt(); stopTimer(); stopRoute(); disposeTool() }
},
}
可注入服务(先查询再使用)
写代码前用 cordis_inspect_query(Host Service provider)确认签名
(这些 inspect 工具只在官方「创造模式」会话里;DIY 模式不挂 tool-cordis):
Service.listService { } → 服务目录
Service.listService { service: "x" } → 精确契约
常用服务:credentials / timer / tools / webServer / fs / shell / subprocess / settings / agentDefaultModel / llm。
静态插件 vs 动态插件的环境差异
| 能力 | 静态插件(bundle) | 动态插件(沙箱) |
|---|---|---|
全局 fetch | ✅ 直接可用 | ❌ 被禁用(提示走 ctx.web) |
import 其他包 | ✅ Node ESM 可用 | ❌ 沙箱禁用 |
| 生命周期 | 随 profile 持久 | 进程重启即失 |
| 网络/进程 | 直接用 | 必须经服务中转 |
早期余额插件在动态沙箱里被迫用
subprocess+curl.exe绕行, 且环境变量名撞上SENSITIVE_ENV_PATTERN(/KEY|PASSWORD|SECRET|TOKEN/i) 被 scrub 导致 401。固化到 bundle 后一行fetch解决 —— 新功能优先写成 bundle 静态插件,不要从动态插件起步。
4.5 系统提示词注入(systemPrompt)
对 DSH 系统提示词动手脚的正规入口是 ctx.systemPrompt 服务(bundle 层注册
= 全局生效,所有会话、所有工作区都包含):
| 方法 | 作用 | 本仓库用法 |
|---|---|---|
section({ name, order, text }) | 注册系统提示词段落,按 order 升序拼接 | dizzy-dsh:agent-instructions,order -50 |
context({ name, order, text }) | 注册动态上下文(user-role 快照) | 暂未用 |
variable(name, provider) | 注册 {{variable}} 插值变量 | 暂未用 |
suppressRuntimeContext() | 抑制动态运行时上下文 | 慎用,勿全局抑制 |
tools(provider) | 注册工具 schema 提供者 | 见 tools.register |
order 约定(查证 dsh-system-prompt 源码)
-100 harness 身份(最先)
-50 ← 本仓库注入的 Agent 规则
0 persona(部署人格 / agent preset 覆盖)
100+ 工具指南
负数段在 persona 之前渲染,适合"规则/约束"类内容;complete: true
会独占整个系统提示词,绝不用于注入(会覆盖 harness 身份)。
内容动态读取
text 支持函数,每次模型步骤组装时求值 → 编辑 prompts/agent-instructions.md
无需重启,下一个模型步骤即生效:
const disposePrompt = ctx.systemPrompt.section({
name: 'dizzy-dsh:agent-instructions',
order: -50,
text: () => readFileSync(PROMPT_FILE, 'utf8'),
})
与 DSH 内置 agent-instructions 的分工
| DSH 内置 agent-instructions | 本仓库注入 | |
|---|---|---|
| 注入路径 | user-message(会话历史) | system-prompt section |
| 内容来源 | 工作区的 AGENTS.md/CLAUDE.md | 仓库 prompts/agent-instructions.md |
| 依赖 | 工作区存在该文件 | 全局,不依赖工作区 |
| 优先级 | 作为用户消息 | 系统提示词,模型最先读取 |
两者名字不同、注入路径不同,同时存在不冲突,互补使用。
5. Client 半区开发(plugins//client.js)
免构建 ModuleLoader bundle 骨架
window.__ModuleLoader__.load({
id: 'dizzy-dsh', // 必须等于 entry 的包名
factory: (require) => {
var module = { exports: {} }
var exports = module.exports
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
const React = require('react') // 平台 seed,免 import
const apply = (ctx) => {
const slots = ctx.get('slots')
if (slots === undefined) return
// 注册 Slot UI(创造模式里先 cordis_inspect_query 查 Slots.listSubTree)
slots.inject('conversation.input.right', () => slots.register(
{ name: 'conversation.input.right', id: 'deepseek-balance', label: 'DeepSeek 余额' },
(props) => React.createElement(Badge, props)
))
return () => { /* 清理 */ }
}
exports.apply = apply
return module.exports
},
})
组件内取数(经 Host 路由)
// 浏览器真实环境:fetch / setInterval 全局可用(非动态守卫)
const response = await fetch('/dizzy/balance', { credentials: 'same-origin' })
const r = await response.json()
const timer = setInterval(load, 60000)
// 清理:return () => { clearInterval(timer) }
Slot 选择
用 cordis_inspect_query(Client Slots provider)listSubTree 查实时插槽树
(同样只在创造模式;DIY 对照本仓库已有 client 插件即可),
再对精确 root 查完整契约(ownerProps / registration / occupants)。常用:
| Slot | 用途 |
|---|---|
conversation.input.right | 输入栏模型选择器左侧(本仓库徽章位置) |
conversation.input.left | 输入栏工具行左侧 |
conversation.composer.dock | 输入栏下方状态行 |
conversation.view | 会话视图环(本仓库「用量」Tab 挂载点:一 entry 一 Tab,宿主只渲染激活视图;chat=0/trajectory=10,新视图取 order 20) |
conversation.session.header.utilities | 会话页头右侧列表(加法,不替换页头) |
conversation.session.header | single 槽,宿主已占 priority 0;占用即替换整条页头,本仓库不用 |
settings.section | 设置页(完整页面) |
tool.view.cordis | 动态插件 Run 卡片内交互区 |
Client 端依赖声明
package.json 的 dsh.client.inject 控制 client bundle 的加载顺序依赖
(不是服务注入):
"dsh": {
"client": {
"platform": "web",
"inject": ["@deepseek-ai/dsh-client-runtime", "@deepseek-ai/dsh-client-ui-conversation"]
}
}
如果新 UI 依赖其他 client 包的 Slot/服务,按需追加(创造模式里先查
cordis_inspect_list 确认 client 服务是否存在;DIY 对照已有 client 插件)。
6. 关键技术机制(踩坑总结)
6.1 loader 相对/包名解析
- entry
name以.开头 → 相对组合文件目录解析(new URL(name, baseUrl)) - entry
name是包名 → 从 profile 的 node_modules 解析 - bundle 层 entry 必须用包名(
dizzy-dsh),不能是dizzy-dsh/plugins/x.js子路径 —— client-modules 按 entry 名解析require.resolve(<entryName>/package.json)找包声明, 子路径会导致解析失败、client 半区不加载。
6.2 ESM 声明
package.json 必须有 "type": "module"(或插件文件用 .mjs),
否则 Node 把 .js 当 CJS,export default 直接语法错误。
6.3 环境变量 scrub
subprocess 会剔除环境中的 KEY|PASSWORD|SECRET|TOKEN 命名的变量和全部
DSH_* 前缀变量。不要把敏感值放环境变量传给子进程;直接用
credentials 服务 + 全局 fetch,或显式 argv 传参。
6.4 webServer 路由契约
ctx.webServer.register({
kind: 'exact', // exact(精确路径)或 prefixes(前缀,最长匹配)
path: '/dizzy/balance',
handler: async (req, res) => { ... }, // node:http 风格
})
// 返回 disposer;重复 (kind, path) 抛错
6.5 client-modules 扫描链
loader entry(name=包名) → require.resolve(包名/package.json)
→ dsh.client.platform === 'web'
→ exports["./client"] → 读 bundle 文件(缺失抛 MissingClientBundleError)
→ 注入浏览器 window.__DSH_BOOT__ → ModuleLoader 加载
7. 添加新功能(新子包)的完整流程
# 0. 建子包目录 plugins/<name>/
# package.json:name = 包名,声明 main + exports["./client"] + dsh.client
# 1. Host 逻辑 → plugins/<name>/index.js
# - 数据/工具/路由加入 apply,返回 disposer;name 用子包名
# 2. Client UI(如果需要)→ plugins/<name>/client.js
# - window.__ModuleLoader__.load({ id: <包名>, ... })
# - slots.inject 注册目标 Slot
# - 数据经 ctx.webServer.register 的新路由获取
# 3. 主包 package.json 的 dependencies 加 "包名": "file:./plugins/<name>"
# cordis.patch.yml 加一行 entry(name = 包名)
# 4. 验证
Remove-Item ~/.dsh/profiles/web/node_modules/dizzy-dsh* -Recurse -Force
dsh plugin --profile web add file:<仓库绝对路径>
dsh --profile web --dump-config # 新 entry 是否挂载
# 浏览器刷新,检查 UI 是否出现
# 5. 提交
git add -A && git commit -m "feat: ..." && git push
# 用户侧:git pull + 删副本重装 + 重启 dsh web
验证清单
-
dsh --dump-config输出包含# == dizzy-dsh段,且该段出现全部 entry(balance / usage-card / dizzy-agent-instructions / kimi-webbridge / dsh-subscription-auth / better-sidebar / dsh-vision-toolkit / genui / dsh-notification / ui-gui-customization) - Host:每个子包可加载且 name/inject 正确:
node --input-type=module -e "import('dizzy-dsh-balance')",import('dizzy-dsh-usage-card')、import('dizzy-dsh-agent-instructions')、import('dizzy-dsh-kimi-webbridge')、import('dsh-subscription-auth') -
node --test plugins/balance/grok-parse.test.js全绿 - 收录的第三方可加载(依赖齐全):
import('dsh-better-sidebar')、import('@anionex/dsh-vision-toolkit')与import('dsh-gui-customization')均不报Cannot find package ...(link: 安装的典型症状);dsh-gui-customization是空 Host(typeof apply === 'function'),无 name/inject - 每个有 UI 的子包 Client:
exports["./client"]指向的文件存在,含window.__ModuleLoader__.load且 id 等于包名 - 浏览器:目标 Slot 出现 UI,数据路由返回正确 JSON
- 重启 dsh web 后功能仍在(验证持久化)
- 提示词注入:新会话中系统提示词包含注入规则(可在对话中询问 agent 是否遵循 First-Principles 等规则验证)
7.5 收录第三方插件(third-party)
仓库收录他人已做好的 DSH 插件快照,供"克隆即装",同时明确保留上游 地址与来源标注。规范:
- 目录:
third-party/<包名>/,包名与插件package.json的name一致 (如DSH-better-sidebar对应dsh-better-sidebar) - 每个目录必须有
UPSTREAM.md:上游仓库、收录版本、上游 commit、License、收录方式、更新方式、本地安装命令 - 快照不改:收录内容与上游一致(允许整文件格式层同步),功能修改 一律提交到上游;本地有未提交补丁时在 UPSTREAM.md 中注明
- 排除
.git、node_modules、__pycache__、*.tgz(.gitignore 已兜底) - README「收录的第三方插件」表格同步登记:插件名 / 上游链接 / 版本 / 收录位置 / 说明
安装收录的插件不需要单独 add:主插件 package.json 的 dependencies
声明了它们(dsh-better-sidebar@0.15.0 registry + @anionex/dsh-vision-toolkit
file:./third-party/... 快照),一条 dsh plugin add file:<仓库> 全部安装
并随主插件 patch 一起挂载:
dsh plugin --profile web add file:<仓库>
dsh --profile web --dump-config # # == dizzy-dsh 段出现全部 entry(含 ui-gui-customization)
已验证(
tmp-file*临时 profile 完整实测):file:安装主插件时 pnpm 递归解析其 dependencies —— better-sidebar 及其全部运行时依赖 (ws/codemirror/xterm/node-pty 等)从 registry 安装,vision-toolkit 快照 及其依赖 saxes 一并装入,hoisted 提升到顶层 node_modules,自有与收录 entry 全部 import 成功、dump-config 全部出现。link:方案则不行(不装依赖, better-sidebar 缺 ws 直接 import 失败)。首次安装记得配置pnpm-workspace.yaml的allowBuilds(node-pty/protobufjs → true), 否则 pnpm 以ERR_PNPM_IGNORED_BUILDS非零退出、reconcile 不执行。
更新上游快照:重新获取发布包/同步 checkout(见各 UPSTREAM.md),更新版本 与 commit 记录后提交。
7.6 本月用量视图(usage view)
会话页头的「用量」Tab(conversation.view 视图环,列在「对话」「轨迹」
右侧),整页仪表面:统计卡 + 月度热力图与近 7 天曲线 + 今日明细(分模型)+ 北京时间峰谷状态。
主数字是本月合计,热力图/明细/时钟都是次级信息。
| 面 | 实现 |
|---|---|
| 数据 | Host 扫描 ~/.dsh/sessions/**/session.jsonl.zstd。DSH 0.1.1-rc.2 起按 token-meter:每步 assistant/chunk { type: 'usage' } 先入账,同 turn/step 的 assistant/message.usage 覆盖(失败请求只留 chunk 也计入);旧日志只有 message.usage 时按条累计。模型归属取 message 的 data.message.source(provider/model,缺省 unknown)。金额按本地价 > DeepSeek 官网峰谷价 > OpenRouter 聚合价。增量刷新(文件 mtime+size 变化才重读,30s TTL) |
| 路由 | GET /dizzy/usage?month=YYYY-MM → { month, days, total, detail, scannedAt, errors }:days 保持「日期 → 总 tokens」(后向兼容),detail = { days: 逐日 input/output/cacheRead 分项, recent7: 近 7 天(与查看月无关,含零用量天), today: 今日分模型 };errors > 0 时副标题提示「N 个日志文件解析失败,用量可能被低估」。旧 Host(无 detail)下 client 自动退化:弹窗只显总量、今日明细显重启提示 |
| 挂载 | Client 注册 conversation.view list 插槽(id: 'usage'、order: 20、label: '用量';chat=0、trajectory=10)。宿主把每个 entry 投影为页头 Tab,renderSlot(..., { only: activeId }) 一次只渲染激活视图;选中状态存于宿主每会话 store(persist: dsh.conversation.chat),刷新页面保持;插件卸载后宿主 resolveActiveView 自动回退 chat。视图本身是普通整页流,不需要 portal;悬浮读数是视图内的 position:fixed 弹窗,跟鼠标并避让视口边缘 |
| 热力图 | 周一起始 7 × 周数 网格(34px 格,行=周一~周日、列=周),格内日期数字;DeepSeek 蓝阶四档(lv1–lv4,按当月峰值比例分档),月外 visibility:hidden;今日描边+脉冲;hover/focus 浮层显示日期 + 总量 + 输入(未命中)/输入(命中缓存)/输出 |
| 曲线 | 热力图右侧「近 7 天」用量曲线(detail.recent7):Catmull-Rom 转三次贝塞尔平滑过点 + 面积淡填;鼠标按横坐标吸附最近一天,竖线跟随鼠标 X,弹窗跟鼠标并避让视口边缘;旧 Host 退化为查看月内 7 天 |
| 今日明细 | 按模型分行的统一蓝底堆叠条:输入未命中(蓝) / 输入命中缓存(青绿) / 输出(琥珀);条长相对当日峰值,段宽相对该模型三项之和;缺分项时整条纯蓝;旧 Host 无 detail 时显示重启提示 |
| 月份切换 | 页头 ‹ › ±1 月;点月份打开年/12 月格 + 「回到本月」;Esc / 点外侧关闭。60s 自动重取 + 手动刷新按钮;数据按 data.month === 查看月 判定可见(切月即骨架,旧月数据不串月),同月刷新静默 |
| 时钟 | 独立 PeakClock(不拖整页重绘);底栏小圆点 + HH:MM + 峰谷标签;色从 --dsw-static-green-500 / --dsw-static-red-500 读 rgb 再渐变 |
| 外观 | 居中栏(max-width 860px),全部吃宿主 --dsw-* token(明暗主题跟随);纵向滚动由宿主 scrollBody 提供,视图只管内容流;scrollBody 与对话共用,激活时主动 scrollTop = 0 回顶(对话自身有每会话滚动位置存档,不受影响) |
注意:DeepSeek 官方 API 无按天用量接口。DSH 0.1.1-rc.2 token-meter 以 每步
assistant/chunk { type: 'usage' }为样本,同 turn/step 的assistant/message.usage覆盖该步;本视图按同一规则聚合本地会话日志, 与官方控制台「用量」页口径可能不同。 zstd 多帧解压逻辑复刻自@deepseek-ai/dsh-session-persistence-jsonl的scanZstdFrames(按 block 遍历,不依赖 FCS),插件不能 import 该包(profile 的 node_modules 里没有 @deepseek-ai/*)。
7.7 余额徽章的 Grok 模式(dizzy-dsh-balance)
输入栏右侧同一徽章(conversation.input.right / id: deepseek-balance):
deepseek-official 显示 ¥,grok 显示 SuperGrok 周额度剩余百分比。
不是 grok.com 网页的 2h 查询桶。
| 面 | 实现 |
|---|---|
| 凭证 | 读订阅插件写入的 GROK_SUBSCRIPTION_TOKEN(JSON:refresh/access/expires);过期或 401 时 POST https://auth.x.ai/oauth2/token 续期,写回前再读盘合并,热登录换了 refresh 不覆盖 |
| 上游 | GET https://cli-chat-proxy.grok.com/v1/billing?format=credits,头 Authorization: Bearer + X-XAI-Token-Auth: xai-grok-cli + x-grok-client-mode: interactive。可选再打 /v1/settings 取 subscription_tier_display |
| 账本 | 只用 config.creditUsagePercent + config.currentPeriod;省略 percent 且有 period = 真实 0%;禁止用 monthlyLimit/used 反推。展示已用 floor,剩余 100 - floor(已用) |
| 路由 | GET /dizzy/grok-quota → { status, remainingPercent, creditUsagePercent, periodEnd, subscriptionTier, error, at }。同源校验;响应不含 token / email / accountId |
| 工具 | grok_quota_check(无参);未登录提示去设置 → 订阅服务 |
| 挂载 | 仍是 dizzy-dsh-balance 的同一 slot,不另开插件 |
企业代理把
dizzy-balance.grokBillingBaseURL指到自建 cli-chat-proxy(/v1结尾)。不要读~/.grok/auth.json,也不要打 grok.com gRPC-web。
8. 常见问题排查
| 症状 | 原因 | 处理 |
|---|---|---|
| 重启后功能消失 | 动态插件(进程级) | 固化为 bundle 层 |
| 客户端 UI 不加载 | entry name 不是包名 / exports["./client"] 缺失 / client.js 文件缺失 | 对照 §6.5 扫描链逐环检查 |
export default 语法错误 | 缺 "type": "module" | package.json 补声明 |
余额显示 … 不更新 | Host 路由未注册 / 缓存未刷新 | 检查 webServer 路由与 interval |
| Grok 徽章不出现 / 一直「未登录」 | 当前模型不是 provider === grok,或尚未走订阅登录,或 file: 快照未同步 | 切到 Grok 模型;设置 → 订阅服务登录;删 node_modules/dizzy-dsh-balance 后重装合集 |
| 401 授权失败 | 敏感 env 被 scrub / key 未配置 | 用 credentials + fetch,检查 DEEPSEEK_API_KEY |
| patch 不生效 | 改完没重启 / link 指向旧路径 | 重启 dsh web;确认 bundles 列表 |
| 重复路由报错 | (kind, path) 重复注册 | 换路径或复用已有注册 |
duplicate loader entry id: agent-instructions | patch 里用了官方已占用的 entry id | 改成 dizzy-agent-instructions;官方 id 即使用 disabled: true 也仍占着 |
single slot "conversation.session.header" already has a registration at priority 0 | 误占宿主 single 槽 | 改挂 conversation.session.header.utilities(list);不要用换 priority 去 shadow 整条页头 |
corrupt Zstandard session log: first frame is not exactly one header line | 某份 session.jsonl.zstd 的第一帧明文不是单独一行 header(常见于把多帧日志解压后再一次性压回);官方 workspace list() 对这类文件零容忍,整棵插件树起不来。不是 Dizzy-DSH 插件运行时逻辑错误 | 从仓库根跑 node scripts/repair-zstd-header-frame.mjs 先 dry-run(只报 session.jsonl.zstd,不改盘);确认 actionable 后加 --apply。解压明文与 .bak 完全一致才还原多帧备份,否则拆成「header 一帧 + 其余一帧」(能过 list,不等于恢复官方逐批追加的帧布局)。空文件/撕坏首帧官方会跳过,脚本也 leave,除非旁边有可用 .bak。修完仍可能因同目录残留 session.jsonl、重复 session id、header 路径对不上而起不来 |
history unavailable + uncachedInputTokens / Too small: expected number to be >=0 | 订阅插件(Kimi Anthropic)曾把净增量 input_tokens 再减 cache,写出负 usage;DSH tokenUsage schema 非负校验会让 session.history 整页失败 | 已在 third-party/dsh-subscription-auth 写入侧 mapUsage 钳零,读路径 projection-guard 按 unit 钳非负(本地补丁,见 THIRD-PARTY-PATCHES.md)。不要改 DSH 内核。旧日志仍炸则换新会话;改完重启 dsh --profile web |
duplicate loader entry id: dsh-subscription-auth | profile 的 cordis.patch.yml 仍单独 insert 了试装行,与合集 bundle patch 撞 id | 删掉 ~/.dsh/profiles/web/cordis.patch.yml 里那条 insert,并去掉指向仓库外的 junction |
duplicate loader entry id: ui-gui-customization | 本机曾单独 dsh plugin add dsh-gui-customization,与合集 bundle patch 撞同一 id | 先 dsh plugin --profile web remove dsh-gui-customization,再重装合集 |
9. 与动态插件 / agent preset 的协作
- 动态插件仍可用于:临时调试、原型验证、需要审批流程的交互式工具
(
tool.view.cordis面板)。成熟后固化进本仓库。工具集只挂在官方 「创造模式」(cordispreset 的tool-cordis);cordisInspect是进程 单例,第二个含该行的 preset standing mount 会抛already registered。 - DIY 模式(
presets/diy/→~/.dsh/.agent-presets/diy):持久造物 preset,不挂tool-cordis,与创造模式同进程共存。动态探测请另开创造模式。 - agent preset(
~/.dsh/.agent-presets/):管理 persona、每会话工具集、 技能目录。本仓库是 Host 平面;两者互补,不冲突。 - 本仓库插件发布服务时,服务名全局唯一(进程级注册表),避免与 profile 其他 bundle 撞名。
文档版本:1.1(2026-08-22,对齐 DSH 0.1.1-rc.2)。所有机制均经实际验证;修改架构前请先在
创造模式用 cordis_inspect_query 确认当前运行时契约,再更新本文档。