@michengai/dsh-archive-manager 0.1.43 → 1.0.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.
@@ -0,0 +1,123 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lstat, readFile, readdir, open, link, unlink, realpath, rename } from "node:fs/promises";
3
+ import { join, resolve, basename } from "node:path";
4
+ import * as zlib from "node:zlib";
5
+ import { classifySessionError } from "./archive-discovery.js";
6
+ const LIMIT = 32 * 1024 * 1024;
7
+ const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
8
+ function decodeRepairLog(bytes, compressed) {
9
+ if (bytes.length > LIMIT) throw new Error("\u65E5\u5FD7\u8D85\u8FC7 32 MB\uFF0C\u9700\u79BB\u7EBF\u5904\u7406");
10
+ const chunks = [];
11
+ let offset = 0, size = 0;
12
+ if (compressed && typeof zlib.zstdDecompressSync !== "function") throw new Error("\u5F53\u524D Node.js \u4E0D\u652F\u6301\u538B\u7F29\u65E5\u5FD7\u8BCA\u65AD");
13
+ while (compressed && offset < bytes.length) {
14
+ const result = zlib.zstdDecompressSync(bytes.subarray(offset), { info: true, maxOutputLength: LIMIT - size });
15
+ if (!result.engine.bytesWritten) throw new Error("\u538B\u7F29\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574\u5E27");
16
+ offset += result.engine.bytesWritten;
17
+ size += result.buffer.length;
18
+ chunks.push(result.buffer);
19
+ if (size > LIMIT) throw new Error("\u89E3\u538B\u65E5\u5FD7\u8D85\u8FC7 32 MB\uFF0C\u9700\u79BB\u7EBF\u5904\u7406");
20
+ }
21
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(compressed ? Buffer.concat(chunks) : bytes);
22
+ if (!text.endsWith("\n")) throw new Error("\u65E5\u5FD7\u672B\u5C3E\u4E0D\u5B8C\u6574\uFF0C\u4E0D\u80FD\u81EA\u52A8\u4FEE\u590D");
23
+ return text.trimEnd().split("\n").map((line) => JSON.parse(line));
24
+ }
25
+ function normalizeAutomationSources(input) {
26
+ const rows = structuredClone(input);
27
+ let count = 0;
28
+ const fix = (message) => {
29
+ const source = message?.source;
30
+ if (source?.kind !== "automation") return;
31
+ if (Object.keys(source).some((key) => !["kind", "automationId", "runId", "scheduledFor"].includes(key)) || !["automationId", "runId", "scheduledFor"].every((key) => typeof source[key] === "string" && source[key].length > 0 && source[key].length < 512) || !Number.isFinite(Date.parse(source.scheduledFor))) {
32
+ throw new Error("\u81EA\u52A8\u5316\u5F52\u5C5E\u4FE1\u606F\u4E0D\u5B8C\u6574\u6216\u5B58\u5728\u672A\u77E5\u5B57\u6BB5\uFF0C\u9700\u4EBA\u5DE5\u68C0\u67E5");
33
+ }
34
+ message.source = { kind: "plugin", plugin: "dsh-automation", form: "notice", summary: JSON.stringify(source) };
35
+ count++;
36
+ };
37
+ for (const row of rows.slice(1)) {
38
+ if (row.type === "user/message") fix(row.data);
39
+ if (["assistant/message", "tool/result"].includes(row.type)) fix(row.data?.message);
40
+ if (row.type === "agent/inbox/spliced") row.data?.inserted?.forEach(fix);
41
+ if (row.type === "session/title-llm-request") row.data?.messages?.forEach(fix);
42
+ }
43
+ return { rows, count };
44
+ }
45
+ async function regularFile(path) {
46
+ const stat = await lstat(path);
47
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > LIMIT) throw new Error("\u5DE5\u4EF6\u4E0D\u662F\u53EF\u5B89\u5168\u5904\u7406\u7684\u5E38\u89C4\u65E5\u5FD7\u6587\u4EF6");
48
+ }
49
+ async function prepareAutomationRepair({ directory, target, sessionId, format }) {
50
+ if (!format || format.currentVersion !== 3 || typeof format.createRestore !== "function") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u6B64\u4FEE\u590D\uFF0C\u8BF7\u5347\u7EA7 DSH \u540E\u91CD\u8BD5");
51
+ if (resolve(await realpath(directory)).toLowerCase() !== resolve(directory).toLowerCase()) throw new Error("\u4F1A\u8BDD\u76EE\u5F55\u5B58\u5728\u94FE\u63A5\u91CD\u5B9A\u5411\uFF0C\u9700\u4EBA\u5DE5\u5904\u7406");
52
+ const names = (await readdir(directory)).filter((name) => /^session(?:\.v[1-9][0-9]*)?\.jsonl(?:\.zstd)?$/.test(name));
53
+ const sources = names.filter((name) => /^session\.jsonl(?:\.zstd)?$/.test(name));
54
+ if (sources.length !== 1 || names.some((name) => name !== sources[0] && name !== basename(target))) throw new Error("\u53D1\u73B0\u591A\u4E2A\u6216\u975E\u65E7\u7248\u65E5\u5FD7\u5DE5\u4EF6\uFF0C\u8BF7\u5148\u68C0\u67E5\u4EE3\u9645\u72B6\u6001");
55
+ const source = join(directory, sources[0]);
56
+ await regularFile(source);
57
+ const bytes = await readFile(source);
58
+ const rows = decodeRepairLog(bytes, source.endsWith(".zstd"));
59
+ if (rows[0]?.type !== "session" || rows[0]?.version !== 0 || rows[0]?.id !== sessionId) throw new Error("\u65E5\u5FD7\u8EAB\u4EFD\u6216\u7248\u672C\u4E0D\u5339\u914D\uFF0C\u4E0D\u80FD\u81EA\u52A8\u4FEE\u590D");
60
+ const normalized = normalizeAutomationSources(rows);
61
+ if (!normalized.count) throw new Error("\u672A\u627E\u5230\u7B26\u5408\u4FEE\u590D\u89C4\u5219\u7684\u81EA\u52A8\u5316\u6765\u6E90");
62
+ const restore = format.createRestore(normalized.rows[0]);
63
+ normalized.rows.slice(1).forEach((row) => restore.decodeRow(row));
64
+ const artifact = restore.finish();
65
+ const lines = [format.encodeHeader(artifact.header, artifact.inheritedEventCount), ...artifact.events.map((event) => format.encodeEvent(event))];
66
+ const verify = format.createRestore(lines[0]);
67
+ lines.slice(1).forEach((row) => verify.decodeRow(row));
68
+ const checked = verify.finish();
69
+ await format.validate?.(checked);
70
+ if (checked.header.id !== sessionId || checked.events.length !== artifact.events.length || checked.inheritedEventCount !== artifact.inheritedEventCount) throw new Error("\u751F\u6210\u7269\u6821\u9A8C\u5931\u8D25");
71
+ const plain = Buffer.from(lines.map((row) => JSON.stringify(row)).join("\n") + "\n", "utf8");
72
+ if (plain.length > LIMIT) throw new Error("\u4FEE\u590D\u7ED3\u679C\u8D85\u8FC7\u5904\u7406\u4E0A\u9650");
73
+ const compress = (value) => zlib.zstdCompressSync(value, { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } });
74
+ const headerBytes = Buffer.from(JSON.stringify(lines[0]) + "\n", "utf8");
75
+ const output = target.endsWith(".zstd") ? Buffer.concat([compress(headerBytes), ...lines.length > 1 ? [compress(plain.subarray(headerBytes.length))] : []]) : plain;
76
+ await format.validateBytes?.(output);
77
+ let previousTarget;
78
+ if (names.includes(basename(target))) {
79
+ await regularFile(target);
80
+ previousTarget = await readFile(target);
81
+ if (!target.endsWith(".zstd")) throw new Error("\u5DF2\u6709\u65B0\u7248\u65E5\u5FD7\uFF0C\u4E0D\u80FD\u8986\u76D6");
82
+ const first = zlib.zstdDecompressSync(previousTarget, { info: true, maxOutputLength: LIMIT });
83
+ if (first.engine.bytesWritten !== previousTarget.length || !first.buffer.equals(plain) || first.buffer.indexOf(10) === first.buffer.length - 1) throw new Error("\u5DF2\u6709\u65E5\u5FD7\u4E0E\u65E7\u7248\u4FEE\u590D\u4EA7\u7269\u4E0D\u5339\u914D\uFF0C\u4E0D\u80FD\u8986\u76D6");
84
+ }
85
+ const token = digest(previousTarget ? Buffer.concat([bytes, previousTarget]) : bytes);
86
+ const unchanged = async () => {
87
+ if (digest(await readFile(source)) !== digest(bytes)) throw new Error("\u65E5\u5FD7\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BCA\u65AD");
88
+ if (previousTarget) {
89
+ await regularFile(target);
90
+ if (digest(await readFile(target)) !== digest(previousTarget)) throw new Error("\u4FEE\u590D\u4EA7\u7269\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BCA\u65AD");
91
+ }
92
+ };
93
+ return { token, correctingFrame: Boolean(previousTarget), count: normalized.count, source, async publish(expectedToken, ensureIdle) {
94
+ if (expectedToken !== token) throw new Error("\u65E5\u5FD7\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BCA\u65AD");
95
+ await ensureIdle();
96
+ await regularFile(source);
97
+ await unchanged();
98
+ const temp = join(directory, ".archive-repair-" + randomUUID());
99
+ let handle;
100
+ try {
101
+ handle = await open(temp, "wx", 384);
102
+ await handle.writeFile(output);
103
+ await handle.sync();
104
+ await handle.close();
105
+ handle = void 0;
106
+ await ensureIdle();
107
+ await unchanged();
108
+ if (previousTarget) await rename(temp, target);
109
+ else await link(temp, target);
110
+ } finally {
111
+ if (handle) await handle.close();
112
+ await unlink(temp).catch((error) => {
113
+ if (error.code !== "ENOENT") throw error;
114
+ });
115
+ }
116
+ } };
117
+ }
118
+ export {
119
+ classifySessionError,
120
+ decodeRepairLog,
121
+ normalizeAutomationSources,
122
+ prepareAutomationRepair
123
+ };
package/lib/workspace.js CHANGED
@@ -1,9 +1,20 @@
1
+ import * as SessionRuntime from "@deepseek-ai/dsh-session";
2
+ import { prepareAutomationRepair } from "./session-repair.js";
3
+ import { repairInputSchema, classifySessionError, detailsInputSchema, countConversationTurns, discoveryInvocations, searchInputSchema, previewInputSchema, extractConversation, findContentMatch, previewConversation } from "./archive-discovery.js";
1
4
  import { lstat, rm } from "node:fs/promises";
2
5
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
3
6
  import { WorkspaceRegistry } from "@deepseek-ai/dsh-workspace";
4
7
  import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
5
8
  import { sessionDir } from "@deepseek-ai/dsh-spill-local";
6
9
  import { trackTombstone } from "./tombstone.js";
10
+ import { defineDomain } from "@deepseek-ai/dsh-storage-domain";
11
+ import { favoriteInputSchema, favoriteStateSchema, favoriteInvocations } from "./archive-organizer.js";
12
+ const favoriteDomainSpec = defineDomain({
13
+ name: "archive_manager_favorites",
14
+ version: 1,
15
+ tables: {},
16
+ global: { schema: favoriteStateSchema, initial: { favoriteSessionIds: [] } }
17
+ });
7
18
  function jsonlSessionDirectory(persistence, header, location) {
8
19
  if (persistence.name !== "session-persistence-jsonl" || location.kind !== "jsonl")
9
20
  return;
@@ -30,6 +41,14 @@ function jsonlSessionDirectory(persistence, header, location) {
30
41
  if (location.path !== join(directory, basename(location.path))) return;
31
42
  return directory;
32
43
  }
44
+ function strictCodec(typeSymbol, schema) {
45
+ return {
46
+ mode: "strict",
47
+ typeSymbol,
48
+ create: () => schema,
49
+ schema
50
+ };
51
+ }
33
52
  function markRemoteMethod(instance, method) {
34
53
  const context = {
35
54
  private: false,
@@ -120,6 +139,8 @@ const archivedSessionMetadataSchema = {
120
139
  }
121
140
  };
122
141
  const ARCHIVE_MANAGER_INVOCATIONS = [
142
+ ...favoriteInvocations(),
143
+ ...discoveryInvocations(),
123
144
  {
124
145
  id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSession",
125
146
  service: "workspaceRegistry",
@@ -131,18 +152,10 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
131
152
  name: "sessionId",
132
153
  wire: "sessionId",
133
154
  source: "json",
134
- codec: {
135
- mode: "strict",
136
- typeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
137
- schema: sessionIdSchema
138
- }
155
+ codec: strictCodec("@deepseek-ai/dsh-session/types#SessionId", sessionIdSchema)
139
156
  }
140
157
  ],
141
- result: {
142
- mode: "strict",
143
- typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionIds",
144
- schema: archivedSetSchema
145
- },
158
+ result: strictCodec("@michengai/dsh-archive-manager/types#ArchivedSessionIds", archivedSetSchema),
146
159
  sourceLocation: {
147
160
  file: "@michengai/dsh-archive-manager/lib/workspace.js",
148
161
  line: 1,
@@ -160,18 +173,10 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
160
173
  name: "sessionId",
161
174
  wire: "sessionId",
162
175
  source: "json",
163
- codec: {
164
- mode: "strict",
165
- typeSymbol: "@deepseek-ai/dsh-session/types#SessionId",
166
- schema: sessionIdSchema
167
- }
176
+ codec: strictCodec("@deepseek-ai/dsh-session/types#SessionId", sessionIdSchema)
168
177
  }
169
178
  ],
170
- result: {
171
- mode: "strict",
172
- typeSymbol: "@michengai/dsh-archive-manager/types#Deleted",
173
- schema: deletedSchema
174
- },
179
+ result: strictCodec("@michengai/dsh-archive-manager/types#Deleted", deletedSchema),
175
180
  sourceLocation: {
176
181
  file: "@michengai/dsh-archive-manager/lib/workspace.js",
177
182
  line: 1,
@@ -189,18 +194,10 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
189
194
  name: "target",
190
195
  wire: "target",
191
196
  source: "json",
192
- codec: {
193
- mode: "strict",
194
- typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedBatchTarget",
195
- schema: archivedBatchTargetSchema
196
- }
197
+ codec: strictCodec("@michengai/dsh-archive-manager/types#ArchivedBatchTarget", archivedBatchTargetSchema)
197
198
  }
198
199
  ],
199
- result: {
200
- mode: "strict",
201
- typeSymbol: "@michengai/dsh-archive-manager/types#DeletedBatch",
202
- schema: deletedBatchSchema
203
- },
200
+ result: strictCodec("@michengai/dsh-archive-manager/types#DeletedBatch", deletedBatchSchema),
204
201
  sourceLocation: {
205
202
  file: "@michengai/dsh-archive-manager/lib/workspace.js",
206
203
  line: 1,
@@ -214,11 +211,7 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
214
211
  method: "archivedSessionMetadata",
215
212
  invocation: { kind: "direct" },
216
213
  parameters: [],
217
- result: {
218
- mode: "strict",
219
- typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionMetadata",
220
- schema: archivedSessionMetadataSchema
221
- },
214
+ result: strictCodec("@michengai/dsh-archive-manager/types#ArchivedSessionMetadata", archivedSessionMetadataSchema),
222
215
  sourceLocation: {
223
216
  file: "@michengai/dsh-archive-manager/lib/workspace.js",
224
217
  line: 1,
@@ -278,8 +271,217 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
278
271
  markRemoteMethod(this, "deleteSession");
279
272
  markRemoteMethod(this, "deleteArchivedSessions");
280
273
  markRemoteMethod(this, "archivedSessionMetadata");
274
+ markRemoteMethod(this, "favoriteSessions");
275
+ markRemoteMethod(this, "setSessionFavorite");
276
+ markRemoteMethod(this, "searchArchivedContent");
277
+ markRemoteMethod(this, "searchSessionContent");
278
+ markRemoteMethod(this, "previewArchivedSession");
279
+ markRemoteMethod(this, "sessionDetails");
280
+ markRemoteMethod(this, "diagnoseSession");
281
+ markRemoteMethod(this, "repairSession");
281
282
  registerHostRemote(this.ctx);
282
283
  }
284
+ /** 只读取归档日志;已有实时实例读取快照,不调用 prepare、enter 或恢复接口。 */
285
+ async readArchivedConversation(sessionId) {
286
+ const ensureArchived = () => {
287
+ if (!this.requireState().archivedSessionIds.includes(sessionId)) throw new Error("\u4F1A\u8BDD\u5DF2\u4E0D\u5728\u5F52\u6863\u4E2D\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
288
+ };
289
+ ensureArchived();
290
+ const events = await this.readConversationEvents(sessionId);
291
+ ensureArchived();
292
+ return extractConversation(events);
293
+ }
294
+ /** 复用宿主只读接口读取事件,不激活会话。 */
295
+ async readConversationEvents(sessionId) {
296
+ const live = this.ctx.get("sessions")?.get(sessionId);
297
+ if (typeof live?.snapshotEvents === "function") return live.snapshotEvents();
298
+ const persistence = this.ctx.get("sessionPersistence");
299
+ if (!persistence || typeof persistence.readFrom !== "function" && typeof persistence.open !== "function") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u53EA\u8BFB\u4F1A\u8BDD\uFF0C\u8BF7\u6253\u5F00\u5B8C\u6574\u4F1A\u8BDD\u67E5\u770B");
300
+ return (await this.readStoredProjectionSource(persistence, sessionId)).events;
301
+ }
302
+ /** 只读取当前宿主已知会话;路径由宿主定位,不根据错误文本或 ID 拼接。 */
303
+ async sessionDetails(input) {
304
+ const { sessionIds } = detailsInputSchema.parse(input);
305
+ const items = [];
306
+ for (const sessionId of sessionIds) {
307
+ const item = { sessionId, turnCount: null, path: null, error: "" };
308
+ try {
309
+ if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
310
+ const header = await this.readSessionHeader(sessionId);
311
+ if (Number.isSafeInteger(header.createdAt) && header.createdAt >= 0) item.createdAt = header.createdAt;
312
+ const persistence = this.ctx.get("sessionPersistence");
313
+ try {
314
+ const location = typeof persistence?.locate === "function" ? await persistence.locate(header) : void 0;
315
+ if (typeof location?.path === "string") {
316
+ const path = jsonlSessionDirectory(persistence, header, location) ?? location.path;
317
+ await lstat(path);
318
+ item.path = path;
319
+ }
320
+ } catch (error) {
321
+ item.error = "\u8DEF\u5F84\u4E0D\u53EF\u7528\uFF1A" + String(error?.message ?? error).slice(0, 350);
322
+ }
323
+ item.turnCount = countConversationTurns(await this.readConversationEvents(sessionId));
324
+ } catch (error) {
325
+ item.error = String(error?.message ?? error).slice(0, 500);
326
+ }
327
+ items.push(item);
328
+ }
329
+ return { items };
330
+ }
331
+ /** 诊断与执行共用同一工件校验;客户端不能提供文件路径。 */
332
+ async sessionRepairPlan(sessionId) {
333
+ if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
334
+ await this.ensureSessionRepairIdle(sessionId);
335
+ const persistence = this.ctx.get("sessionPersistence");
336
+ const header = await this.readSessionHeader(sessionId);
337
+ const location = await persistence?.locate?.(header);
338
+ const directory = location && jsonlSessionDirectory(persistence, header, location);
339
+ if (!directory || !/^session\.v3\.jsonl(?:\.zstd)?$/.test(basename(location.path))) throw new Error("\u5F53\u524D\u5B58\u50A8\u540E\u7AEF\u6216\u683C\u5F0F\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D");
340
+ return prepareAutomationRepair({ directory, target: location.path, sessionId, format: { ...persistence.generationFormat, validateBytes: async (bytes) => {
341
+ if (location.path.endsWith(".zstd")) {
342
+ if (typeof persistence.readZstdPrefix !== "function") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u7F3A\u5C11\u538B\u7F29\u65E5\u5FD7\u6821\u9A8C\u80FD\u529B");
343
+ const parsed = await persistence.readZstdPrefix(bytes);
344
+ if (parsed.meta.id !== sessionId || parsed.tornTruncateTo !== void 0) throw new Error("\u751F\u6210\u7684\u538B\u7F29\u65E5\u5FD7\u672A\u901A\u8FC7\u5BBF\u4E3B\u6821\u9A8C");
345
+ }
346
+ }, validate: (artifact) => {
347
+ if (typeof SessionRuntime.Session?.fromRestore !== "function") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u7F3A\u5C11\u5B8C\u6574\u4F1A\u8BDD\u6821\u9A8C\u80FD\u529B");
348
+ SessionRuntime.Session.fromRestore(artifact.header.id, artifact.events, artifact.header, artifact.inheritedEventCount, "detached");
349
+ } } });
350
+ }
351
+ async ensureSessionRepairIdle(sessionId) {
352
+ if (!this.requireState().archivedSessionIds.includes(sessionId)) throw new Error("\u8BF7\u5148\u5F52\u6863\u4F1A\u8BDD\uFF0C\u518D\u8FDB\u884C\u4FEE\u590D");
353
+ if (this.ctx.get("sessions")?.get(sessionId)) throw new Error("\u4F1A\u8BDD\u4ECD\u5728\u5BBF\u4E3B\u5185\u6253\u5F00\uFF0C\u8BF7\u5173\u95ED\u4F1A\u8BDD\u5E76\u91CD\u542F DSH \u540E\u4FEE\u590D");
354
+ }
355
+ async diagnoseSession(input) {
356
+ const { sessionId } = repairInputSchema.parse(input);
357
+ const result = { sessionId, repairable: false, repaired: false, token: "", count: 0, reason: "", advice: "" };
358
+ try {
359
+ if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
360
+ await this.readConversationEvents(sessionId);
361
+ return { ...result, code: "healthy", reason: "\u4F1A\u8BDD\u5DF2\u53EF\u6B63\u5E38\u8BFB\u53D6", advice: "\u5237\u65B0\u4F1A\u8BDD\u8BE6\u60C5\u5373\u53EF\uFF0C\u65E0\u9700\u4FEE\u590D\u3002" };
362
+ } catch (error) {
363
+ const diagnostic = classifySessionError(error);
364
+ result.code = diagnostic.code;
365
+ result.reason = diagnostic.reason;
366
+ result.advice = diagnostic.advice;
367
+ }
368
+ try {
369
+ const plan = await this.sessionRepairPlan(sessionId);
370
+ return {
371
+ ...result,
372
+ code: plan.correctingFrame ? "ready-frame" : "ready",
373
+ repairable: true,
374
+ token: plan.token,
375
+ count: plan.count,
376
+ reason: plan.correctingFrame ? "\u53D1\u73B0\u65E7\u7248\u4FEE\u590D\u7684\u538B\u7F29\u5E27\u95EE\u9898\uFF0C\u5DF2\u901A\u8FC7\u5BBF\u4E3B\u8BFB\u53D6\u6821\u9A8C" : `\u53D1\u73B0 ${plan.count} \u5904\u65E7\u81EA\u52A8\u5316\u6765\u6E90\uFF0C\u5DF2\u901A\u8FC7\u5BBF\u4E3B\u683C\u5F0F\u8F6C\u6362\u6821\u9A8C`,
377
+ advice: "\u5C06\u6765\u6E90\u8F6C\u6362\u4E3A dsh-automation \u63D2\u4EF6\u5F52\u5C5E\uFF0C\u4FDD\u7559\u4EFB\u52A1\u4FE1\u606F\u4E0E\u6B63\u6587\uFF0C\u751F\u6210\u65B0\u7248\u65E5\u5FD7\uFF1B\u65E7\u65E5\u5FD7\u539F\u6837\u4FDD\u7559\u3002\u8BF7\u786E\u4FDD\u5176\u4ED6 DSH \u8FDB\u7A0B\u672A\u6253\u5F00\u8BE5\u4F1A\u8BDD\u3002"
378
+ };
379
+ } catch (error) {
380
+ return {
381
+ ...result,
382
+ code: result.code === "unknown" ? "blocked" : result.code,
383
+ advice: (result.advice + " \u6682\u4E0D\u80FD\u81EA\u52A8\u4FEE\u590D\uFF1A" + String(error?.message ?? error)).slice(0, 1e3)
384
+ };
385
+ }
386
+ }
387
+ async repairSession(input) {
388
+ const { sessionId, token } = repairInputSchema.parse(input);
389
+ if (!token) throw new Error("\u8BF7\u5148\u8BCA\u65AD\uFF0C\u518D\u786E\u8BA4\u4FEE\u590D");
390
+ return this.enqueueOperation(async () => {
391
+ const plan = await this.sessionRepairPlan(sessionId);
392
+ const persistence = this.ctx.get("sessionPersistence");
393
+ if (typeof persistence.acquireLease !== "function") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u7F3A\u5C11\u4F1A\u8BDD\u5199\u9501\uFF0C\u65E0\u6CD5\u5B89\u5168\u4FEE\u590D");
394
+ const lease = await persistence.acquireLease(sessionId, void 0, dirname(plan.source));
395
+ try {
396
+ await plan.publish(token, () => this.ensureSessionRepairIdle(sessionId));
397
+ } finally {
398
+ await lease.release();
399
+ }
400
+ try {
401
+ await this.readConversationEvents(sessionId);
402
+ } catch {
403
+ return { sessionId, repairable: false, repaired: false, token: "", count: plan.count, code: "published-unreadable", reason: "\u65B0\u7248\u65E5\u5FD7\u5DF2\u751F\u6210\uFF0C\u4F46\u5BBF\u4E3B\u590D\u8BFB\u672A\u901A\u8FC7", advice: "\u65E7\u65E5\u5FD7\u4ECD\u4FDD\u7559\uFF0C\u8BF7\u91CD\u542F DSH \u540E\u91CD\u65B0\u8BCA\u65AD\uFF1B\u4E0D\u8981\u53CD\u590D\u4FEE\u590D\u6216\u5220\u9664\u65E5\u5FD7\u3002" };
404
+ }
405
+ return { sessionId, repairable: false, repaired: true, token: "", count: plan.count, code: "repaired", reason: "\u4FEE\u590D\u5B8C\u6210\uFF0C\u5BBF\u4E3B\u5DF2\u80FD\u6B63\u5E38\u8BFB\u53D6", advice: "\u65E7\u65E5\u5FD7\u4FDD\u7559\uFF0C\u5F52\u6863\u72B6\u6001\u4E0D\u53D8\uFF1B\u53EF\u4EE5\u91CD\u65B0\u9884\u89C8\u6216\u6253\u5F00\u4F1A\u8BDD\u3002" };
406
+ });
407
+ }
408
+ /** 已归档与未归档共用正文检索;只读已知会话,不激活或改变归档状态。 */
409
+ async searchSessionContent(input) {
410
+ const { sessionIds, query } = searchInputSchema.parse(input);
411
+ const items = [], failures = [];
412
+ for (const sessionId of sessionIds) {
413
+ try {
414
+ if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
415
+ const messages = extractConversation(await this.readConversationEvents(sessionId));
416
+ if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u5DF2\u79FB\u9664\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
417
+ const match = findContentMatch(messages, query);
418
+ if (match) items.push({ sessionId, ...match });
419
+ } catch (error) {
420
+ failures.push({ sessionId, message: String(error?.message ?? error).slice(0, 500) });
421
+ }
422
+ }
423
+ return { items, failures };
424
+ }
425
+ /** 输入为限定批次的归档 ID 和关键词,逐条返回命中或失败。 */
426
+ async searchArchivedContent(input) {
427
+ const { sessionIds, query } = searchInputSchema.parse(input);
428
+ const items = [], failures = [];
429
+ for (const sessionId of sessionIds) {
430
+ try {
431
+ const match = findContentMatch(await this.readArchivedConversation(sessionId), query);
432
+ if (match) items.push({ sessionId, ...match });
433
+ } catch (error) {
434
+ failures.push({ sessionId, message: String(error?.message ?? error).slice(0, 500) });
435
+ }
436
+ }
437
+ return { items, failures };
438
+ }
439
+ /** 返回最近八条对话或关键词附近的八条,单条最多 2000 字,不写入宿主数据。 */
440
+ async previewArchivedSession(input) {
441
+ const { sessionId, query } = previewInputSchema.parse(input);
442
+ return { sessionId, ...previewConversation(await this.readArchivedConversation(sessionId), query) };
443
+ }
444
+ /** 独立收藏域不改变官方工作区数据结构;旧版本回退时可保留收藏。 */
445
+ async favoriteDomain() {
446
+ if (!this.favoriteDomainPromise) {
447
+ this.favoriteDomainPromise = this.ctx.storageDomain.open(favoriteDomainSpec).then((domain) => {
448
+ this.ctx.effect(() => () => domain.close(), "archive-manager: \u6536\u85CF\u5B58\u50A8\u5173\u95ED");
449
+ return domain;
450
+ }).catch((error) => {
451
+ this.favoriteDomainPromise = void 0;
452
+ throw error;
453
+ });
454
+ }
455
+ return this.favoriteDomainPromise;
456
+ }
457
+ /** 返回宿主保存的收藏;读取时清理已确认不存在的会话。 */
458
+ async favoriteSessions() {
459
+ return this.enqueueOperation(async () => {
460
+ const domain = await this.favoriteDomain();
461
+ const state = favoriteStateSchema.parse(domain.global.get());
462
+ const kept = [];
463
+ for (const id of state.favoriteSessionIds) {
464
+ if (this.ctx.get("sessions")?.get(id) !== void 0 || !await this.sessionArtifactMissing(id)) kept.push(id);
465
+ }
466
+ if (kept.length !== state.favoriteSessionIds.length) await domain.global.set({ favoriteSessionIds: kept });
467
+ return { favoriteSessionIds: kept };
468
+ });
469
+ }
470
+ /** 设置单条收藏,串行写入避免不同浏览器相互覆盖。 */
471
+ async setSessionFavorite(input) {
472
+ const { sessionId, favorite } = favoriteInputSchema.parse(input);
473
+ return this.enqueueOperation(async () => {
474
+ if (favorite && !await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u6536\u85CF");
475
+ const domain = await this.favoriteDomain();
476
+ const state = favoriteStateSchema.parse(domain.global.get());
477
+ const ids = new Set(state.favoriteSessionIds);
478
+ if (favorite) ids.add(sessionId);
479
+ else ids.delete(sessionId);
480
+ const next = favoriteStateSchema.parse({ favoriteSessionIds: [...ids] });
481
+ await domain.global.set(next);
482
+ return next;
483
+ });
484
+ }
283
485
  /** 归档集合未变时复用反查表,避免每次 sessionPath 都扫全部工作区记账。 */
284
486
  archivedWorkspacePath(sessionId) {
285
487
  const state = this.requireState();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.43",
3
+ "version": "1.0.0",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
@@ -23,7 +23,8 @@
23
23
  "README.zh-CN.md",
24
24
  "LICENSE",
25
25
  "CHANGELOG.md",
26
- "CHANGELOG.zh-CN.md"
26
+ "CHANGELOG.zh-CN.md",
27
+ "RELEASE_NOTES.md"
27
28
  ],
28
29
  "scripts": {
29
30
  "build": "node scripts/build.mjs && node scripts/check-package.mjs",
@@ -38,22 +39,22 @@
38
39
  },
39
40
  "peerDependencies": {
40
41
  "@deepseek-ai/cordis": "^4.0.1",
41
- "@deepseek-ai/dsh-client-connection": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
42
- "@deepseek-ai/dsh-client-locale": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
43
- "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
44
- "@deepseek-ai/dsh-client-store": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
45
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
46
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
47
- "@deepseek-ai/dsh-client-ui-sidebar": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
48
- "@deepseek-ai/dsh-client-ui-workspace": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
49
- "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
50
- "@deepseek-ai/dsh-session": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
51
- "@deepseek-ai/dsh-session-projection-cache": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
52
- "@deepseek-ai/dsh-spill-local": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
53
- "@deepseek-ai/dsh-storage-domain": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
54
- "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
55
- "@deepseek-ai/dsh-typert-registry": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1",
56
- "@deepseek-ai/dsh-workspace": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1"
42
+ "@deepseek-ai/dsh-client-connection": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
43
+ "@deepseek-ai/dsh-client-locale": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
44
+ "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
45
+ "@deepseek-ai/dsh-client-store": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
46
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
47
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
48
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
49
+ "@deepseek-ai/dsh-client-ui-workspace": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
50
+ "@deepseek-ai/dsh-host-webserver": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
51
+ "@deepseek-ai/dsh-session": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
52
+ "@deepseek-ai/dsh-session-projection-cache": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
53
+ "@deepseek-ai/dsh-spill-local": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
54
+ "@deepseek-ai/dsh-storage-domain": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
55
+ "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
56
+ "@deepseek-ai/dsh-typert-registry": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2",
57
+ "@deepseek-ai/dsh-workspace": "0.1.0-rc.8 || 0.1.1-rc.2 || 0.1.2-rc.1 || 0.1.5-rc.1 || 0.1.5-rc.2 || 0.1.6-alpha.1 || 0.1.6-alpha.2"
57
58
  },
58
59
  "peerDependenciesMeta": {
59
60
  "@deepseek-ai/dsh-client-runtime": {
@@ -65,30 +66,30 @@
65
66
  },
66
67
  "devDependencies": {
67
68
  "@deepseek-ai/cordis": "4.0.2",
68
- "@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.1",
69
- "@deepseek-ai/dsh-client-connection": "0.1.6-alpha.1",
70
- "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.1",
71
- "@deepseek-ai/dsh-client-store": "0.1.6-alpha.1",
72
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.6-alpha.1",
73
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.1",
74
- "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.1",
75
- "@deepseek-ai/dsh-client-ui-sidebar": "0.1.6-alpha.1",
76
- "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.1",
77
- "@deepseek-ai/dsh-client-ui-workspace": "0.1.6-alpha.1",
78
- "@deepseek-ai/dsh-host-webserver": "0.1.6-alpha.1",
79
- "@deepseek-ai/dsh-invariants": "0.1.6-alpha.1",
80
- "@deepseek-ai/dsh-scope": "0.1.6-alpha.1",
81
- "@deepseek-ai/dsh-session": "0.1.6-alpha.1",
82
- "@deepseek-ai/dsh-session-persistence": "0.1.6-alpha.1",
83
- "@deepseek-ai/dsh-session-projection": "0.1.6-alpha.1",
84
- "@deepseek-ai/dsh-session-projection-cache": "0.1.6-alpha.1",
85
- "@deepseek-ai/dsh-spill": "0.1.6-alpha.1",
86
- "@deepseek-ai/dsh-spill-local": "0.1.6-alpha.1",
87
- "@deepseek-ai/dsh-storage": "0.1.6-alpha.1",
88
- "@deepseek-ai/dsh-storage-domain": "0.1.6-alpha.1",
89
- "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.1",
90
- "@deepseek-ai/dsh-typert-registry": "0.1.6-alpha.1",
91
- "@deepseek-ai/dsh-workspace": "0.1.6-alpha.1",
69
+ "@deepseek-ai/dsh-api-gateway": "0.1.6-alpha.2",
70
+ "@deepseek-ai/dsh-client-connection": "0.1.6-alpha.2",
71
+ "@deepseek-ai/dsh-client-locale": "0.1.6-alpha.2",
72
+ "@deepseek-ai/dsh-client-store": "0.1.6-alpha.2",
73
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.6-alpha.2",
74
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.2",
75
+ "@deepseek-ai/dsh-client-ui-renderer": "0.1.6-alpha.2",
76
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.6-alpha.2",
77
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.6-alpha.2",
78
+ "@deepseek-ai/dsh-client-ui-workspace": "0.1.6-alpha.2",
79
+ "@deepseek-ai/dsh-host-webserver": "0.1.6-alpha.2",
80
+ "@deepseek-ai/dsh-invariants": "0.1.6-alpha.2",
81
+ "@deepseek-ai/dsh-scope": "0.1.6-alpha.2",
82
+ "@deepseek-ai/dsh-session": "0.1.6-alpha.2",
83
+ "@deepseek-ai/dsh-session-persistence": "0.1.6-alpha.2",
84
+ "@deepseek-ai/dsh-session-projection": "0.1.6-alpha.2",
85
+ "@deepseek-ai/dsh-session-projection-cache": "0.1.6-alpha.2",
86
+ "@deepseek-ai/dsh-spill": "0.1.6-alpha.2",
87
+ "@deepseek-ai/dsh-spill-local": "0.1.6-alpha.2",
88
+ "@deepseek-ai/dsh-storage": "0.1.6-alpha.2",
89
+ "@deepseek-ai/dsh-storage-domain": "0.1.6-alpha.2",
90
+ "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.2",
91
+ "@deepseek-ai/dsh-typert-registry": "0.1.6-alpha.2",
92
+ "@deepseek-ai/dsh-workspace": "0.1.6-alpha.2",
92
93
  "react": "18.3.1",
93
94
  "react-dom": "18.3.1",
94
95
  "esbuild": "0.25.0",