packages/dsh-task-board
zhu1090093659/dsh-web侧边栏多列任务看板:卡片交给真实 DSH 智能体会话执行,支持 cron 定时(Host 侧到点执行,关浏览器也生效)。
DSH 会话头里的下载进度指示器:读取后台任务名册,把下载器自己已经打印出来的进度(curl --progress-bar、wget、aria2,或 JSON 进度文件)解析成环形进度、动画进度条、传输速率与剩余时间;不上报进度的任务显示为未知进度而不是被隐藏。已完成文件单击即开,也可在文件管理器里定位。不依赖任何第三方插件,运行时零依赖。
专为 DSH 的下载任务做的进度指示器 —— 会话头里一个常驻的下载图标(Chrome 下载按钮那种), 自动发现正在跑的下载任务与后台任务,用环形进度、动画进度条、速率和预计剩余时间实时呈现。
English: A download-progress indicator for DSH (DeepSeek Harness): a persistent icon in the session header that finds your running downloads on its own, and renders them as a progress ring, animated bars, transfer speed and ETA. Full English documentation: README.en.md.
它盯的是「下载」这一件事。
在 DSH 里,下载几乎总是以后台任务的形式发生的:curl / wget / aria2c 下载模型权重、
安装包、数据集、字体包,或者包管理器在拉依赖。它们的进度只打在任务输出里,于是你要么盯着
终端,要么反复问模型「下完了没」,要么干脆忘了它还在跑。
这个插件把这件事变成一眼可见:会话头右上角一个图标,有下载在跑时外圈亮起环形进度弧、 右下角出现数量角标;点开是一份清单,每条一行动画进度条,带百分比、已传输 / 总量、速率与剩余 时间。下完之后,点一下文件名就能打开它。
它不启动任务、不杀任务、不写文件——是一个纯粹的观察者(唯一的例外是「打开文件 / 打开文件夹」,那需要你明确点击,而且只对面板上正在显示的那几个文件生效)。
为什么它同时也会列出别的后台任务? 因为在 DSH 里下载和普通后台任务走的是同一本名册 (
ctx.jobs)——插件没法、也不该去猜哪条才是下载。所以它把名册里的任务都读出来,能解析出 进度的就画成进度条,解析不出的就显示成「未知进度」的走马灯条。对下载任务来说,这正是你要的 效果;对别的后台任务来说,它顺带也是个进度看板。
--progress-bar 的 \r 刷新进度表、wget 的 45%[===> ] 7.98M 1.21MB/s eta 8s、
aria2 的汇总行、各类 已传输 x / y 写法,以及通用的 JSON 进度文件。dsh-better-sidebar 之类的插件装没装都不影响它。package.json 里 dependencies 是空的,包内不含 node_modules。node tools/build.mjs 就是全部构建步骤,产物随仓库提交,
安装端不需要构建。dsh plugin --profile <profile 名> add github:MorixMa/dsh-download-progress
产物(lib/)随仓库提交,所以安装端不需要跑构建,也不需要联网装依赖。
dsh plugin --profile <profile 名> add link:<本仓库绝对路径>
desktopprofile 由 Electron 应用独占管理,命令行会被设计性拒绝 (error: profile "desktop" is managed exclusively by the Electron application)。 那个 profile 请用应用内插件管理器,填同一个github:或link:规格。
装完需要重启 DSH(host 半边要重新装载),然后硬刷新页面(Ctrl+Shift+R)。 会话头右上角就会出现下载图标。
卸载与逐步验证见 INSTALL.md。
| 状态 | 样子 |
|---|---|
| 空闲 | 灰化的下载图标,无角标 |
| 有任务在跑 | 图标外圈一条环形进度弧,平滑推进(stroke-dashoffset 过渡 0.55s);右下角一个蓝色数量角标,带缓慢呼吸动画 |
| 进度未知(源里没有百分比) | 环变成旋转 + 伸缩的走马灯弧 |
| 只剩失败任务 | 角标转红,显示失败数 |
N 个运行中,下面一条总览进度条与聚合数字live-ls-lR.gz)。下载位置、目录、盘符、整行命令行一律
不进 UI——连鼠标悬浮的 title 里都没有100% · 36.71 MB / 36.71 MB · 48.1 MB/s所有动画在 prefers-reduced-motion: reduce 下自动关闭。
界面不渲染源里的原文。进度块(长得像 markdown 的那种)只作为数据被解析成
条 / 数字 / 速率;原文仍留在 host 响应的 raw 字段里,方便 curl 排查,但一行都不会
出现在 UI 上。
| 用在哪 | 从哪来 | |
|---|---|---|
fileName | 给人看,就是那一行标题 | 产物路径的 basename → 进度块行首的 wget 式文件名 → 进度源写的 label → 被擦掉所有本地路径的命令行首行 |
filePath | 不显示,只用于"打开 / 定位" | 命令行里的 -o / --output / -OutFile 开关;认不到就退回最后一个非脚本绝对路径 |
路径解析带两条防误判:(?<![A-Za-z0-9]) 让 https://… 里的 s:// 不被当成盘符;
POSIX 分支的 (?<![\w:/]) 让 URL 里切不出 /host/a/b.gz 来。
按固定优先级看三个来源,并在空态里如实报告每个来源的可用性:
| 顺序 | 来源 | 说明 |
|---|---|---|
| ① | dsh-ops/background | 结构探测:找到发布 list() 的服务就用,找不到就报「未发布服务扩展点」。详见「已知限制」 |
| ② | DSH 后台任务 | 宿主 ctx.jobs 注册表(@deepseek-ai/dsh-jobs-local)。按会话读,读的是非消耗接口 list() + readAt()——不移动模型自己的 job_output 游标,也不触发任何完成通知 |
| ③ | 进度源文件 | 配置项 progressSources 里的绝对路径。文件按扩展名解析;目录浅扫描一层,取 *.md / *.markdown / *.txt / *.json / *.jsonl / *.progress |
进度块(多行文本逐行抽取,后出现的字段覆盖先出现的):
# 字体包下载
进度 ██████░░░░ 11/47 (23%)
体积 90.0 MB / 100.0 MB
速度 0.42 MB/s
剩余 00:02:31
认得的东西:百分比(23% / (23%))、比例(11/47)、进度条(██████░░░░、
[####------])、字节数(90.0 MB / 1.2GiB / 94371840 B)、速率(0.42 MB/s /
512 KBps / 3 MiB per second)、剩余时间(00:02:31 / 1m30s / 2 分 31 秒)。
标签取标题行 / **加粗** / 第一行无指标的文本。
通用 JSON:
{ "label": "模型权重", "total": 47, "done": 11, "bytes": 94371840, "bytesTotal": 104857600, "speed": 440401, "eta": 24000 }
也接受 { "tasks": [ … ] } 数组形态(一个文件携带多项任务)与顶层数组。
eta 是毫秒数或时长字符串。
不猜:源里没写速率就不显示速率(而不是显示 0)。速率与剩余时间的兜底来自
两帧差分——客户端记住上一次的字节数,用 Δ字节 / Δ时间 算出真实速率(指数滑动
平均 α=0.4 压抖动),再用 剩余字节 ÷ 速率 推剩余时间。同一个任务连续三次没有增量
就停止外推(任务卡住时不会一直显示一个旧速率)。
配置落在插件 Loader 行的 config: 上。默认值写在插件自带的
cordis.patch.yml 里,也可以写在 profile 的 patch 层里。
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
progressSources | string[] | [] | 进度源路径(绝对路径)。目录浅扫描一层 |
pollIntervalMs | number | 1500 | 客户端拉取间隔,钳制在 500–60000 |
- id: download-progress
name: dsh-download-progress
config:
progressSources:
- 'D:\字体_进度.md'
- 'D:\downloads\progress'
pollIntervalMs: 1500
改了必须重启 DSH。 配置在插件装载时被读一次并收窄;改了
config会让 Loader 重挂这一行,正在跑的 host 半边不会热更新。
ctx.settings.register(ns, schema) 在 DSH 0.1.7 起已经整体删除。替代品是宿主
SettingsForms:命名空间 = 插件 Loader 行的 entry id(这里是 download-progress),
表单 schema = 插件模块导出的 Config。本插件照这条规则导出了 Config,Loader 的
装载期校验认它。
代价说清楚:Config 用的是 Standard Schema 形状,不是宿主内建的 schemastery
形状。前者能被 Loader 正常校验(插件照常装载、config 照常生效),但宿主设置页不会
为它生成自动表单——Inspect 的 Config 通道里本条目显示 unsupported。
不换 schemastery 的原因是硬约束:host 产物零 import / 零 require,而
@deepseek-ai/schemastery 在 link: 安装下从插件目录解析不到(Node 走软链 realpath,
往上没有 node_modules/@deepseek-ai)。两者不可兼得,这里选了"插件一定能装上"。
| 现象 | 原因 / 处理 |
|---|---|
| 会话头里没有下载图标 | 客户端半边没加载。确认 dsh.profile.bundles 里有 dsh-download-progress,然后硬刷新(Ctrl+Shift+R);还不行就重启 DSH |
| 图标在,点开报「读取任务进度失败:Failed to fetch」 | host 半边的路由没挂上,重启 DSH。host 半边改动一律要重启才生效 |
| 报「路由返回 HTTP 403」 | 信任围栏拦下了。桌面版正常不会出现(dsh-app://app 已放行);出现说明请求来自别的源,或 GUI 被反代到了非回环地址——后者需要把该权威加进部署的 trustedHosts |
| 一直「现在没有运行中的任务」 | 弹层空态里会列出每个来源为什么是空的 |
| 进度条一直是走马灯(未知进度) | 该任务的输出里没有可识别进度。curl "http://127.0.0.1:19387/download-progress" 看那条任务的 raw 字段 |
| 速率一直是空 | 源没写速率,而且两次拉取之间字节数没变(差分没有基线)。需要 2 个采样点 |
| 进度条停在某个数字不动 | 正常——进度来自源;源不更新就不会动。看弹层底部的「刷新于」确认轮询还活着 |
| 解析出来的数字不对 | 对照上面「支持的进度写法」;要么改源,要么提 issue 带上那几行原文 |
dsh-ops/background 目前没有服务扩展点。它的 dsh 组件只注册
run_background / job_output / job_list / job_kill 四个工具,不 provide
任何 cordis 服务,所以来源 ① 在本机上是"不可用"(空态里会写明)。这一格保留了结构
探测:一旦它(或别的插件)发布带 list() 的服务,立刻会用上,不必改代码。真正的
数据来自来源 ②。src/client/90-entry.js 的 SLOT_NAME 换成
shell.overlay 即可(shell.overlay 是根作用域的浮层,那份 slot 的说明里明确写着
"a badge, a toast stack or a status pill all belong here")——代价是它不带 sessionId,
会退化成不带会话围栏的全局视图。ctx.jobs 名册的全部内容——
包括它的常驻 PowerShell/POSIX 会话(名字是那行命令的首行,例如
$ErrorActionPreference='SilentlyContinue'),它没有可识别进度,所以显示为"未知进度"
的走马灯条。DSH 自己的任务抽屉里同样列着它们。觉得吵的话可以在
src/host/40-tasks.js 的 collectJobTasks 里按 kind 过滤,但那属于按喜好筛选,
默认不替用户决定。| 路由 | 副作用 | 授权 |
|---|---|---|
GET /download-progress | 无(不改文件、不杀任务、不推游标) | 浏览器信任围栏 |
POST /download-progress/open | 会让宿主用系统默认应用打开一个文件 / 在资源管理器里定位它 | 信任围栏 + 白名单 |
围栏规则(与 DSH /api 网关同款):Host 必须是回环或部署声明的 trustedHosts;
Sec-Fetch-Site: cross-site 一律拒绝;带 Origin 时必须是桌面壳的 dsh-app://app
或与 Host 同名。非可信 Host 返回 403。
只认本插件自己刚报告过的文件。 采集任务时把每个已知产物的绝对路径写进一份
10 分钟 TTL、最多 128 条的白名单;POST /download-progress/open 收到的 path 不在
白名单里一律 403,连 stat 都不做。
这条设计的意义:即使有人能发同源请求,也不能把这条路由当成「启动任意路径」的后门—— 他只能打开面板上此刻正在显示的那几个文件。
打开与定位走 DSH 自己的原生能力:宿主服务 ctx.sessionController 的
openPath(path, signal) / revealPath(path, signal) / canOpenPath()
(实现在 @deepseek-ai/dsh-native-command 的 path-opener)。本插件不 exec、不 spawn、
不拼命令行。宿主没有这个服务时(非桌面部署),面板上两个按钮直接不出现,而不是点了报错。
围栏是这两条路由唯一的门。 实测 DSH 的 web token 只作用于它自己的 /api*
(GET /api/modules 无 token → 401,插件注册的 GET /download-progress → 200),所以任何
本机进程都能读到任务列表。这是平台约定;不想要就别装。
node tools/build.mjs # 构建 lib/(唯一构建步骤,零依赖)
node tests/test-parsers.mjs # host 逻辑单测(解析器 / 围栏 / 三路合成 / 进度源)
node tests/test-client.mjs # client 冒烟(ModuleLoader + React 替身真跑产物)
node tests/verify-install.mjs # 门禁:上面两个套件 + 安装/产物/dsh 字段检查
src/host/ 00-runtime.js → 内建模块垫片 + JSON 应答 + 信任围栏
10-config.js → Config(Standard Schema)+ 收窄
20-parse.js → 进度解析(进度块 / 通用 JSON)
30-sources.js → 进度源文件扫描
40-tasks.js → 三路数据合成任务列表
50-route.js → GET /download-progress
90-entry.js → apply / inject
src/client/ 00-format.js → 数字格式化(中文)
05-route.js → 宿主路由 URL 解析(壳传输基底 / baseURI)
10-sampler.js → 两帧差分 + 聚合
20-styles.js → CSS + 全部 @keyframes
30-view.js → 图标 / 弹层 / 任务行(React.createElement)
90-entry.js → ctx.slots 注册
tools/build.mjs → 碎片拼接(零依赖构建)
碎片按文件名编号装载,共享同一个模块作用域——所以源码里一条 import 都没有。
client 产物是 window.__ModuleLoader__.load({ id, factory }) 包装,factory(require) 里
只 require("react")(宿主自带)。一个第三方插件都不 require:Slot 服务经
ctx.slots 注入,HTTP 路由自己按传输基底解析。dsh.client.inject 因此只需声明
@deepseek-ai/dsh-client-ui-slots 一个包做装载排序。
link: 安装下,host 产物里任何对宿主包的 import 都会在链接期失败,而 DSH 不会
为此报错:插件只是永远停在未激活状态。所以 host 产物零 import / 零 require,
Node 内建模块经全局函数 process.getBuiltinModule() 取;运行时不支持它时(Node < 22.3)
只有「进度源文件」这一路降级为不可用,插件照常启动。
两个产物、零依赖、没有 JSX(用 React.createElement)。tools/build.mjs 就是
"按固定顺序拼接 + 套一层 ModuleLoader 包装",可读、可复现,也省掉一整套
devDependencies——而"包内不得有 node_modules"这条硬约束让打包器的收益进一步归零。
MIT