dsh-session-rollover 0.0.0-stage → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/LICENSE +201 -0
  3. package/LICENSE.upstream +21 -0
  4. package/NOTICE +9 -0
  5. package/README.md +157 -2
  6. package/assets/icon.png +0 -0
  7. package/assets/icon.svg +7 -0
  8. package/client.js +656 -0
  9. package/compatibility/policy.json +21 -0
  10. package/compatibility/runtime-0.2.2-rc.1.json +32 -0
  11. package/cordis.patch.yml +9 -0
  12. package/docs/COMPATIBILITY.md +70 -0
  13. package/docs/DETAIL.md +44 -0
  14. package/docs/DETAIL.zh-CN.md +49 -0
  15. package/docs/DEVELOPMENT.md +104 -0
  16. package/docs/PUBLISHING.md +80 -0
  17. package/docs/architecture.md +71 -0
  18. package/docs/live-host-validation.json +37 -0
  19. package/docs/marketplace-validation.md +22 -0
  20. package/docs/prototype-validation.json +10 -0
  21. package/docs/r3-demo.html +981 -0
  22. package/docs/r3-validation.md +59 -0
  23. package/docs/screenshots/help-dark.png +0 -0
  24. package/docs/screenshots/help-light.png +0 -0
  25. package/docs/screenshots/host-archived.png +0 -0
  26. package/docs/screenshots/host-dark.png +0 -0
  27. package/docs/screenshots/host-rollover.png +0 -0
  28. package/docs/screenshots/prototype-fixed.png +0 -0
  29. package/index.js +2 -0
  30. package/locale/en.json +7 -0
  31. package/locale/zh.json +7 -0
  32. package/marketplace/Oak-liuhj__dsh-session-rollover.yml +7 -0
  33. package/marketplace/SUBMISSION.md +37 -0
  34. package/package.json +103 -4
  35. package/screenshots.json +6 -0
  36. package/scripts/build-client.mjs +12 -0
  37. package/scripts/check-dsh-compatibility.mjs +44 -0
  38. package/scripts/check-package.mjs +51 -0
  39. package/scripts/check-release.mjs +76 -0
  40. package/scripts/dsh-contracts.mjs +69 -0
  41. package/src/application/handoff-message.mjs +53 -0
  42. package/src/application/rollover.mjs +236 -0
  43. package/src/assembly.mjs +4 -0
  44. package/src/client/rollover-ui.js +311 -0
  45. package/src/client/toast-client.js +342 -0
  46. package/src/cluster.mjs +3 -0
  47. package/src/domain/cluster-index.mjs +65 -0
  48. package/src/domain/handoff.mjs +67 -0
  49. package/src/domain/inflight.mjs +25 -0
  50. package/src/domain/mode.mjs +34 -0
  51. package/src/domain/naming.mjs +78 -0
  52. package/src/domain/summary.mjs +68 -0
  53. package/src/domain/todos.mjs +23 -0
  54. package/src/domain/turns.mjs +43 -0
  55. package/src/handoff.mjs +2 -0
  56. package/src/inflight.mjs +2 -0
  57. package/src/mode.mjs +2 -0
  58. package/src/naming.mjs +2 -0
  59. package/src/notices.mjs +2 -0
  60. package/src/server/compatibility.mjs +84 -0
  61. package/src/server/host-adapter.mjs +146 -0
  62. package/src/server/notice-route.mjs +104 -0
  63. package/src/server/plugin.mjs +447 -0
  64. package/src/storage/index-store.mjs +48 -0
  65. package/src/storage/notices.mjs +117 -0
  66. package/src/storage/rollover-state.mjs +75 -0
  67. package/src/summary.mjs +2 -0
  68. package/src/todos.mjs +2 -0
  69. package/src/turns.mjs +2 -0
@@ -0,0 +1,59 @@
1
+ # R3 修复与验证记录
2
+
3
+ 本轮按“先修分卷数据与状态一致性 → 补齐界面 → 调整样式”完成 1.1.0,基于原 c43b01d 的需求/页面分析落实。工作于 2026-11-06 开始,跨日完成验证。
4
+
5
+ ## 已修复的问题
6
+
7
+ | 领域 | 修复后的行为 | 证据 |
8
+ | --- | --- | --- |
9
+ | 轮次/时机 | 按真实用户消息计数,排除宿主内部上下文;第十条消息回合结束后仅分一次 | 服务端回归、真实宿主日志与 UI |
10
+ | marker/换段 | system Handoff 和内部上下文不误降 manual;forceAuto 仍等待压缩结束 | 来源/压缩互斥回归 |
11
+ | 快照 | todos/jobs 同刻捕获,人读与 JSON 一致;多 active、原文约束、文件线索保留 | 接线回归、真实首条卡片/剪贴板逐字段比较 |
12
+ | 预算/失败 | 1500 字符及完整首条消息 16 KiB 硬闸;初次+两次重试持久化;建卷前存储预检 | 超限、重启、目录碰撞回归 |
13
+ | 卷序/命名 | 真卷序与重名后缀分离;用户标题保留;超 9 名称后缀不嵌套;第九卷可转独立新簇 | 命名与上限回归 |
14
+ | 恢复 | 索引丢失/损坏/合法但缺项均可补齐;废弃卷不参与重建;显式恢复写入不被覆盖 | 多种重启夹具 |
15
+ | 提交后异常 | 辅助状态写盘异常不会把成功翻成失败后再次创建;degraded 可见 | 异常注入回归 |
16
+ | 模型 | 继承失败通过公开默认模型服务回落并提示 | 接线夹具;真实宿主正常继承 |
17
+ | UI | 标题状态、列表徽标、会话操作、首条摘要、JSON 展开/复制、来源导航、归档开关 | 真实宿主浏览器 |
18
+ | 输入保护 | 公开 conversation.blocks 禁用输入;允许写入/恢复只读双向生效、刷新持久化 | 浏览器 contenteditable/ARIA 检查、阻塞释放回归 |
19
+ | 样式 | 摘要三行预览/展开、长文本、原生 dialog、防裁切、深色主题、旧宿主窄徽标 | 浏览器与截图复核 |
20
+ | 文档/入口 | README/架构说明更新;生成式 client;保留根入口与旧子路径 | 构建检查、包导出测试与实际打包导入 |
21
+
22
+ 旧版只覆盖提示的前端已扩展为独立 `src/client/`;保持根 client.js 的宿主加载方式。未改宿主核心,也未修改相邻 R2 仓库源码。
23
+
24
+ ## 自动化验证
25
+
26
+ - Node.js 24.22.1 内置测试:**86/86 通过**;旧基线 67 项基础上增加 19 项回归。support 模块不计入测试数。
27
+ - `npm run build:check` 检查生成入口与源文件一致。
28
+ - 实际 `npm pack` 后解压,**28 个入口/源码模块导入通过**,必需发布文件齐全,清单不含运行 profile、认证信息或测试产物。
29
+ - 原型浏览器:**14 项检查通过**,包含多入口回溯、双向只读/刷新、摘要/JSON 展开和复制、展开后滚动、快照不随任务续跑变化、37% 进度一致及重置。
30
+
31
+ 机器结果:[真实宿主](./live-host-validation.json)、[原型](./prototype-validation.json)。打包与单元测试应在发布前再次使用上方命令复验。
32
+
33
+ ## 真实宿主验证范围
34
+
35
+ 使用本机已安装的 DeepSeek Harness,以独立 `DSH_HOME` 和 `r3-review` Web profile 启动,旁挂 R2/R3;模型请求接入本地 OpenAI 兼容夹具,不使用生产密钥。
36
+
37
+ 实际过程发送十条用户消息,其中宿主还产生多条 `runtime-context` / `skill-catalog` / `time-context` user/message;统计结果仍为旧卷 10、新卷 0,只产生一个接续卷。新卷首条交接含 8 项待办、当前第 4 步,夹具通过真实 `todo_write` 完成第 4 步并启动第 5 步,右侧 R2 更新到 4/8、50%;历史 JSON 保持第 4 步时的原始快照。
38
+
39
+ 后续在同一对已生成会话上继续浏览器复验,并重启宿主复验恢复:卡片/剪贴板与后端逐字段一致、来源跳转留在旧卷、readonly/可写双向设置跨刷新、contenteditable 真正禁用、旧宿主 leading 徽标回退,以及通过系统色彩偏好触发的宿主真实深色主题。最终浏览器记录无页面/控制台错误和失败资源请求。
40
+
41
+ 中途测试暴露并修复了两个夹具没有覆盖的宿主差异:内部 user/message 来源不能当人类发言;输入组件子槽不能被普通包装/重复声明。因此最终改用公开 `conversation.blocks`,并为释放与其他阻塞原因共存增加测试。
42
+
43
+ ![真实宿主分卷与 R2 续接](./screenshots/host-rollover.png)
44
+
45
+ ![归档只读](./screenshots/host-archived.png)
46
+
47
+ ![真实深色主题](./screenshots/host-dark.png)
48
+
49
+ ## 原型同步
50
+
51
+ 仓库 [r3-demo.html](./r3-demo.html) 修复原 R3 页面的演示状态:统一旧卷 ID、多入口共享状态、双向只读持久化、真实摘要/JSON 展开、全部继承任务来源标识、任务续跑不篡改历史、清除状态后重置。3/8 进度与当前 R2 `Math.floor` 口径统一为 37%。旧八段注入默认关闭,仅作为兼容示意。
52
+
53
+ 用户原路径的 HTML 已同步为验证版,覆盖前比对原始哈希并保留同目录 `v0.1.before-r3-fix.html` 备份。原型仍是固定数据、单次分卷演示,不作为真实模型运行证据。
54
+
55
+ ## 验收边界
56
+
57
+ 本轮已完成代码修复、隔离宿主 Web 与固定模型工具接续验证;未据此重新宣称全部历史 AC 或 Bot 群端验收通过。生产模型输出质量、真实后台进程跨卷运行及 Bot 群消息端仍需在对应目标环境验收;后台任务当前仅传句柄/状态,不迁移进程。
58
+
59
+ 宿主会话和多个 JSON 文件不是原子数据库事务;存储异常已预检、补偿、恢复并暴露降级,但不能保证断电/多文件同时损坏时零损失。索引恢复依赖宿主仍保留交接消息和有效来源。测试进程已使用独立 profile,用户默认 profile/凭据未参与修改。
Binary file
Binary file
Binary file
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // 稳定的服务端包入口;实现位于 server 层,默认与具名导出保持兼容。
2
+ export {default, SessionRolloverPlugin} from './src/server/plugin.mjs';
package/locale/en.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-session-rollover",
4
+ "description": "Split long conversations into volumes with todo handoff snapshots, source navigation and read-only archives in DSH Web and desktop.",
5
+ "detail": "docs/DETAIL.md"
6
+ }
7
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "meta": {
3
+ "title": "dsh-session-rollover",
4
+ "description": "长会话自动分卷,携带待办交接快照继续工作;支持来源回溯、归档只读和 Web / 桌面客户端。",
5
+ "detail": "docs/DETAIL.zh-CN.md"
6
+ }
7
+ }
@@ -0,0 +1,7 @@
1
+ url: https://github.com/Oak-liuhj/dsh-session-rollover
2
+ name: Oak-liuhj/dsh-session-rollover
3
+ category: session
4
+ description:
5
+ en: 'Split long DSH conversations into volumes with todo handoff snapshots, source navigation and read-only archives.'
6
+ zh: '长会话自动分卷,携带待办交接快照继续工作,支持来源回溯与归档只读。'
7
+ icon: https://raw.githubusercontent.com/Oak-liuhj/dsh-session-rollover/main/assets/icon.png
@@ -0,0 +1,37 @@
1
+ # 市场投稿材料
2
+
3
+ ## 投稿记录
4
+
5
+ 2026-11-09 已提交首次上架 PR:[awesome-dsh-plugin/awesome-dsh-plugin#13453](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/pull/13453)。
6
+
7
+ - 插件版本:`1.1.1`,投稿时已发布源码提交为 `541321a91e77942ba23bcc19267b2d5a8a79ccbf`。
8
+ - 投稿分支:`Oak-liuhj:codex/add-dsh-session-rollover`。
9
+ - 唯一新增文件:`data/plugins/Oak-liuhj__dsh-session-rollover.yml`。
10
+ - 校验与审核的实时状态以 PR 页面为准。提交 PR 不等于市场已收录;正式收录还需维护者合并及目录后续构建。
11
+
12
+ ## npm 发布记录
13
+
14
+ 2026-11-11 **13:51(北京时间)**,`dsh-session-rollover@1.1.1` 已正式发布到 [npm](https://www.npmjs.com/package/dsh-session-rollover/v/1.1.1)。npm 发布账号为 `oak-ai`,插件作者与 GitHub 仓库所有者为 `Oak-liuhj`。
15
+
16
+ - npm 官方源与腾讯镜像的 `latest` 均已确认为 `1.1.1`。
17
+ - 下载的 tgz 与审核通过的原包一致,SHA1 为 `cbe802fe44f8ae14da3eebc8d6a92e3468609ab5`,SHA512 integrity 也已核对。
18
+ - 两个源均已通过按包名隔离安装;已验证服务端入口导入、挂载补丁、预构建客户端、图标、双语资源及许可证文件。
19
+ - 用户可在「填入插件 npm 包名」中输入 `dsh-session-rollover`;CLI 桌面 profile 示例为 `dsh plugin --profile desktop add dsh-session-rollover`。
20
+
21
+ 本次验证未改动生产 DSH profile;正式版本发布与市场目录收录分别核实,npm 的 `0.0.0-stage` 占位版本不作为发布成功依据。
22
+
23
+ ## 投稿规则与材料
24
+
25
+ 投稿目录为 [awesome-dsh-plugin/awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin),市场应用代码仓库 `dsh-market/dsh-market` 不是插件条目的投稿目标。规则来源:[contributing.md](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin/blob/main/contributing.md)(核对于 2026-11-07)。
26
+
27
+ 仅新增 `data/plugins/Oak-liuhj__dsh-session-rollover.yml`,内容使用本目录同名文件;不手改自动生成的 README,不增加 npm/version/tags 等非目录字段。
28
+
29
+ 建议 PR 标题:`Add Oak-liuhj/dsh-session-rollover`
30
+
31
+ 建议 PR 正文:
32
+
33
+ > Adds a community DSH session plugin that splits long conversations at safe turn boundaries, carries todo handoff snapshots and provides source navigation and read-only archives. The repository includes a DSH bundle patch, prebuilt client, English/Chinese metadata and details, a PNG icon, screenshots and both license notices. Verified against DSH 0.2.1-rc.5 with automated regression tests and an isolated local-model fixture; Bot group channels are not fully validated.
34
+
35
+ 提交前确认公开 main 分支或安装目标包含 `client.js`、图标和截图;仓库 topic 包含 `dsh-plugin`,创建满一天。仓库创建时间核对为 2026-11-06 06:45:34 UTC,最早满一天时间为北京时间 **2026-11-07 14:45:34**。npm 发布可选,未发布 npm 时使用 GitHub 安装源。
36
+
37
+ 本目录的 YAML 已通过上述 PR 提交。市场收录、npm 发布和 GitHub Release 是独立状态;首次市场投稿未包含 npm 发布,后续 npm 发布见上方记录。本记录不表示已创建 GitHub Release。
package/package.json CHANGED
@@ -1,6 +1,105 @@
1
1
  {
2
2
  "name": "dsh-session-rollover",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "description": "DSH 会话分卷与续接:自动分卷、待办交接快照、来源回溯和归档只读,支持 Web 与桌面客户端。",
6
+ "main": "index.js",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./client": "./client.js",
10
+ "./src/*": "./src/*",
11
+ "./package.json": "./package.json",
12
+ "./cordis.patch.yml": "./cordis.patch.yml",
13
+ "./locale/*.json": "./locale/*.json",
14
+ "./icon": "./assets/icon.png"
15
+ },
16
+ "files": [
17
+ "index.js",
18
+ "client.js",
19
+ "src/",
20
+ "scripts/",
21
+ "assets/",
22
+ "locale/",
23
+ "screenshots.json",
24
+ "cordis.patch.yml",
25
+ "README.md",
26
+ "CHANGELOG.md",
27
+ "NOTICE",
28
+ "LICENSE",
29
+ "LICENSE.upstream",
30
+ "docs/DETAIL.md",
31
+ "docs/DETAIL.zh-CN.md",
32
+ "docs/PUBLISHING.md",
33
+ "docs/architecture.md",
34
+ "docs/r3-validation.md",
35
+ "docs/marketplace-validation.md",
36
+ "docs/screenshots/host-rollover.png",
37
+ "docs/screenshots/host-archived.png",
38
+ "docs/screenshots/host-dark.png",
39
+ "docs/screenshots/help-light.png",
40
+ "docs/screenshots/help-dark.png",
41
+ "marketplace/",
42
+ "compatibility/",
43
+ "docs/COMPATIBILITY.md",
44
+ "docs/r3-demo.html",
45
+ "docs/screenshots/prototype-fixed.png",
46
+ "docs/live-host-validation.json",
47
+ "docs/prototype-validation.json",
48
+ "docs/DEVELOPMENT.md"
49
+ ],
50
+ "scripts": {
51
+ "build": "node scripts/build-client.mjs",
52
+ "build:check": "node scripts/build-client.mjs --check",
53
+ "pretest": "npm run build",
54
+ "prepack": "npm run build && node scripts/check-release.mjs",
55
+ "test": "node --test \"test/**/*.test.mjs\"",
56
+ "check:release": "npm run build:check && node scripts/check-release.mjs",
57
+ "check:dsh": "node scripts/check-dsh-compatibility.mjs",
58
+ "check:package": "npm run check:release && node scripts/check-package.mjs"
59
+ },
60
+ "dsh": {
61
+ "manifestVersion": 1,
62
+ "verifiedDsh": "0.2.1-rc.5",
63
+ "bundle": {
64
+ "patch": "./cordis.patch.yml"
65
+ },
66
+ "client": {
67
+ "platform": "web",
68
+ "inject": [
69
+ "@deepseek-ai/dsh-client-ui-conversation",
70
+ "@deepseek-ai/dsh-client-ui-workspace"
71
+ ]
72
+ }
73
+ },
74
+ "dsBot": {
75
+ "min": "0.2.6",
76
+ "verified": "0.2.6"
77
+ },
78
+ "license": "(Apache-2.0 AND MIT)",
79
+ "author": "Oak-liuhj",
80
+ "repository": {
81
+ "type": "git",
82
+ "url": "git+https://github.com/Oak-liuhj/dsh-session-rollover.git"
83
+ },
84
+ "homepage": "https://github.com/Oak-liuhj/dsh-session-rollover#readme",
85
+ "bugs": {
86
+ "url": "https://github.com/Oak-liuhj/dsh-session-rollover/issues"
87
+ },
88
+ "keywords": [
89
+ "dsh-plugin",
90
+ "deepseek-harness",
91
+ "dsh",
92
+ "session",
93
+ "rollover",
94
+ "handoff",
95
+ "productivity"
96
+ ],
97
+ "icon": "assets/icon.png",
98
+ "publishConfig": {
99
+ "access": "public",
100
+ "registry": "https://registry.npmjs.org/"
101
+ },
102
+ "engines": {
103
+ "dsh": "0.2.1-rc.5 || 0.2.2-rc.1"
104
+ }
105
+ }
@@ -0,0 +1,6 @@
1
+ [
2
+ "docs/screenshots/host-rollover.png",
3
+ "docs/screenshots/host-archived.png",
4
+ "docs/screenshots/host-dark.png",
5
+ "docs/screenshots/help-light.png"
6
+ ]
@@ -0,0 +1,12 @@
1
+ /** 无构建依赖:将独立 UI factory 嵌入 ModuleLoader 模板,保留既有 toast API。 */
2
+ import {readFileSync, writeFileSync} from 'node:fs';
3
+ const root = new URL('../', import.meta.url);
4
+ const template = readFileSync(new URL('src/client/toast-client.js', root), 'utf8');
5
+ const ui = readFileSync(new URL('src/client/rollover-ui.js', root), 'utf8').replace('export function createRolloverUI', 'function createRolloverUI');
6
+ const {version} = JSON.parse(readFileSync(new URL('package.json', root), 'utf8'));
7
+ const output = `// Generated by npm run build; edit src/client/. Version ${version}.\n` + template.replace(' function apply(ctx) {',
8
+ ui + '\n function apply(ctx) {\n try { createRolloverUI(require("react")).apply(ctx); } catch (e) { debug(e); }');
9
+ const path = new URL('client.js', root);
10
+ if (process.argv.includes('--check')) {
11
+ if (readFileSync(path, 'utf8') !== output) throw new Error('client.js is stale; run npm run build');
12
+ } else writeFileSync(path, output);
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import {tmpdir} from 'node:os';
4
+ import {registryJSON, publishedTargets, checkPackage} from './dsh-contracts.mjs';
5
+ const policy = JSON.parse(await fs.readFile(new URL('../compatibility/policy.json', import.meta.url), 'utf8'));
6
+ const args = process.argv.slice(2);
7
+ const list = args.includes('--list');
8
+ const target = args.find(value => !value.startsWith('--')) ?? 'latest';
9
+ const metadata = await registryJSON(policy.registry + encodeURIComponent(policy.package));
10
+ const versions = publishedTargets(metadata, policy.baseline);
11
+ if (!versions.length || versions.length > 200) throw new Error('DSH compatibility matrix requires baseline maintenance');
12
+ if (list) {
13
+ // 超出 GitHub matrix 上限时明确失败,要求维护者调整基线,不静默漏掉新版本。
14
+ console.log(JSON.stringify({version: versions}));
15
+ } else {
16
+ const selected = target === 'all' ? versions : [metadata['dist-tags']?.[target] ?? target];
17
+ const directory = await fs.mkdtemp(path.join(tmpdir(), 'dsh-contracts-'));
18
+ const report = {level: 'public-declaration-smoke', runtimeVerified: false,
19
+ checkedAt: new Date().toISOString(), baseline: policy.baseline, targets: []};
20
+ try {
21
+ for (const version of selected) {
22
+ const results = [];
23
+ for (const contract of policy.contracts) {
24
+ try { results.push(await checkPackage(contract, version, policy, directory)); }
25
+ catch (error) { results.push({package: contract.package, version, error: String(error.message)}); }
26
+ }
27
+ const passed = results.every(result => !result.error && !result.missing?.length);
28
+ report.targets.push({version, passed, packages: results});
29
+ console.log(`${version}: ${passed ? 'PASS' : 'FAIL'} (public declarations; not runtime verification)`);
30
+ for (const result of results.filter(r => r.error || r.missing?.length)) console.error(JSON.stringify(result));
31
+ }
32
+ report.passed = report.targets.every(target => target.passed);
33
+ const output = process.env.DSH_COMPATIBILITY_REPORT ?? path.resolve('out/dsh-compatibility.json');
34
+ await fs.mkdir(path.dirname(output), {recursive: true});
35
+ await fs.writeFile(output, JSON.stringify(report, null, 2) + '\n');
36
+ if (!report.passed) process.exitCode = 1;
37
+ } finally {
38
+ // 只删除 mkdtemp 创建并持有的独立目录。
39
+ if (path.dirname(path.resolve(directory)) !== path.resolve(tmpdir()) || !path.basename(directory).startsWith('dsh-contracts-')) {
40
+ throw new Error('Refusing cleanup outside the owned temporary directory');
41
+ }
42
+ await fs.rm(directory, {recursive: true, force: true});
43
+ }
44
+ }
@@ -0,0 +1,51 @@
1
+ /** 校验真正交付的 tgz:隔离安装、稳定导出、清单和预构建浏览器入口;不接触 DSH profile。 */
2
+ import assert from 'node:assert/strict';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import {tmpdir} from 'node:os';
6
+ import {execFile} from 'node:child_process';
7
+ import {promisify} from 'node:util';
8
+ import {pathToFileURL, fileURLToPath} from 'node:url';
9
+ import {createHash} from 'node:crypto';
10
+ const run = promisify(execFile);
11
+ const root = fileURLToPath(new URL('../', import.meta.url));
12
+ const npm = process.env.npm_execpath;
13
+ assert.ok(npm && path.basename(npm) === 'npm-cli.js', 'Run this check with npm run check:package');
14
+ const owned = await fs.mkdtemp(path.join(tmpdir(), 'dsh-rollover-package-'));
15
+ const output = path.join(root, 'out');
16
+ try {
17
+ // 调用方已完成 build:check 和发布检查;这里禁用生命周期,确保检查同一份构建产物。
18
+ const packed = JSON.parse((await run(process.execPath, [npm, 'pack', '--json', '--ignore-scripts', '--pack-destination', owned],
19
+ {cwd: root, windowsHide: true, maxBuffer: 4 * 1024 * 1024})).stdout)[0];
20
+ const archive = path.join(owned, path.basename(packed.filename));
21
+ const install = path.join(owned, 'install');
22
+ await fs.mkdir(install);
23
+ await fs.writeFile(path.join(install, 'package.json'), '{"name":"dsh-rollover-package-check","private":true}\n');
24
+ await run(process.execPath, [npm, 'install', archive, '--ignore-scripts', '--no-audit', '--no-fund', '--package-lock=false'],
25
+ {cwd: install, windowsHide: true, maxBuffer: 4 * 1024 * 1024});
26
+ const installed = path.join(install, 'node_modules', 'dsh-session-rollover');
27
+ const manifest = JSON.parse(await fs.readFile(path.join(installed, 'package.json'), 'utf8'));
28
+ const entry = await import(pathToFileURL(path.join(installed, manifest.main)).href);
29
+ assert.equal(entry.default, entry.SessionRolloverPlugin);
30
+ assert.equal(typeof entry.default, 'function');
31
+ const files = packed.files.map(file => file.path);
32
+ for (const required of ['client.js', 'src/server/compatibility.mjs', 'compatibility/policy.json', 'docs/COMPATIBILITY.md']) {
33
+ assert.ok(files.includes(required), `Missing packed resource: ${required}`);
34
+ }
35
+ // 从安装后的目录重新校验全部面板资源和 ModuleLoader,避免只检查源码目录。
36
+ await run(process.execPath, [path.join(installed, 'scripts/check-release.mjs')], {cwd: installed, windowsHide: true});
37
+ const bytes = await fs.readFile(archive);
38
+ await fs.mkdir(output, {recursive: true});
39
+ await fs.copyFile(archive, path.join(output, packed.filename));
40
+ const report = {passed: true, level: 'isolated-package-install', runtimeVerified: false,
41
+ checkedAt: new Date().toISOString(), platform: process.platform, node: process.version,
42
+ name: manifest.name, version: manifest.version, files: files.length,
43
+ sha256: createHash('sha256').update(bytes).digest('hex'), artifact: packed.filename};
44
+ await fs.writeFile(path.join(output, 'package-verification.json'), JSON.stringify(report, null, 2) + '\n');
45
+ console.log(`Package PASS: ${manifest.name}@${manifest.version}; isolated install, exports, client and resources (${files.length} files).`);
46
+ } finally {
47
+ if (path.dirname(path.resolve(owned)) !== path.resolve(tmpdir()) || !path.basename(owned).startsWith('dsh-rollover-package-')) {
48
+ throw new Error('Refusing cleanup outside the owned temporary directory');
49
+ }
50
+ await fs.rm(owned, {recursive: true, force: true});
51
+ }
@@ -0,0 +1,76 @@
1
+ /** 只读离线发布校验。YAML 校验限定为本项目固定模板,不作为通用解析器。 */
2
+ import assert from 'node:assert/strict';
3
+ import {readFile, stat, realpath} from 'node:fs/promises';
4
+ import {fileURLToPath, pathToFileURL} from 'node:url';
5
+ import path from 'node:path';
6
+ import vm from 'node:vm';
7
+
8
+ const root = await realpath(fileURLToPath(new URL('../', import.meta.url)));
9
+ const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
10
+ const repo = 'https://github.com/Oak-liuhj/dsh-session-rollover';
11
+ async function localFile(relative) {
12
+ assert.equal(typeof relative, 'string');
13
+ assert.ok(!path.isAbsolute(relative) && !relative.split(/[\\/]/).includes('..'), `Unsafe resource: ${relative}`);
14
+ const file = await realpath(path.resolve(root, relative));
15
+ assert.ok(file.startsWith(root + path.sep), `Resource escapes package: ${relative}`);
16
+ assert.ok((await stat(file)).isFile());
17
+ return file;
18
+ }
19
+ assert.equal(pkg.name, 'dsh-session-rollover');
20
+ assert.match(pkg.version, /^\d+\.\d+\.\d+$/);
21
+ assert.equal(pkg.author, 'Oak-liuhj');
22
+ assert.equal(pkg.license, '(Apache-2.0 AND MIT)');
23
+ assert.equal(pkg.repository.url, 'git+' + repo + '.git');
24
+ assert.equal(pkg.bugs.url, repo + '/issues');
25
+ assert.ok(pkg.keywords.includes('dsh-plugin'));
26
+ assert.equal(pkg.dsh.manifestVersion, 1);
27
+ assert.equal(pkg.dsh.client.platform, 'web');
28
+ const compatibility = JSON.parse(await readFile(await localFile('compatibility/policy.json'), 'utf8'));
29
+ assert.equal(compatibility.baseline, pkg.dsh.verifiedDsh);
30
+ assert.ok(compatibility.verifiedRuntimeVersions.includes(compatibility.baseline));
31
+ assert.ok(compatibility.verifiedRuntimeVersions.every(version => /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version)));
32
+ assert.equal(pkg.engines.dsh, compatibility.verifiedRuntimeVersions.join(' || '));
33
+ for (const version of compatibility.verifiedRuntimeVersions.filter(version => version !== compatibility.baseline)) {
34
+ const evidence = JSON.parse(await readFile(await localFile(`compatibility/runtime-${version}.json`), 'utf8'));
35
+ assert.equal(evidence.passed, true);
36
+ assert.equal(evidence.runtimeVerified, true);
37
+ assert.equal(evidence.dsh, version);
38
+ assert.equal(evidence.plugin, pkg.version);
39
+ }
40
+ await localFile('docs/COMPATIBILITY.md');
41
+ assert.deepEqual(Object.keys(pkg.dependencies ?? {}), []);
42
+ assert.equal(pkg.scripts.prepare, undefined);
43
+ assert.equal(pkg.scripts.postinstall, undefined);
44
+
45
+ const patch = await readFile(await localFile(pkg.dsh.bundle.patch), 'utf8');
46
+ assert.match(patch, /- insert:\s+\n?\s*- id: session-rollover\s+name: 'dsh-session-rollover'/);
47
+ for (const lang of ['zh', 'en']) {
48
+ const {meta} = JSON.parse(await readFile(await localFile(pkg.exports['./locale/*.json'].replace('*', lang)), 'utf8'));
49
+ for (const key of ['title', 'description', 'detail']) assert.ok(meta[key]?.trim());
50
+ await localFile(meta.detail);
51
+ }
52
+ const icon = await readFile(await localFile(pkg.icon));
53
+ assert.deepEqual([...icon.subarray(0, 8)], [137, 80, 78, 71, 13, 10, 26, 10]);
54
+ assert.ok(icon.length <= 256 * 1024);
55
+ assert.equal(icon.readUInt32BE(16), icon.readUInt32BE(20));
56
+ const shots = JSON.parse(await readFile(await localFile('screenshots.json'), 'utf8'));
57
+ assert.ok(Array.isArray(shots) && shots.length >= 1 && shots.length <= 8);
58
+ for (const shot of shots) { assert.match(shot, /\.(png|jpe?g|webp)$/i); await localFile(shot); }
59
+
60
+ const entry = await readFile(await localFile('marketplace/Oak-liuhj__dsh-session-rollover.yml'), 'utf8');
61
+ assert.deepEqual(entry.split(/\r?\n/).filter(line => /^\w/.test(line)).map(line => line.split(':')[0]).sort(),
62
+ ['category', 'description', 'icon', 'name', 'url']);
63
+ for (const line of [`url: ${repo}`, 'name: Oak-liuhj/dsh-session-rollover', 'category: session',
64
+ `icon: https://raw.githubusercontent.com/Oak-liuhj/dsh-session-rollover/main/${pkg.icon}`]) assert.ok(entry.split(/\r?\n/).includes(line));
65
+ assert.match(entry, /^ en: '[^'\n]+\.'$/m);
66
+ assert.match(entry, /^ zh: '[^'\n]+。'$/m);
67
+ for (const file of ['README.md', 'CHANGELOG.md', 'LICENSE', 'LICENSE.upstream', 'NOTICE']) await localFile(file);
68
+ const server = await import(pathToFileURL(await localFile(pkg.main)).href);
69
+ assert.equal(typeof server.default, 'function');
70
+ let moduleInfo;
71
+ const client = await readFile(await localFile('client.js'), 'utf8');
72
+ vm.runInNewContext(client, {window: {__ModuleLoader__: {load: value => { moduleInfo = value; }}}});
73
+ assert.equal(moduleInfo.id, pkg.name, 'Client ModuleLoader id must match public package');
74
+ assert.equal(typeof moduleInfo.factory, 'function');
75
+ assert.ok(client.includes(`Version ${pkg.version}.`));
76
+ console.log(`Release format PASS: ${pkg.name}@${pkg.version}; bundle, locales, PNG, ${shots.length} screenshots, catalog and runtime entries.`);
@@ -0,0 +1,69 @@
1
+ /** 检查 npm 公共声明中的依赖面;不执行上游包,不把声明扫描当作真实宿主验收。 */
2
+ import assert from 'node:assert/strict';
3
+ import {createHash} from 'node:crypto';
4
+ import {execFile} from 'node:child_process';
5
+ import {promisify} from 'node:util';
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ const run = promisify(execFile);
9
+
10
+ export async function registryJSON(url) {
11
+ const response = await fetch(url, {signal: AbortSignal.timeout(30000)});
12
+ assert.equal(response.status, 200, `Registry unavailable: ${url} (${response.status})`);
13
+ return response.json();
14
+ }
15
+
16
+ /** 按官方发布时刻枚举基线之后的每个版本,包含 alpha/rc 和后续补丁。 */
17
+ export function publishedTargets(metadata, baseline) {
18
+ const since = Date.parse(metadata.time?.[baseline]);
19
+ assert.ok(Number.isFinite(since), 'Missing baseline publication time');
20
+ const versions = Object.keys(metadata.versions ?? {});
21
+ assert.ok(versions.includes(baseline), 'Missing baseline package version');
22
+ for (const version of versions) {
23
+ assert.match(version, /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/);
24
+ assert.ok(Number.isFinite(Date.parse(metadata.time?.[version])), `Missing publication time: ${version}`);
25
+ }
26
+ return versions.filter(version => Date.parse(metadata.time?.[version]) >= since)
27
+ .sort((a, b) => Date.parse(metadata.time[a]) - Date.parse(metadata.time[b]));
28
+ }
29
+
30
+ export function inspectDeclarations(text, contract) {
31
+ // 去除注释,避免已删除的方法只在弃用说明中出现时误判通过。
32
+ const source = text.replace(/\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, '');
33
+ const missing = [];
34
+ for (const method of contract.methods ?? []) {
35
+ assert.match(method, /^\w+$/);
36
+ if (!new RegExp(`\\b${method}\\s*(?:<[^;{}]*?>\\s*)?\\(`).test(source)) missing.push(method + '()');
37
+ }
38
+ for (const token of contract.tokens ?? []) if (!source.includes(token)) missing.push(token);
39
+ for (const group of contract.alternatives ?? []) {
40
+ if (!group.some(token => source.includes(token))) missing.push(group.join(' | '));
41
+ }
42
+ return missing;
43
+ }
44
+
45
+ export async function checkPackage(contract, version, policy, directory) {
46
+ const metadata = await registryJSON(policy.registry + encodeURIComponent(contract.package) + '/' + encodeURIComponent(version));
47
+ assert.equal(metadata.version, version);
48
+ assert.equal(new URL(metadata.dist.tarball).hostname, new URL(policy.registry).hostname);
49
+ assert.match(metadata.dist.integrity, /^sha512-[A-Za-z0-9+/=]+$/);
50
+ const response = await fetch(metadata.dist.tarball, {signal: AbortSignal.timeout(30000)});
51
+ assert.equal(response.status, 200);
52
+ const archive = Buffer.from(await response.arrayBuffer());
53
+ assert.equal('sha512-' + createHash('sha512').update(archive).digest('base64'), metadata.dist.integrity);
54
+ const file = path.join(directory, contract.package.replace(/[^\w-]/g, '_') + '.tgz');
55
+ await fs.writeFile(file, archive);
56
+ const {stdout} = await run('tar', ['-tzf', file], {windowsHide: true, maxBuffer: 4 * 1024 * 1024});
57
+ const declarations = stdout.split(/\r?\n/).filter(name => /^package\/lib\/.*\.d\.ts$/.test(name));
58
+ assert.ok(declarations.length > 0, 'Missing public type declarations');
59
+ let text = '';
60
+ for (let offset = 0; offset < declarations.length; offset += 40) {
61
+ const names = declarations.slice(offset, offset + 40);
62
+ assert.ok(names.every(name => !name.split('/').includes('..') && !name.includes('\\')));
63
+ // 只读 tar stdout,不把上游归档解压到任何宿主目录。
64
+ text += (await run('tar', ['-xOf', file, ...names], {windowsHide: true, maxBuffer: 16 * 1024 * 1024})).stdout + '\n';
65
+ }
66
+ return {package: contract.package, version, integrity: metadata.dist.integrity,
67
+ declarations: declarations.length,
68
+ missing: inspectDeclarations(text, {...contract, ...contract.versionOverrides?.[version]})};
69
+ }
@@ -0,0 +1,53 @@
1
+ // 交接消息排版与模式检测输入转换;不访问宿主或文件系统。
2
+ import {todosMarkdown} from '../domain/handoff.mjs';
3
+ import {textOf} from '../domain/summary.mjs';
4
+
5
+ /** 事件 → 分段检测用的 {text} 列表(只读文本,不猜结构)。
6
+ * 宿主真实事件把用户正文放在 data.content[].text(user/message、assistant/message),
7
+ * 只有测试夹具用扁平 {text};三种形态都读,否则 markers 在真机恒 false(AC-R3-9 实拍发现的缺陷)。 */
8
+ export function eventsToMessages(events) {
9
+ return events.map((e) => {
10
+ const type = e.type ?? e.kind;
11
+ const source = e.data?.source ?? e.source;
12
+ return {type, text: textOf(e), internal: textOf(e).startsWith('【分卷交接】') ||
13
+ (type === 'user/message' && !!source?.kind && source.kind !== 'user')};
14
+ });
15
+ }
16
+
17
+ /**
18
+ * 注入正文(新会话首条 user/message)。刻意不含 "[Checkpoint N]"/"[Handoff · part N]" 形态:
19
+ * 那是模式检测的降级信号,自己写进去会让下一轮 auto 误降 manual(S9 三路线要求的 auto 直通)。
20
+ * cluster_id / parent_session_id 两行是重建对账标记(rebuildFromTitles 的 markers 来源)。
21
+ */
22
+ export function renderHandoffMessage(payload, {volume = null, at = null} = {}) {
23
+ const lines = [];
24
+ lines.push(`【分卷交接】${volume ? `进入第 ${volume} 卷` : '进入新卷'} ← 会话 ${payload.from_session_id ?? '(未知)'}`);
25
+ lines.push('rollover: v1');
26
+ lines.push(`cluster_id: ${payload.cluster?.cluster_id ?? payload.cluster?.degraded_link ?? 'unknown'}`);
27
+ lines.push(`parent_session_id: ${payload.from_session_id ?? 'unknown'}`);
28
+ if (at) lines.push(`at: ${at}`);
29
+ lines.push('');
30
+ const segs = Object.entries(payload.summary);
31
+ const covered = (kw) => segs.some(([name]) => name.includes(kw)); // 八段摘要已覆盖的块不再重复贴
32
+ segs.forEach(([name, text], i) => {
33
+ lines.push(`## ${i + 1}. ${name}`, String(text), '');
34
+ });
35
+ lines.push('## 待办(旧卷最后一次投影)', todosMarkdown(Array.isArray(payload.todos) ? payload.todos : []), '');
36
+ const cs = payload.current_step;
37
+ if (cs && typeof cs === 'object') {
38
+ const items = Array.isArray(payload.todos) ? payload.todos : [];
39
+ const cur = items[cs.index - 1];
40
+ lines.push(`## 当前进度`, `第 ${cs.index} 步进行中${cur ? `:${cur.content}` : ''}`, '');
41
+ } else {
42
+ lines.push('## 当前进度', String(cs ?? '尚未开始'), '');
43
+ }
44
+ const jobs = payload.open_jobs ?? [];
45
+ if (!covered('在跑')) {
46
+ lines.push('## 在跑 / 未收尾任务', jobs.length ? jobs.map((j) => `- ${j.id} \`${j.command ?? ''}\` status=${j.status}${j.recoverable ? '' : '(需重跑)'}`).join('\n') : '无', '');
47
+ }
48
+ if (!covered('约束')) lines.push('## 约束(原文)', (payload.constraints ?? []).map((c) => `- ${c}`).join('\n'), '');
49
+ const files = payload.changed_files ?? [];
50
+ if (!covered('改动文件')) lines.push('## 改动文件', files.length ? files.map((f) => `- ${f.path}${f.deleted ? '(已删除)' : ''}${f.note ? ` — ${f.note}` : ''}`).join('\n') : '无', '');
51
+ lines.push('## 交接快照 JSON', '```json', JSON.stringify(payload), '```');
52
+ return lines.join('\n');
53
+ }