wfengq/DSH-Tally--packages-tally-ui ↗★ 0
dsh-tally-ui
dsh-tally 的参考界面:费用条、每轮费用、会话累计、用量看板、预算提醒和设置页 适合需要在Web界面直观查看费用、会话累计和进行预算设置的用户。
Install
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:wfengq/DSH-Tally#e9cbf3529557185b9eac305c5c7ca569573383c8&path:packages/tally-uiREADME
Read the full README ↗dsh-tally-ui
dsh-tally 的参考界面插件,运行在 dsh 的 Web 界面(dsh --profile web)里。它只通过同源的 Remote API 命名空间 tally 读写 dsh-tally 的数据,自己不读凭据、不发起其他网络请求。
安装
先安装 dsh-tally,再安装本插件,然后重启 dsh:
dsh plugin --profile web add dsh-tally@beta dsh-tally-ui@beta
浏览器端代码只在 dsh 启动时加载,所以安装或升级后需要重启 dsh。需要显示 DeepSeek 余额时,再安装 dsh-tally-balance-deepseek。
注意:浏览器端代码只会挂到以包名(dsh-tally-ui)命名的 Loader 条目上,请用 dsh plugin add 安装,不要在 patch 文件里用路径引用 lib/client.js。
功能
界面共有六处,布局照 dsh 自带页面的规范:设置内容放在统一材质的圆角卡片里,每行左边是名称和说明、右边是控件,按钮、输入框、开关、弹窗、菜单、折叠行都用 dsh 的公共组件。下表的“截图”一列是 e2e 测试(tests/e2e/web.e2e.test.ts)运行后保存在 tests/e2e/screenshots/ 里的文件,可以对照查看(截图不提交到仓库)。所有金额都按币种分开显示,不做换算;没有价格时显示“未定价”,不会显示 ¥0;小于 0.0001 的金额显示为 { … } }),由 dsh 的客户端模块加载器在需要时执行;
- 只有宿主共享模块保持为
require()(tsdown 0.23 的deps.neverBundle,即旧的external),其余全部内联。
node scripts/check-client-bundle.mjs packages/tally-ui/lib/client.js:扫描 bundle 里所有require("…"),出现共享模块表以外的模块就报错退出。
bundle 共享模块限制:dsh 的客户端模块加载器只向插件提供固定的宿主共享模块表:react、react/jsx-runtime、react-dom、react-dom/client、@deepseek-ai/cordis、@deepseek-ai/dsh-client-store、@deepseek-ai/dsh-client-ui-slots、@deepseek-ai/dsh-client-ui-primitives、@deepseek-ai/dsh-client-ui-dockkit。bundle 里 require() 其他任何模块都会在浏览器中失败,所以 zod、dsh-tally/remote、本包自己的代码和样式全部内联;其他 @deepseek-ai/dsh-client-* 包只能 import type(用于类型和声明合并),不能引入运行时值。目前的 bundle 实际只引用了 react、react/jsx-runtime 和 @deepseek-ai/dsh-client-ui-primitives。
样式:采用“factory 执行时注入 ``”的方式,不使用 .module.css。src/client 下的 .css 文件在打包时被转换成一段代码,factory 执行时向 document.head 插入一个 ">(与 dsh 官方插件的标记相同,插件卸载时由 dsh 回收)。类名不做哈希,统一加 dsh-tally- 前缀避免冲突。样式只使用 --dsw-alias-* 语义变量和 --dsw-elevation-*,不写主题选择器和字面颜色值。
接口描述:浏览器端调用 Remote API 需要一份带严格编解码器的接口描述(TypertRemoteContribution)。dsh 官方用构建期生成器产出这份描述,但生成器依赖 dsh 仓库内部的工作区结构,所以由 dsh-tally 手写(dsh-tally/remote,见 packages/tally/src/remote/contract.ts),本包在启动时用其中的 mountTallyRemote(ctx) 挂载:如果另一个客户端插件已经挂载过 tally(remote.tally 服务已存在),就跳过挂载,直接使用已有的命名空间;否则调用 ctx.remote.$mount()。两个插件同时挂载时,输掉的一方会收到宿主的 client api: direct method tally/… is already mounted(或 Typert 注册表的 Remote package "dsh-tally" is already registered),同样视为成功。
执行中的判断规则(Task 1):两条备用方案都没有用到,最终的端到端验收也是在这个前提下通过的。
- Host 端:编译后的
lib里,标注了@Remote的方法能被 dsh 网关直接识别(网关从运行中的服务读取装饰器标记,参数名从编译后的函数读取),不需要改用bindTypertRemote手动注册。 - 浏览器端:手写的接口描述可以被
ctx.remote.$mount()接受,不需要改用官方生成器typertPlugin。因此也不需要在发布前复核生成器版本与 dsh 版本的对应关系。
宿主表单接口的两个细节(在真实 dsh 中验证时发现):
- 宿主的
ConfigForm是一个类,getSnapshot/subscribe依赖this,传给 React 的useSyncExternalStore前必须包一层,不能直接传方法引用。 ConfigForm.set(field, value)的field只是一层键名,'elements.turnTail'会被当成一个字面键而被拒绝;嵌套字段要用mutate([{ op: 'set', path: ['elements', 'turnTail'], value }])。
测试
pnpm test:组件和数据层的单元测试(jsdom),以及 bundle 共享模块检查。pnpm test:e2e:在真实的dsh --profile web中,用 Chromium 依次验证费用条(含与 dsh 自带统计按钮的外观和交互一致)、本轮费用(含点击详情,样式与 dsh 自带按钮一致)、会话累计、余额(含刷新失败后的“已过期”)、用量看板(含 CSV 下载)、设置页(新建预算后出现预算提醒、关闭turnTail后新一轮不再显示本轮费用)以及页面重新加载后数值不变;全过程页面没有console.error,dsh 的 stderr 没有ExperimentalWarning和failed to import。模型请求由@deepseek-ai/dsh-llm-mock-server应答,余额由测试内的本地服务应答。测试固定使用网页内的目录选择器(不弹出系统文件夹窗口),所以在 Windows、macOS 和 Linux 上行为一致。本机第一次运行前需要装一次 Playwright 的浏览器:pnpm exec playwright install chromium。
许可证
MIT