Repository navigation
Releases: wjf1/dsh-portable
Releases · wjf1/dsh-portable
Release list
v0.1.10: 补齐审计清单最后五项
0.1.10 — 2026-10-20
修复
- 上传输入没有禁用(
lib/client.js):上传进行中仍能再次选择文件,第二次点击会被uploadBusy静默丢弃,用户看不出为什么没反应。现在上传或任务进行中文件输入框是disabled。 - 删除包用的是闭包里的旧选中值(
lib/client.js的deleteArtifact):回调捕获的selected可能是上一次渲染的值,删掉当前选中的包时不会清空导入卡片。现在改用函数式setSelected,删除任何包后都清空预览结果。 - 刷新后丢失正在运行的任务(
lib/client.js的refresh):面板重载只恢复数据、不恢复busyJobId,进度条消失、按钮全部可点。现在refresh读到status.busyJobId且当前没有轮询时,取/jobs/detail重新接管该任务。 - 导入与恢复完成后没有提示重启(
lib/client.js):写入的配置要重启 DSH 才生效,面板此前只报「已恢复」。现在导入完成与备份恢复的提示都带「需要重启 DSH 才会生效」。 - 包读取失败被伪装成「缺 manifest」(
lib/archive.mjs的readManifest/extractPackage):createReadStream的错误没有转发给gunzip,pipe会吞掉它,调用方拿到的是missing dsh-portable/package.json(指向包内容而不是不可读的路径),还可能因未处理的error事件让进程退出。现在 source 的错误直接gunzip.destroy(error)。
测试
- 新增 1 项(缺失文件抛
ENOENT而不是「缺 manifest」),全量 60 项通过。
v0.1.9: 成功提示自动收起与复制超时清理
0.1.9 — 2026-10-20
修复
- 成功提示永不消失(
lib/client.js):导出完成、上传完成、备份已恢复这些notice提示此前会一直挂在面板顶部,下一次操作的反馈只能叠在旧提示下面。现在 4 秒后自动收起,重设或组件卸载时清掉计时器。 - 复制超时的计时器不释放(
lib/client.js的copyText):1.5 秒超时用的setTimeout在navigator.clipboard.writeText成功时不会被清理,页面里留下一个悬空计时器。现在用try/finally保证clearTimeout。
测试
- 无新增(纯前端计时器行为,
node --check+ 59 项全量通过)。
v0.1.8: 审计问题收口与稳定性修复
0.1.8 — 2026-10-20
按「同类问题全项目审计」的结论逐项收口:4 个 P1 正确性缺陷、8 个 P2 健壮性缺陷、6 个 P3 细节与 2 个疑似项。没有新功能,全部是让既有行为按文档所写的方式兑现。
修复(正确性)
package.json合并从来没有执行过(lib/rewrite.mjs/lib/installer.mjs):mergeProfilePackage写好了、HANDOFF 也宣称「按目标机现状合并」,但 install 路径只调用了rewriteExtractedTree,后者只做路径替换。结果导入会把源机的dependencies直接盖在目标机上,目标机后来装的插件在下次npm install时会被当成 extraneous 删掉。现在rewriteExtractedTree接收targetPackagePath,读目标机package.json后按mergeProfilePackage合并,结果写入report.rewrites.merged并在面板显示(保留 / 缺失的 bundle)。- 备份恢复会覆盖无法回滚的文件(
lib/routes.mjs的/backups/restore):pre-restore 拷贝失败时被catch(() => {})吞掉,接着照样把备份写回去。现在失败项进入failed并跳过,绝不覆盖没有留下应急副本的文件。 jobs.dispose()返回undefined(lib/jobs.mjs):index.js写的是void jobs.dispose().catch(...),worker 优雅退出路径根本没有被等待。现在dispose()收集每个worker.terminate()并返回Promise.all。- Range 下载不计入次数(
lib/share.mjs):浏览器断点续传会发Range: bytes=0-,此前只看「有没有 Range 头」,于是一次性链接可以无限下载。现在只有完整区间(range === null或0..size-1)才计数。 - worker 退出未上报结果时任务永久 running(
lib/jobs.mjs):原逻辑只在退出码非 0 时标失败,code 0 会让任务卡在 running、面板按钮全禁用。现在任何「running/queued 状态下退出且没上报结果」都记为 failed 并带退出码。
修复(面板)
- 刷新会清掉刚显示的错误(
lib/client.js的refresh):成功分支的setError(null)会吃掉follow刚设置的失败信息(0.1.7 只修了时序,本次从根上去掉)。 - 上传没有进度、也没有防重入(
lib/client.js):fetch改XMLHttpRequest,upload.onprogress驱动百分比;uploadBusyref 挡住连点。 - 轮询可能重叠(
lib/client.js的follow):前一次jobs/detail未返回时不会发起下一次(pollBusyref)。 - 请求没有超时(
lib/client.js的call):统一 60 秒AbortController,超时错误写明timeout after 60s: <path>。 - 任务无法取消:
JobBar增加取消按钮(POST /jobs/cancel),running 时才显示。 - 导出没有百分比(
lib/archive.mjs):write 阶段进度补percent,面板显示已完成 / 总数 files。 JOB_BUSY被当成 500(lib/routes.mjs):现在回 409 并带busyJobId。
修复(边界与清理)
/artifacts/delete删除不存在的包:404,不再是静默成功。/import预检:包不在磁盘上直接 404,不进 worker。/share/remove移除不存在的分享:404。/artifacts/download支持HEAD(返回头、空 body),handle把 HEAD 映射到 GET 路由。- 备份保留策略(
lib/installer.mjs):keep: 0现在真的清空普通快照;*-pre-restore目录永不被预算修剪(否则「恢复」唯一能撤销的手段会被自己删掉)。 - 导入中途失败不再留下
portable-staging目录:staging 清理移入finally。 settings()优先取profileContext.dshHome,lib/paths.mjs删除 7 个死导出与fs/os引用;lib/manifest.mjs删除describeManifest/formatBytesShort。
安全
/health不再回报当前连接数(未认证调用者不该知道有多少条链接在跑)。
测试
- 新增 9 项:删除不存在 404、导入预检 404、下载 HEAD、
JOB_BUSY409、分享移除 404、/health不暴露数量、完整 Range 计数、keep:0与-pre-restore、失败导入不留 staging。全量 59 项通过。
文档
- README 双语补
report合并展示、取消按钮、下载 HEAD 与测试数;HANDOFF 记录审计分类与逐项结论。
v0.1.7: 失败错误可见与上传包校验
0.1.7 — 2026-10-20
真机反馈「预览变更点了没反应」:查下来是两层问题叠在一起 —— 一个被截断的包,以及失败原因刚显示就被自己清掉。
修复
- 任务失败的错误只存在一帧(
lib/client.js的follow):failed 分支先setError(...)再void refresh(),而refresh()成功时会setError(null),于是错误立刻被清空,面板看起来像什么都没发生。改为refresh().finally(() => setError(message)),失败原因稳定留在顶部。 - 上传不校验包是否完整(
lib/routes.mjs的receiveUpload):截断的上传文件(例如 521.5 MB 的包只传了 22.7 MB)会被当成正常包收下,之后每次预览 / 导入都报unexpected end of file。现在落盘前先用readManifest读一次 manifest,读不出就删掉临时文件并回 400BAD_ARCHIVE。
测试
upload用例改为上传真实包(writeRealPackage辅助);新增「截断包被拒且不落库」(400BAD_ARCHIVE,列表里看不到)。全量 50 项。
文档
- HANDOFF 记录两处根因与排查路径。
v0.1.6: 进度条收尾与剪贴板超时兜底
0.1.6 — 2026-10-20
0.1.5 的真机走查揪出两个让面板「看起来卡死」的缺陷:一个是自 0.1.0 就写错的终态判断,一个是剪贴板 API 挂起。
修复
- 导出/预览/导入的进度条永不收尾(
lib/client.js的follow):后端任务终态是succeeded(lib/jobs.mjs第 83 行),前端却等done,于是stopPolling()与setBusyJob(null)永不执行、busy一直为真 —— 创建包、刷新、选择、分享、删除、执行导入全部禁用。本机导出 568.4 MB 后稳定复现。现在按succeeded收尾;state.succeeded也补了 i18n 文案(此前直接显示英文枚举)。 - 剪贴板写入可能永久挂起:
copyText直接await navigator.clipboard.writeText,在未聚焦或受限窗口里该 Promise 既不 resolve 也不 reject,按钮停在待定态。现在 1.5 秒超时后兜底到textarea+document.execCommand('copy')。
测试
- 全量 49 项通过(本次只改客户端行为,未新增用例)。
文档
- HANDOFF 增加「job 终态是
succeeded,不是done」的避坑条目。
v0.1.5: 面板可用性修复(备份恢复、导入确认、剪贴板)
0.1.5 — 2026-10-19
按一次真机走查的结论修面板:功能都在,但「点了到底发生了什么」不清楚,还有两处会让人误判成功。
新增
- 备份可恢复(
lib/routes.mjs的POST /backups/restore+lib/client.js的「恢复」按钮):把某份导入前备份写回主目录;写回前先把当前版本另存为<stamp>-pre-restore,所以恢复本身也能反悔。恢复只覆盖备份里存在的文件,replaced/目录不参与。 - 执行导入前弹内联确认:原来点「执行导入」直接落盘,和同一面板里「删除」的二次确认不一致。现在先展开确认块(写明当前是 merge 还是 replace、并提示 replace 会先移走旧项),确认后才调
POST /import。 - 上传进度:上传改走
XMLHttpRequest,显示百分比;完成/失败都有明确提示,不再「选完文件就没了动静」。 - 导出完成提示、以及删除/预览/恢复后清空的 notice 行:每张卡片操作完都有可读结果。
- 导入结果表格化:
applied/rewritten/backupDir/warnings用键值表格呈现,不再把整个 report 塞进<pre>。 - 凭据勾选警示:勾上「凭据文件(含密钥)」时,导出卡片下方出现红字提示(此前 PR 正文声称有警告,实际没有)。
修复
- 分享链接实际没进剪贴板:
shareArtifact原来只setCopied(url)就显示「已复制」,从未调用剪贴板 API。现在真正写入并把结果作为显示依据;失败时给出提示。 - 「复制」按钮的兜底分支假成功:剪贴板不可用或写入失败时仍无条件标记已复制。现在两条路径都返回布尔值,只有成功才显示「已复制」;纯 HTTP 来源下走
textarea+document.execCommand('copy')兜底。 - 包列表的「导入」按钮名不副实:它只做选中,真正导入在下方卡片。改名「选择」,并同步清空上一次的预览/结果/确认状态。
- 模式按钮用
disabled表示选中:视觉上像「不可用」,改成active高亮 +aria-pressed,点击不再被吞。 act.inspect死键:i18n 里定义了却没有引用,删掉。
测试
- 新增 3 项:备份恢复写入并保留被覆盖文件(
<stamp>-pre-restore)、恢复不存在的备份回 404、stamp 越界(../profiles)回 400BAD_STAMP。全量 49 项。
文档
- 双语 README 的六卡说明与 HTTP API 表补上「恢复」与执行导入确认、上传进度、复制兜底;测试数改为 49。
v0.1.4: 包列表支持下载到本机
0.1.4 — 2026-10-19
补上一条实际卡住换机流程的缺口:包能导出来,却没有任何入口把它交给浏览器存盘,U 盘 / 内网机这条离线路径走不通。
新增
- 包列表新增「下载」按钮(
lib/client.js):每个.dshpkg旁边都有「下载」,浏览器按a[download]同源下载存盘,文件名沿用包名;悬停提示说明用途。 - 新路由
GET /artifacts/download?id=(lib/routes.mjs):按 artifact id 校验(只认exports:/uploads:前缀,拒绝路径分隔符与..),返回application/octet-stream+content-disposition: attachment+content-length,用pipeline流式发送,几百 MB 的包也不会进内存。缺包回 404、越界 id 回 400,与其它 JSON 端点口径一致。
修复
- 流式响应之后不能再写 JSON:
sendJson/sendText增加headersSent/destroyed防护,外层 catch 在响应头已经发出时改为response.destroy();否则下载中断(浏览器取消、磁盘错误)会触发ERR_HTTP_HEADERS_SENT覆盖真实错误。
测试
- 新增 3 项:下载正常路径(200、类型、
attachment; filename="trip.dshpkg"、长度与正文一致)、包不存在(404NOT_FOUND)、artifact id 越界(400BAD_ARTIFACT)。全量 46 项。
文档
- 双语 README 的面板说明与 HTTP API 表补上「下载 / download」。
- 两张已脱敏的面板截图并入仓库
assets/(导出卡片 + 包列表、上传 / 导入 / 局域网分享 / 备份),仓库根新增screenshots.json声明这两张图,供插件市场详情页展示;package.json的files加入assets,下一次发版起 npm 页面里的 README 截图才不会 404。 - 脱敏说明:主目录路径里的用户名改为
you,分享链接的内网 IP、端口与 32 位 token 换成示例值。
v0.1.3: 同机导入时 bootstrap 不再误报失败
0.1.3 — 2026-10-18
修一个只在「同机同主目录」导入时才会触发的崩溃:安装其实已经成功,但 bootstrap CLI 在打印结果时把 null 当对象读,直接抛 TypeError。无 API/包格式变更,dsh-portable/1 与 0.1.x 互通。
修复
- 同机导入(源 home 与目标 home 相同)时 bootstrap 装完就崩:
lib/installer.mjs只在「包内记录的源 home ≠ 本机 home」时才做路径重写,其余情况把report.rewrites留成null;而bootstrap/bootstrap.mjs无条件读result.rewrites.files.length,于是在文件已全部落盘之后抛TypeError: Cannot read properties of null (reading 'files'),让用户以为导入失败。现在改为区分两种情况打印:有重写时报「N 个文件 M 处路径」,没有时明确说none (package was already built for <home>)。
测试
- 新增 1 项:
test/installer.test.mjs用同一主目录既导出又导入,断言report.rewrites === null且文件照常落地(回归上面那个崩溃)。全量 43 项。
验证
- 空机器全链路手测:在临时 home 里造「源机器」目录树并打包 → 用一个完全空白的新 home 跑
node bootstrap/bootstrap.mjs install <pkg> --home <新home> --profile desktop --yes→ 输出applied AGENTS.md, hooks, profiles、rewrites 3 files, 1 paths;新 home 里profiles/desktop/node_modules/dsh-portable/index.js就位、cordis.patch.yml里的旧 home 全部换成本机 home。 - 无 Node 的机器也能跑:
ELECTRON_RUN_AS_NODE=1+DeepSeek Harness.exe可当 Node 24.21.0 用,实测能执行bootstrap.mjs --help(exit 0)。 - 插件自身可从包里捞出来再跑:
tar -xzf <pkg> -C <kit> profiles/<p>/node_modules/dsh-portable抽出插件(含bootstrap/install.ps1),再用它导入同一个包。
v0.1.2: 修复包元信息读取与查看包报错
0.1.2 — 2026-10-18
修两个只在真机上才会撞到的读取缺陷:包内 manifest 被多包了一层,导致「查看包」和「包元信息」两个入口都拿不到 roots。无 API/包格式变更,dsh-portable/1 与 0.1.0/0.1.1 互通。
修复
portable_inspect与GET /artifacts/meta都无法列出包内根目录:readManifest()返回的是{ manifest, raw },两处调用点却把整个返回值当成 manifest 用,于是manifest.roots为undefined,模型工具直接抛Cannot read properties of undefined (reading 'map'),HTTP 入口则把raw(整份 manifest 原文)多包一层返回。现在两处都显式取.manifest,并让inspectPackage在缺少 manifest 时报可读错误(lib/tools.mjs、lib/routes.mjs)。- 元信息响应补
roots:GET /artifacts/meta现在除manifest外再返回拍平后的roots({id, logical, files, bytes}),与面板和模型工具用同一份形状。
测试
- 新增 2 项:
test/inspect.test.mjs用真包跑createTools().inspectPackage(断言 roots 是数组而不是 worker 包装对象)、test/routes.test.mjs用writePackage造真包验证/artifacts/meta。全量 42 项。
v0.1.1: 修复面板 404 提示与安装依赖登记
0.1.1 — 2026-10-18
修一个会把面板变成「能看不能点」的安装陷阱,并把那种状态下甩给用户的裸状态码换成人话。无 API 变更、无包格式变更,dsh-portable/1 与 0.1.0 互通。
修复
- 面板在宿主端未加载时只显示
HTTP 404:客户端 bundle 与宿主半边是分开加载的,宿主半边缺失时(插件没装完、被npm install清掉、或装好后还没重启 DSH)六个卡片照常渲染,但每个请求都拿 404,用户只看到一行红字。现在404提示「宿主端未加载:插件可能没装完,或者装好后还没重启 DSH」,403提示「请求被宿主拒绝:请在 DSH 窗口里打开这个面板」,其余状态码仍回落到HTTP <code>;文案随宿主语言切换(lib/client.js的describeFailure)。 - 安装文档漏了「必须登记为依赖」:把包目录手工拷进
profiles/<p>/node_modules也能跑,但该 profile 下一次npm install会把未声明的包当 extraneous 清掉,插件随后静默失效、面板只剩 404。README 双语在安装章节写明:用dsh plugin add或npm install写进dependencies,不要手工拷贝。
安全加固
- 错误提示只暴露宿主已经返回的状态码语义,不回显请求路径、包名或文件系统路径。