dsh-plugin-tool-management 0.9.1 → 0.11.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 (79) hide show
  1. package/CHANGELOG.md +119 -1
  2. package/README.md +227 -201
  3. package/README_EN.md +227 -199
  4. package/docs/images/1-EN.png +0 -0
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2-EN.png +0 -0
  7. package/docs/images/2.png +0 -0
  8. package/docs/images/3-EN.png +0 -0
  9. package/docs/images/3.png +0 -0
  10. package/docs/images/4-EN.png +0 -0
  11. package/docs/images/4.png +0 -0
  12. package/docs/images/5-EN.png +0 -0
  13. package/docs/images/5.png +0 -0
  14. package/docs/images/6-EN.png +0 -0
  15. package/docs/images/6.png +0 -0
  16. package/docs/images/7-EN.png +0 -0
  17. package/docs/images/7.png +0 -0
  18. package/docs/images/8-EN.png +0 -0
  19. package/docs/images/8.png +0 -0
  20. package/docs/update.md +144 -12
  21. package/lib/client.js +8437 -5929
  22. package/lib/compat/preset-reach.js +1 -10
  23. package/lib/compat/probe.js +158 -25
  24. package/lib/context-inject.js +396 -53
  25. package/lib/host-names.js +12 -0
  26. package/lib/http-fence.js +35 -15
  27. package/lib/hub.js +31 -3
  28. package/lib/imports/parsers.js +15 -9
  29. package/lib/imports/upload.js +43 -4
  30. package/lib/index.js +940 -3765
  31. package/lib/mcp/loader-token.js +238 -0
  32. package/lib/mcp/manager.js +1681 -0
  33. package/lib/mcp/override-blocks.js +20 -9
  34. package/lib/mcp/patch-yaml.js +351 -0
  35. package/lib/mcp/secret-guard.js +145 -0
  36. package/lib/mcp/state-section.js +64 -21
  37. package/lib/{rules → memories}/archive-engine.js +65 -10
  38. package/lib/{rules → memories}/archive.js +6 -7
  39. package/lib/memories/constants.js +128 -0
  40. package/lib/memories/index-io.js +330 -0
  41. package/lib/memories/projection.js +280 -0
  42. package/lib/memories/service.js +686 -0
  43. package/lib/memories/snapshot.js +672 -0
  44. package/lib/ops/candidates.js +64 -0
  45. package/lib/ops/compat.js +136 -0
  46. package/lib/ops/ctx.js +9 -0
  47. package/lib/ops/memory.js +678 -0
  48. package/lib/ops/prompts.js +107 -0
  49. package/lib/ops/scene-records.js +460 -0
  50. package/lib/ops/scene-sync.js +17 -0
  51. package/lib/ops/sessions.js +603 -0
  52. package/lib/ops/trash.js +140 -0
  53. package/lib/paths.js +103 -0
  54. package/lib/prompts/preset-id.js +49 -0
  55. package/lib/{agents-md → prompts}/service.js +1 -1
  56. package/lib/request-gate.js +320 -0
  57. package/lib/scene-prompt-sync.js +4 -4
  58. package/lib/scenes/candidates.js +344 -0
  59. package/lib/{history → sessions}/bridge.js +22 -5
  60. package/lib/sessions/history.js +323 -0
  61. package/lib/{history → sessions}/tombstone.js +1 -2
  62. package/lib/{history → sessions}/workspace.js +92 -24
  63. package/lib/skills/catalog.js +6 -9
  64. package/lib/skills/core.js +79 -43
  65. package/lib/skills/readonly-discovery.js +4 -1
  66. package/lib/skills/service.js +98 -13
  67. package/lib/subagents/catalog.js +31 -15
  68. package/lib/subagents/service.js +506 -89
  69. package/lib/subagents/tools.js +29 -4
  70. package/lib/tools/deps.js +8 -0
  71. package/lib/tools/mcp.js +110 -0
  72. package/lib/tools/memory.js +87 -0
  73. package/lib/tools/prompt.js +70 -0
  74. package/lib/tools/skills.js +139 -0
  75. package/lib/tools/subagent.js +40 -0
  76. package/package.json +105 -102
  77. package/lib/agents-md/preset-id.js +0 -49
  78. package/lib/history/projcache.js +0 -335
  79. package/lib/rules/service.js +0 -2971
package/package.json CHANGED
@@ -1,102 +1,105 @@
1
- {
2
- "name": "dsh-plugin-tool-management",
3
- "version": "0.9.1",
4
- "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagents, AGENTS.md presets and archived sessions — scene memory injects into the system prompt, scene lock freezes all five domains, deleted workspaces re-register with one click. Settings UI + HTTP API + 14 model tools (mcp_manager_*, skill_manager_*, prompt_manager_*, memory_manager_*, subagent_*). Install with one command: dsh plugin --profile web add dsh-plugin-tool-management@latest",
5
- "repository": {
6
- "type": "git",
7
- "url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
8
- },
9
- "type": "module",
10
- "main": "lib/index.js",
11
- "exports": {
12
- ".": "./lib/index.js",
13
- "./client": "./lib/client.js",
14
- "./workspace": "./lib/history/workspace.js",
15
- "./projcache": "./lib/history/projcache.js",
16
- "./package.json": "./package.json"
17
- },
18
- "files": [
19
- "lib",
20
- "cordis.patch.yml",
21
- "README.md",
22
- "README_EN.md",
23
- "CHANGELOG.md",
24
- "LICENSE",
25
- "screenshots.json",
26
- "docs"
27
- ],
28
- "engines": {
29
- "node": ">=18"
30
- },
31
- "publishConfig": {
32
- "registry": "https://registry.npmjs.org",
33
- "access": "public"
34
- },
35
- "scripts": {
36
- "build": "tsc -p tsconfig.json && node scripts/sync-client.mjs",
37
- "build:client": "node scripts/sync-client.mjs",
38
- "prepublishOnly": "npm run build",
39
- "lint": "node --check lib/client.js && node --check lib/index.js",
40
- "check:i18n": "node scripts/check-i18n.mjs src/client.js",
41
- "check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
42
- "host-deps": "node scripts/host-deps.mjs --fix",
43
- "doctor": "node scripts/doctor.mjs",
44
- "test": "npm run build && npm run check:i18n && node --test test/client-exports.test.mjs test/client-render.test.mjs",
45
- "sync:profile": "node scripts/sync-profile.mjs"
46
- },
47
- "keywords": [
48
- "dsh",
49
- "deepseek-harness",
50
- "skill",
51
- "skills",
52
- "mcp",
53
- "mcp-server",
54
- "plugin",
55
- "cordis"
56
- ],
57
- "dsh": {
58
- "bundle": {
59
- "patch": "./cordis.patch.yml"
60
- },
61
- "client": {
62
- "platform": "web",
63
- "inject": [
64
- "@deepseek-ai/dsh-client-locale"
65
- ]
66
- }
67
- },
68
- "peerDependencies": {
69
- "@deepseek-ai/cordis": "^4.0.1",
70
- "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
71
- "@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
72
- "@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2",
73
- "@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2",
74
- "@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2",
75
- "@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2",
76
- "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
77
- "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2",
78
- "react": "^18.2.0"
79
- },
80
- "peerDependenciesMeta": {
81
- "@deepseek-ai/dsh-subagent-spawn-in-process": {
82
- "optional": true
83
- }
84
- },
85
- "devDependencies": {
86
- "@deepseek-ai/cordis": "^4.0.1",
87
- "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
88
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
89
- "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
90
- "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
91
- "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
92
- "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
93
- "@deepseek-ai/dsh-user-approval": "^0.1.5-rc.2",
94
- "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
95
- "@types/node": "^24.0.0",
96
- "typescript": "^6.0.0"
97
- },
98
- "license": "MIT",
99
- "dependencies": {
100
- "fflate": "^0.8.3"
101
- }
102
- }
1
+ {
2
+ "name": "dsh-plugin-tool-management",
3
+ "version": "0.11.0",
4
+ "description": "DSH plugin: one settings panel for MCP servers, skills, scenes, memories, subagents, AGENTS.md presets and archived sessions — scene memory injects into the system prompt, scene lock freezes all five domains, deleted workspaces re-register with one click. Settings UI + HTTP API + 14 model tools (mcp_manager_*, skill_manager_*, prompt_manager_*, memory_manager_*, subagent_*). Install with one command: dsh plugin --profile web add dsh-plugin-tool-management@latest",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/ouli-1242/dsh-plugin-tool-management"
8
+ },
9
+ "type": "module",
10
+ "main": "lib/index.js",
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
+ "./client": "./lib/client.js",
14
+ "./workspace": "./lib/sessions/workspace.js",
15
+ "./package.json": "./package.json"
16
+ },
17
+ "files": [
18
+ "lib",
19
+ "cordis.patch.yml",
20
+ "README.md",
21
+ "README_EN.md",
22
+ "CHANGELOG.md",
23
+ "LICENSE",
24
+ "screenshots.json",
25
+ "docs"
26
+ ],
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "publishConfig": {
31
+ "registry": "https://registry.npmjs.org",
32
+ "access": "public"
33
+ },
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.json && node scripts/sync-client.mjs && node scripts/sync-profile.mjs",
36
+ "prepublishOnly": "npm run build && npm run lint && node --test test/client-exports.test.mjs",
37
+ "lint": "node --check lib/client.js && node --check lib/index.js",
38
+ "typecheck:client": "node scripts/sync-client.mjs && tsc -p tsconfig.client.json",
39
+ "check:i18n": "node scripts/check-i18n.mjs lib/client.js",
40
+ "check:host": "node scripts/host-deps.mjs && node scripts/doctor.mjs",
41
+ "host-deps": "node scripts/host-deps.mjs --fix",
42
+ "doctor": "node scripts/doctor.mjs",
43
+ "test": "npm run build && npm run check:i18n && node --test test/contracts.test.mjs test/client-exports.test.mjs",
44
+ "sync:profile": "node scripts/sync-profile.mjs"
45
+ },
46
+ "keywords": [
47
+ "dsh",
48
+ "deepseek-harness",
49
+ "skill",
50
+ "skills",
51
+ "mcp",
52
+ "mcp-server",
53
+ "plugin",
54
+ "cordis"
55
+ ],
56
+ "dsh": {
57
+ "bundle": {
58
+ "patch": "./cordis.patch.yml"
59
+ },
60
+ "client": {
61
+ "platform": "web",
62
+ "inject": [
63
+ "@deepseek-ai/dsh-client-locale"
64
+ ]
65
+ }
66
+ },
67
+ "peerDependencies": {
68
+ "@deepseek-ai/cordis": "^4.0.1",
69
+ "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
70
+ "@deepseek-ai/dsh-session": ">=0.1.5-rc.2",
71
+ "@deepseek-ai/dsh-session-projection-cache": ">=0.1.5-rc.2",
72
+ "@deepseek-ai/dsh-spill-local": ">=0.1.5-rc.2",
73
+ "@deepseek-ai/dsh-storage-domain": ">=0.1.5-rc.2",
74
+ "@deepseek-ai/dsh-subagent-fork-in-process": ">=0.1.5-rc.2",
75
+ "@deepseek-ai/dsh-subagent-spawn-in-process": ">=0.1.5-rc.2",
76
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
77
+ "@deepseek-ai/dsh-workspace": ">=0.1.5-rc.2",
78
+ "react": "^18.2.0"
79
+ },
80
+ "peerDependenciesMeta": {
81
+ "@deepseek-ai/dsh-subagent-fork-in-process": {
82
+ "optional": true
83
+ },
84
+ "@deepseek-ai/dsh-subagent-spawn-in-process": {
85
+ "optional": true
86
+ }
87
+ },
88
+ "devDependencies": {
89
+ "@deepseek-ai/cordis": "^4.0.1",
90
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
91
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
92
+ "@deepseek-ai/dsh-session-projection-cache": "^0.1.5-rc.2",
93
+ "@deepseek-ai/dsh-spill-local": "^0.1.5-rc.2",
94
+ "@deepseek-ai/dsh-storage-domain": "^0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-user-approval": "^0.1.5-rc.2",
97
+ "@deepseek-ai/dsh-workspace": "^0.1.5-rc.2",
98
+ "@types/node": "^24.0.0",
99
+ "typescript": "^6.0.0"
100
+ },
101
+ "license": "MIT",
102
+ "dependencies": {
103
+ "fflate": "^0.8.3"
104
+ }
105
+ }
@@ -1,49 +0,0 @@
1
- // dsh-plugin-tool-management —— 提示词预设 id 的唯一口径。
2
- //
3
- // 用户裁定(2026-09-15):「id 应该什么都能写」——不再限定小写字母/数字/连字符,
4
- // 中文、空格、下划线、点都可以。id 同时也是**磁盘目录名**,所以只保留两类硬约束:
5
- // ① 文件系统安全:不含路径分隔符与 Windows 非法字符、不以点开头/结尾、
6
- // 不是 `.` / `..`、不是 Windows 保留设备名;
7
- // ② 一个保留字:`__last-applied__`(apply 的备份槽,不是用户预设)。
8
- // 这套口径由提示词服务(agents-md/ 目录)与场景绑定(`scenes[].prompt`)共用,避免两处漂移。
9
- // 约定镜像 @deepseek-ai/dsh-agent-presets 的「id 即目录名」,但放宽了字符集。
10
- /** id 长度上限(字符数;目录名过长在 Windows 上还受 MAX_PATH 约束)。 */
11
- export const PRESET_ID_MAX = 64;
12
- /** apply 的备份槽:不是用户预设,任何入口都不得当作预设 id。 */
13
- export const LAST_APPLIED_PRESET_ID = '__last-applied__';
14
- /** Windows 保留设备名(不区分大小写,带扩展名同样非法)。 */
15
- const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
16
- /**
17
- * 校验并归一化一个预设 id。
18
- * @param raw - 用户输入或来自索引的原始值。
19
- * @returns `{ ok: true, id }`(已 trim)或 `{ ok: false, error }`(给人看的中文原因)。
20
- */
21
- export function normalizePresetId(raw) {
22
- const id = String(raw ?? '').trim();
23
- if (id === '')
24
- return { ok: false, error: 'id 不能为空' };
25
- if (id.length > PRESET_ID_MAX)
26
- return { ok: false, error: `id 过长(≤${PRESET_ID_MAX} 字符,当前 ${id.length})` };
27
- if (id === LAST_APPLIED_PRESET_ID)
28
- return { ok: false, error: `「${LAST_APPLIED_PRESET_ID}」是「应用」的备份槽,不能用作预设 id` };
29
- if (id === '.' || id === '..')
30
- return { ok: false, error: 'id 不能是「.」或「..」' };
31
- if (id.startsWith('.'))
32
- return { ok: false, error: 'id 不能以「.」开头' };
33
- if (/[\\/]/.test(id))
34
- return { ok: false, error: 'id 不能含路径分隔符(/ 或 \\)' };
35
- if (/[<>:"|?*]/.test(id))
36
- return { ok: false, error: 'id 不能含 < > : " | ? * 这些字符' };
37
- if (/[.\s]$/.test(id))
38
- return { ok: false, error: 'id 不能以点或空格结尾(Windows 会静默去掉)' };
39
- if (WINDOWS_RESERVED.test(id))
40
- return { ok: false, error: `「${id}」是 Windows 保留设备名` };
41
- // 控制字符(含换行/制表)会让目录名与日志不可读,直接拒绝。
42
- if (/[\u0000-\u001f\u007f]/.test(id))
43
- return { ok: false, error: 'id 不能含控制字符' };
44
- return { ok: true, id };
45
- }
46
- /** 便捷判定(索引解析等只关心"能不能用"的地方)。 */
47
- export function isValidPresetId(raw) {
48
- return normalizePresetId(raw).ok;
49
- }
@@ -1,335 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { Service } from "@deepseek-ai/cordis";
3
- import { SessionProjectionCache, checkpointIdentity, checkpointRecord, projectionCacheDomainSpec } from "@deepseek-ai/dsh-session-projection-cache";
4
- import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
5
- import { trackTombstone } from "./tombstone.js";
6
- /**
7
- * 当前投影缓存写入独立的 v2 域,避免 DSH alpha.2 将包含冒号的逻辑会话 ID
8
- * 直接映射为 Windows 文件名。旧名 v1 与旧名 v2 域只在启动时读取并补缺迁入,
9
- * 迁移不删除原始缓存,任意中断都可在后续启动继续。
10
- */
11
- const storedCheckpointRecord = checkpointRecord.extend({
12
- sessionId: checkpointIdentity.shape.cwd.unwrap()
13
- });
14
- const safeDomainInput = {
15
- // 旧版 DSH 不支持同名域的版本迁移。v2 改用新域名,确保升级时不会在
16
- // 打开目标域之前因旧 v1 元数据而终止启动。
17
- name: "session_projcache_archive_manager_v2",
18
- version: 2,
19
- tables: { sessions: domainTable(storedCheckpointRecord) },
20
- ...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
21
- };
22
- const safeProjectionCacheDomainSpec = defineDomain(safeDomainInput);
23
- /** The last official whole-file cache format used by DSH rc.2. */
24
- const legacyOptionalIdentityFields = {};
25
- for (const field of ["isSeeded", "inheritedEventCount"]) {
26
- // rc.2 的身份 schema 尚未定义这些字段;向 Zod 传入不存在的字段会在
27
- // 首次解析历史记录时抛错,因此只能按当前运行时实际的 schema 构造掩码。
28
- if (Object.hasOwn(checkpointIdentity.shape, field))
29
- legacyOptionalIdentityFields[field] = true;
30
- }
31
- const legacyCheckpointRecord = checkpointRecord.extend({
32
- identity: checkpointIdentity.partial(legacyOptionalIdentityFields)
33
- });
34
- const legacyStoredCheckpointRecord = legacyCheckpointRecord.extend({
35
- sessionId: checkpointIdentity.shape.cwd.unwrap()
36
- });
37
- /** v0.1.24 短暂使用过的旧名 v2 安全缓存域。 */
38
- const legacySafeV2ProjectionCacheDomainSpec = defineDomain({
39
- name: "session_projcache_archive_manager",
40
- version: 2,
41
- tables: { sessions: domainTable(storedCheckpointRecord) },
42
- ...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
43
- });
44
- /** v0.1.23 及以前归档管理器自身使用的安全缓存域。 */
45
- const legacySafeProjectionCacheDomainSpec = defineDomain({
46
- name: "session_projcache_archive_manager",
47
- version: 1,
48
- tables: { sessions: domainTable(legacyStoredCheckpointRecord) },
49
- ...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
50
- });
51
- const legacyProjectionCacheDomainSpec = defineDomain({
52
- name: "session_projcache",
53
- version: 3,
54
- tables: { sessions: domainTable(legacyCheckpointRecord) }
55
- });
56
- function projectionCacheStorageKey(sessionId) {
57
- return `session_${createHash("sha256").update(sessionId, "utf8").digest("base64url")}`;
58
- }
59
- function unwrapStoredCheckpoint(stored) {
60
- return { identity: stored.identity, rows: stored.rows };
61
- }
62
- /** Map the Session-id table contract onto path-safe physical keys. */
63
- class SafeSessionTable {
64
- constructor(table) {
65
- this.table = table;
66
- }
67
- get size() {
68
- return [...this.keys()].length;
69
- }
70
- get(sessionId) {
71
- const stored = this.table.get(projectionCacheStorageKey(sessionId));
72
- if (stored === void 0 || stored.sessionId !== sessionId)
73
- return void 0;
74
- return unwrapStoredCheckpoint(stored);
75
- }
76
- has(sessionId) {
77
- return this.get(sessionId) !== void 0;
78
- }
79
- *entries() {
80
- for (const [key, stored] of this.table.entries()) {
81
- if (projectionCacheStorageKey(stored.sessionId) !== key)
82
- continue;
83
- yield [stored.sessionId, unwrapStoredCheckpoint(stored)];
84
- }
85
- }
86
- *keys() {
87
- for (const [sessionId] of this.entries())
88
- yield sessionId;
89
- }
90
- *values() {
91
- for (const [, value] of this.entries())
92
- yield value;
93
- }
94
- async put(sessionId, record) {
95
- const key = projectionCacheStorageKey(sessionId);
96
- const existing = this.table.get(key);
97
- if (existing !== void 0 && existing.sessionId !== sessionId) {
98
- throw new Error(`projection-cache storage-key collision for "${sessionId}"`);
99
- }
100
- await this.table.put(key, { sessionId, identity: record.identity, rows: record.rows });
101
- }
102
- async delete(sessionId) {
103
- const key = projectionCacheStorageKey(sessionId);
104
- const existing = this.table.get(key);
105
- if (existing === void 0 || existing.sessionId !== sessionId)
106
- return false;
107
- return this.table.delete(key);
108
- }
109
- }
110
- async function readSourceRecords(ctx, spec, label) {
111
- let domain;
112
- try {
113
- domain = await ctx.storageDomain.open(spec);
114
- return { opened: true, records: [...domain.table("sessions").entries()] };
115
- }
116
- catch (error) {
117
- ctx.logger.warn(`archive-manager projcache: ${label} cache import skipped: ${String(error)}`);
118
- return { opened: false, records: [] };
119
- }
120
- finally {
121
- try {
122
- await domain?.close();
123
- }
124
- catch (error) {
125
- ctx.logger.warn(`archive-manager projcache: ${label} cache close failed: ${String(error)}`);
126
- }
127
- }
128
- }
129
- async function readLegacySafeRecords(ctx, spec, label) {
130
- const source = await readSourceRecords(ctx, spec, label);
131
- const records = [];
132
- for (const [physicalKey, stored] of source.records) {
133
- if (stored === null || typeof stored !== "object" || typeof stored.sessionId !== "string") {
134
- ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its session id is invalid`);
135
- continue;
136
- }
137
- if (projectionCacheStorageKey(stored.sessionId) !== physicalKey) {
138
- ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its storage key does not match the session id`);
139
- continue;
140
- }
141
- records.push([stored.sessionId, unwrapStoredCheckpoint(stored)]);
142
- }
143
- return { ...source, records };
144
- }
145
- /**
146
- * 旧版记录没有完整身份时,只能安全补齐未播种会话的零继承切点。
147
- * 播种会话无法从头部反推出精确切点,宁可跳过并按需重建。
148
- */
149
- function normalizeLegacyRecord(record) {
150
- if (record === null || typeof record !== "object" || record.identity === null || typeof record.identity !== "object")
151
- return void 0;
152
- const identity = record.identity;
153
- const inheritedEventCount = identity.inheritedEventCount;
154
- const validOffset = Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0;
155
- if (identity.isSeeded === true) {
156
- if (!validOffset)
157
- return void 0;
158
- return record;
159
- }
160
- if (identity.isSeeded !== void 0 && identity.isSeeded !== false)
161
- return void 0;
162
- if (inheritedEventCount !== void 0 && inheritedEventCount !== 0)
163
- return void 0;
164
- return {
165
- ...record,
166
- identity: {
167
- ...identity,
168
- isSeeded: false,
169
- inheritedEventCount: 0
170
- }
171
- };
172
- }
173
- async function importMissingRecords(ctx, target, records, label) {
174
- let imported = 0;
175
- for (const [sessionId, record] of records) {
176
- if (target.has(sessionId))
177
- continue;
178
- try {
179
- const normalized = normalizeLegacyRecord(record);
180
- if (normalized === void 0) {
181
- ctx.logger.warn(`archive-manager projcache: ${label} row "${sessionId}" import skipped because its lifecycle identity is incomplete`);
182
- continue;
183
- }
184
- await target.put(sessionId, normalized);
185
- imported += 1;
186
- }
187
- catch (error) {
188
- ctx.logger.warn(`archive-manager projcache: ${label} row "${sessionId}" import failed: ${String(error)}`);
189
- }
190
- }
191
- return imported;
192
- }
193
- /**
194
- * 合并归档管理器旧名 v2、旧名 v1、官方新版逐会话与旧版整文件缓存。目标域记录
195
- * 最高优先,随后按新旧顺序只补缺失记录;任一来源失败时仍继续尝试另一个来源。
196
- */
197
- async function importPreviousProjectionCache(ctx, target) {
198
- const legacySafeV2 = await readLegacySafeRecords(ctx, legacySafeV2ProjectionCacheDomainSpec, "archive-manager v2");
199
- const legacySafe = await readLegacySafeRecords(ctx, legacySafeProjectionCacheDomainSpec, "archive-manager v1");
200
- const legacy = await readSourceRecords(ctx, legacyProjectionCacheDomainSpec, "legacy v3");
201
- const sameSpec = projectionCacheDomainSpec.version === legacyProjectionCacheDomainSpec.version
202
- && projectionCacheDomainSpec.layout === legacyProjectionCacheDomainSpec.layout;
203
- let imported = 0;
204
- if (legacySafeV2.opened)
205
- imported += await importMissingRecords(ctx, target, legacySafeV2.records, "archive-manager v2");
206
- if (legacySafe.opened)
207
- imported += await importMissingRecords(ctx, target, legacySafe.records, "archive-manager v1");
208
- if (!sameSpec) {
209
- const current = await readSourceRecords(ctx, projectionCacheDomainSpec, "current");
210
- if (current.opened)
211
- imported += await importMissingRecords(ctx, target, current.records, "current");
212
- }
213
- if (legacy.opened)
214
- imported += await importMissingRecords(ctx, target, legacy.records, "legacy v3");
215
- return imported;
216
- }
217
- //#region lib/types/index.js
218
- /**
219
- * dsh-archive-manager projcache 半边。
220
- *
221
- * `ArchiveProjectionCache` 继承上游 `SessionProjectionCache`,保留服务名
222
- * `sessionProjectionCache` 与 fail-soft 写路径,但写入独立、路径安全的
223
- * `session_projcache_archive_manager_v2` 域。旧名 v1 与 v2 域只在启动时
224
- * 读取并补缺迁入;归档管理的
225
- * `deleteSession` 增加三处守护:
226
- *
227
- * - `delete(id)` - 永久移除兼容域中该会话经 SHA-256 映射的缓存投影行,
228
- * 写入前先登记墓碑。
229
- * - `whenIdle()` - 全部在途公开写入落定后 resolve。
230
- * - 墓碑挡住已删除会话的写入:迟到的写入要么被直接拦截,要么在
231
- * 落定后补删自己的残留行,不会复活已删除的缓存条目。守护覆盖
232
- * `write()`(dispose 写后路径)与 `put()`(rc.2 的 `putSoft` 最终调用
233
- * 它,alpha.2 的 `coldSnapshot` 则直接调用它),两代落盘路径都纳入
234
- * `whenIdle` 跟踪。
235
- *
236
- * 默认导出是 Service 子类(与上游 `@deepseek-ai/dsh-session-projection-cache`
237
- * 包同形),profile 补丁可直接替换 `session-projection-cache` 服务行,
238
- * 无需其他接线改动。
239
- */
240
- var ArchiveProjectionCache = class extends SessionProjectionCache {
241
- /** 已永久删除的会话:其投影缓存行不再允许写入。 */
242
- deletedSessionIds = /* @__PURE__ */ new Set();
243
- /** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
244
- deletedSessionOrder = [];
245
- deletedSessionTombstoneLimit = 4096;
246
- /** 在途写入的队尾(只含已落定的 promise)。 */
247
- writeTail = Promise.resolve();
248
- constructor(ctx, config) {
249
- super(ctx, config);
250
- }
251
- /**
252
- * Replace the official cache domain with the path-safe compatibility
253
- * domain, then preserve the upstream listener/write implementation.
254
- */
255
- async [Service.init]() {
256
- const domain = await this.ctx.storageDomain.open(safeProjectionCacheDomainSpec);
257
- this.ctx.effect(() => () => domain.close(), "sessionProjectionCache.domainClose");
258
- const table = new SafeSessionTable(domain.table("sessions"));
259
- this.table = table;
260
- await importPreviousProjectionCache(this.ctx, table);
261
- this.installWritePath();
262
- }
263
- /** 跟踪公开写入路径,避免依赖上游私有 `flushSoft` 的实现细节。 */
264
- write(session) {
265
- const task = this.writeCore(session);
266
- this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
267
- return task;
268
- }
269
- /**
270
- * 墓碑正确性依赖:super.write(session) 一次整体写入,返回后不再有后续异步落盘。
271
- * 若上游改成多阶段异步,(C) 补删会漏掉后续写入,deletedSessionIds 挡不住复活。
272
- */
273
- async writeCore(session) {
274
- if (this.deletedSessionIds.has(session.id))
275
- return;
276
- await super.write(session);
277
- if (this.deletedSessionIds.has(session.id))
278
- await this.requireTable().delete(session.id);
279
- }
280
- /**
281
- * 守住所有底层写入。rc.2 的冷读经 `putSoft -> put`,alpha.2 则
282
- * 直接经 `put`;把墓碑与 whenIdle 跟踪放在这里可同时兼容两代实现。
283
- */
284
- put(id, identity, rows) {
285
- // alpha.2 coldSnapshot unconditionally chains `.catch()` onto put().
286
- // A blocked write must therefore preserve the upstream Promise contract.
287
- if (this.deletedSessionIds.has(id))
288
- return Promise.resolve();
289
- const task = this.putCore(id, identity, rows);
290
- this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
291
- return task;
292
- }
293
- async putCore(id, identity, rows) {
294
- await super.put(id, identity, rows);
295
- if (this.deletedSessionIds.has(id))
296
- await this.requireTable().delete(id);
297
- }
298
- /** rc.2 公开的 fail-soft 辅助在 alpha.2 已移除;保留同形兼容入口。 */
299
- async putSoft(id, identity, rows, what) {
300
- const upstream = SessionProjectionCache.prototype.putSoft;
301
- if (typeof upstream === "function")
302
- return upstream.call(this, id, identity, rows, what);
303
- try {
304
- await this.put(id, identity, rows);
305
- }
306
- catch (error) {
307
- this.ctx.logger.warn(`session projection cache: ${what} for "${id}" failed (cache stays stale): ${String(error)}`);
308
- }
309
- }
310
- /**
311
- * 全部被跟踪的在途写入落定后 resolve(含失败)。空闲缓存上调用立即返回。
312
- * @returns 跟踪写入落定后的 resolution。
313
- */
314
- whenIdle() {
315
- return this.writeTail;
316
- }
317
- /**
318
- * 永久移除一个会话的缓存投影行。
319
- * @param id - 要删除缓存行的会话。
320
- * @returns 行删除完成后的 resolution。
321
- */
322
- async delete(id) {
323
- trackTombstone(this.deletedSessionIds, this.deletedSessionOrder, id, this.deletedSessionTombstoneLimit);
324
- await this.requireTable().delete(id);
325
- }
326
- /** 撤销墓碑(供测试与同 id 新生命周期复用路径使用)。 */
327
- clearTombstone(id) {
328
- this.deletedSessionIds.delete(id);
329
- const idx = this.deletedSessionOrder.indexOf(id);
330
- if (idx !== -1)
331
- this.deletedSessionOrder.splice(idx, 1);
332
- }
333
- };
334
- //#endregion
335
- export { ArchiveProjectionCache, ArchiveProjectionCache as default, SafeSessionTable, importPreviousProjectionCache, legacyProjectionCacheDomainSpec, legacySafeProjectionCacheDomainSpec, legacySafeV2ProjectionCacheDomainSpec, projectionCacheStorageKey, safeProjectionCacheDomainSpec };