@michengai/dsh-archive-manager 1.0.0 → 1.0.1

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,15 @@
1
+ function record(value) {
2
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("\u53C2\u6570\u5FC5\u987B\u4E3A\u5BF9\u8C61");
3
+ return value;
4
+ }
5
+ function errorMessage(error) {
6
+ return String(error && typeof error === "object" && "message" in error ? error.message ?? error : error);
7
+ }
8
+ function errorCode(error) {
9
+ return error && typeof error === "object" && "code" in error ? error.code : void 0;
10
+ }
11
+ export {
12
+ errorCode,
13
+ errorMessage,
14
+ record
15
+ };
@@ -1,3 +1,4 @@
1
+ import { record } from "./contracts.js";
1
2
  import { spawn } from "node:child_process";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { readFile } from "node:fs/promises";
@@ -41,8 +42,9 @@ function profileNameFromArgv(argv) {
41
42
  }
42
43
  function isDshCliEntry(entry, manifest, packageRoot) {
43
44
  if (typeof manifest !== "object" || manifest === null) return false;
44
- if (manifest.name !== "@deepseek-ai/dsh") return false;
45
- const bin = typeof manifest.bin === "string" ? manifest.bin : typeof manifest.bin === "object" && manifest.bin !== null ? manifest.bin.dsh : void 0;
45
+ if (("name" in manifest ? manifest.name : void 0) !== "@deepseek-ai/dsh") return false;
46
+ const item = manifest;
47
+ const bin = typeof item.bin === "string" ? item.bin : typeof item.bin === "object" && item.bin !== null ? "dsh" in item.bin ? item.bin.dsh : void 0 : void 0;
46
48
  return typeof bin === "string" && bin !== "" && !isAbsolute(bin) && resolve(packageRoot, bin) === resolve(entry);
47
49
  }
48
50
  function cliEntry() {
@@ -120,7 +122,7 @@ async function latestVersion(packageName) {
120
122
  try {
121
123
  const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`, { signal: AbortSignal.timeout(8e3) });
122
124
  if (!response.ok) return void 0;
123
- const value = await response.json();
125
+ const value = record(await response.json());
124
126
  if (typeof value.version !== "string" || value.version === "") return void 0;
125
127
  latestCache = { packageName, version: value.version, expiresAt: Date.now() + 5 * 6e4 };
126
128
  return value.version;
@@ -129,7 +131,7 @@ async function latestVersion(packageName) {
129
131
  }
130
132
  }
131
133
  async function currentVersion(manifestUrl) {
132
- const value = JSON.parse(await readFile(manifestUrl, "utf8"));
134
+ const value = record(JSON.parse(await readFile(manifestUrl, "utf8")));
133
135
  if (typeof value.version !== "string" || value.version === "") throw new Error("\u65E0\u6CD5\u8BFB\u53D6\u5F53\u524D\u63D2\u4EF6\u7248\u672C\u3002");
134
136
  return value.version;
135
137
  }
@@ -148,8 +150,9 @@ async function status(options, target) {
148
150
  }
149
151
  async function runCliInstall(target, packageSpec) {
150
152
  if (target.cliEntry === void 0) throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u81EA\u52A8\u66F4\u65B0\uFF0C\u8BF7\u4F7F\u7528\u624B\u5DE5\u66F4\u65B0\u547D\u4EE4\u3002");
153
+ const entry = target.cliEntry;
151
154
  await new Promise((resolvePromise, reject) => {
152
- const child = spawn(process.execPath, [target.cliEntry, "plugin", "--profile", target.profileName, "add", "--config.minimumReleaseAge=0", packageSpec, "--registry=https://registry.npmjs.org/"], {
155
+ const child = spawn(process.execPath, [entry, "plugin", "--profile", target.profileName, "add", "--config.minimumReleaseAge=0", packageSpec, "--registry=https://registry.npmjs.org/"], {
153
156
  cwd: target.profileDir,
154
157
  windowsHide: true,
155
158
  stdio: ["ignore", "pipe", "pipe"],
package/lib/projcache.js CHANGED
@@ -1,3 +1,4 @@
1
+ const CacheBase = SessionProjectionCache;
1
2
  import { createHash } from "node:crypto";
2
3
  import { Service } from "@deepseek-ai/cordis";
3
4
  import {
@@ -115,7 +116,7 @@ async function readLegacySafeRecords(ctx, spec, label) {
115
116
  const source = await readSourceRecords(ctx, spec, label);
116
117
  const records = [];
117
118
  for (const [physicalKey, stored] of source.records) {
118
- if (stored === null || typeof stored !== "object" || typeof stored.sessionId !== "string") {
119
+ if (stored === null || typeof stored !== "object" || !("sessionId" in stored) || typeof stored.sessionId !== "string") {
119
120
  ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its session id is invalid`);
120
121
  continue;
121
122
  }
@@ -123,22 +124,23 @@ async function readLegacySafeRecords(ctx, spec, label) {
123
124
  ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its storage key does not match the session id`);
124
125
  continue;
125
126
  }
126
- records.push([stored.sessionId, unwrapStoredCheckpoint(stored)]);
127
+ records.push([stored.sessionId, { identity: "identity" in stored ? stored.identity : void 0, rows: "rows" in stored ? stored.rows : void 0 }]);
127
128
  }
128
129
  return { ...source, records };
129
130
  }
130
131
  function normalizeLegacyRecord(record) {
131
- if (record === null || typeof record !== "object" || record.identity === null || typeof record.identity !== "object") return void 0;
132
+ if (record === null || typeof record !== "object" || !("identity" in record) || record.identity === null || typeof record.identity !== "object") return void 0;
132
133
  const identity = record.identity;
133
134
  const inheritedEventCount = identity.inheritedEventCount;
134
- const validOffset = Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0;
135
+ const validOffset = typeof inheritedEventCount === "number" && Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0;
135
136
  if (identity.isSeeded === true) {
136
137
  if (!validOffset) return void 0;
138
+ checkpointRecord.parse(record);
137
139
  return record;
138
140
  }
139
141
  if (identity.isSeeded !== void 0 && identity.isSeeded !== false) return void 0;
140
142
  if (inheritedEventCount !== void 0 && inheritedEventCount !== 0) return void 0;
141
- return {
143
+ const normalized = {
142
144
  ...record,
143
145
  identity: {
144
146
  ...identity,
@@ -146,6 +148,8 @@ function normalizeLegacyRecord(record) {
146
148
  inheritedEventCount: 0
147
149
  }
148
150
  };
151
+ checkpointRecord.parse(normalized);
152
+ return normalized;
149
153
  }
150
154
  async function importMissingRecords(ctx, target, records, label) {
151
155
  let imported = 0;
@@ -180,7 +184,7 @@ async function importPreviousProjectionCache(ctx, target) {
180
184
  if (legacy.opened) imported += await importMissingRecords(ctx, target, legacy.records, "legacy v3");
181
185
  return imported;
182
186
  }
183
- var ArchiveProjectionCache = class extends SessionProjectionCache {
187
+ var ArchiveProjectionCache = class extends CacheBase {
184
188
  /** 已永久删除的会话:其投影缓存行不再允许写入。 */
185
189
  deletedSessionIds = /* @__PURE__ */ new Set();
186
190
  /** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
@@ -234,7 +238,7 @@ var ArchiveProjectionCache = class extends SessionProjectionCache {
234
238
  }
235
239
  /** rc.2 公开的 fail-soft 辅助在 alpha.2 已移除;保留同形兼容入口。 */
236
240
  async putSoft(id, identity, rows, what) {
237
- const upstream = SessionProjectionCache.prototype.putSoft;
241
+ const upstream = CacheBase.prototype.putSoft;
238
242
  if (typeof upstream === "function") return upstream.call(this, id, identity, rows, what);
239
243
  try {
240
244
  await this.put(id, identity, rows);
@@ -1,9 +1,16 @@
1
+ import { record } from "./contracts.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { lstat, readFile, readdir, open, link, unlink, realpath, rename } from "node:fs/promises";
3
4
  import { join, resolve, basename } from "node:path";
4
5
  import * as zlib from "node:zlib";
5
6
  import { classifySessionError } from "./archive-discovery.js";
6
7
  const LIMIT = 32 * 1024 * 1024;
8
+ function decompressFrame(bytes, maxOutputLength) {
9
+ const result = record(zlib.zstdDecompressSync(bytes, { info: true, maxOutputLength }));
10
+ const engine = record(result.engine);
11
+ if (!Buffer.isBuffer(result.buffer) || typeof engine.bytesWritten !== "number") throw new Error("\u538B\u7F29\u5E27\u8FD4\u56DE\u683C\u5F0F\u65E0\u6548");
12
+ return { buffer: result.buffer, engine: { bytesWritten: engine.bytesWritten } };
13
+ }
7
14
  const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
8
15
  function decodeRepairLog(bytes, compressed) {
9
16
  if (bytes.length > LIMIT) throw new Error("\u65E5\u5FD7\u8D85\u8FC7 32 MB\uFF0C\u9700\u79BB\u7EBF\u5904\u7406");
@@ -11,7 +18,7 @@ function decodeRepairLog(bytes, compressed) {
11
18
  let offset = 0, size = 0;
12
19
  if (compressed && typeof zlib.zstdDecompressSync !== "function") throw new Error("\u5F53\u524D Node.js \u4E0D\u652F\u6301\u538B\u7F29\u65E5\u5FD7\u8BCA\u65AD");
13
20
  while (compressed && offset < bytes.length) {
14
- const result = zlib.zstdDecompressSync(bytes.subarray(offset), { info: true, maxOutputLength: LIMIT - size });
21
+ const result = decompressFrame(bytes.subarray(offset), LIMIT - size);
15
22
  if (!result.engine.bytesWritten) throw new Error("\u538B\u7F29\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574\u5E27");
16
23
  offset += result.engine.bytesWritten;
17
24
  size += result.buffer.length;
@@ -20,25 +27,30 @@ function decodeRepairLog(bytes, compressed) {
20
27
  }
21
28
  const text = new TextDecoder("utf-8", { fatal: true }).decode(compressed ? Buffer.concat(chunks) : bytes);
22
29
  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));
30
+ return text.trimEnd().split("\n").map((line) => record(JSON.parse(line)));
24
31
  }
25
32
  function normalizeAutomationSources(input) {
26
33
  const rows = structuredClone(input);
27
34
  let count = 0;
28
- const fix = (message) => {
29
- const source = message?.source;
35
+ const fix = (value) => {
36
+ if (!value || typeof value !== "object") return;
37
+ const message = record(value);
38
+ const candidate = message.source;
39
+ if (!candidate || typeof candidate !== "object") return;
40
+ const source = record(candidate);
30
41
  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))) {
42
+ 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(String(source.scheduledFor)))) {
32
43
  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
44
  }
34
45
  message.source = { kind: "plugin", plugin: "dsh-automation", form: "notice", summary: JSON.stringify(source) };
35
46
  count++;
36
47
  };
37
48
  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);
49
+ const data = row.data && typeof row.data === "object" ? record(row.data) : {};
50
+ if (row.type === "user/message") fix(data);
51
+ if (typeof row.type === "string" && ["assistant/message", "tool/result"].includes(row.type)) fix(data.message);
52
+ if (row.type === "agent/inbox/spliced") Array.isArray(data.inserted) && data.inserted.forEach(fix);
53
+ if (row.type === "session/title-llm-request") Array.isArray(data.messages) && data.messages.forEach(fix);
42
54
  }
43
55
  return { rows, count };
44
56
  }
@@ -79,7 +91,7 @@ async function prepareAutomationRepair({ directory, target, sessionId, format })
79
91
  await regularFile(target);
80
92
  previousTarget = await readFile(target);
81
93
  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 });
94
+ const first = decompressFrame(previousTarget, LIMIT);
83
95
  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
96
  }
85
97
  const token = digest(previousTarget ? Buffer.concat([bytes, previousTarget]) : bytes);
package/lib/workspace.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { record, errorMessage, errorCode } from "./contracts.js";
1
2
  import * as SessionRuntime from "@deepseek-ai/dsh-session";
2
3
  import { prepareAutomationRepair } from "./session-repair.js";
3
4
  import { repairInputSchema, classifySessionError, detailsInputSchema, countConversationTurns, discoveryInvocations, searchInputSchema, previewInputSchema, extractConversation, findContentMatch, previewConversation } from "./archive-discovery.js";
@@ -13,6 +14,7 @@ const favoriteDomainSpec = defineDomain({
13
14
  name: "archive_manager_favorites",
14
15
  version: 1,
15
16
  tables: {},
17
+ // 宿主运行时仅调用 parse/safeParse;兼容早期同构 schema,不新增 Zod 运行依赖。
16
18
  global: { schema: favoriteStateSchema, initial: { favoriteSessionIds: [] } }
17
19
  });
18
20
  function jsonlSessionDirectory(persistence, header, location) {
@@ -58,7 +60,8 @@ function markRemoteMethod(instance, method) {
58
60
  fn.call(instance);
59
61
  }
60
62
  };
61
- Remote(method)(void 0, context);
63
+ Remote(method)(function() {
64
+ }, { ...context, kind: "method", access: { has: (value) => method in value, get: (value) => Reflect.get(value, method) }, metadata: void 0 });
62
65
  }
63
66
  function headerIdentity(header) {
64
67
  return {
@@ -76,7 +79,8 @@ const sessionIdSchema = {
76
79
  }
77
80
  };
78
81
  const archivedSetSchema = {
79
- parse(value) {
82
+ parse(input) {
83
+ const value = record(input);
80
84
  if (typeof value !== "object" || value === null || Array.isArray(value))
81
85
  throw new TypeError("result must be an object");
82
86
  const ids = value.archivedSessionIds;
@@ -86,14 +90,16 @@ const archivedSetSchema = {
86
90
  }
87
91
  };
88
92
  const deletedSchema = {
89
- parse(value) {
93
+ parse(input) {
94
+ const value = record(input);
90
95
  if (typeof value !== "object" || value === null || Array.isArray(value) || value.deleted !== true)
91
96
  throw new TypeError("deleted must be true");
92
97
  return value;
93
98
  }
94
99
  };
95
100
  const archivedBatchTargetSchema = {
96
- parse(value) {
101
+ parse(input) {
102
+ const value = record(input);
97
103
  if (typeof value !== "object" || value === null || Array.isArray(value))
98
104
  throw new TypeError("target must be an object");
99
105
  if (value.scope === "all" || value.scope === "ungrouped") return value;
@@ -107,7 +113,8 @@ const archivedBatchTargetSchema = {
107
113
  }
108
114
  };
109
115
  const deletedBatchSchema = {
110
- parse(value) {
116
+ parse(input) {
117
+ const value = record(input);
111
118
  if (typeof value !== "object" || value === null || Array.isArray(value))
112
119
  throw new TypeError("result must be an object");
113
120
  for (const key of [
@@ -126,7 +133,8 @@ const deletedBatchSchema = {
126
133
  }
127
134
  };
128
135
  const archivedSessionMetadataSchema = {
129
- parse(value) {
136
+ parse(input) {
137
+ const value = record(input);
130
138
  if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.items))
131
139
  throw new TypeError("result.items must be an array");
132
140
  if (value.items.some(
@@ -318,11 +326,11 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
318
326
  item.path = path;
319
327
  }
320
328
  } catch (error) {
321
- item.error = "\u8DEF\u5F84\u4E0D\u53EF\u7528\uFF1A" + String(error?.message ?? error).slice(0, 350);
329
+ item.error = "\u8DEF\u5F84\u4E0D\u53EF\u7528\uFF1A" + errorMessage(error).slice(0, 350);
322
330
  }
323
331
  item.turnCount = countConversationTurns(await this.readConversationEvents(sessionId));
324
332
  } catch (error) {
325
- item.error = String(error?.message ?? error).slice(0, 500);
333
+ item.error = errorMessage(error).slice(0, 500);
326
334
  }
327
335
  items.push(item);
328
336
  }
@@ -354,7 +362,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
354
362
  }
355
363
  async diagnoseSession(input) {
356
364
  const { sessionId } = repairInputSchema.parse(input);
357
- const result = { sessionId, repairable: false, repaired: false, token: "", count: 0, reason: "", advice: "" };
365
+ const result = { code: "unknown", sessionId, repairable: false, repaired: false, token: "", count: 0, reason: "", advice: "" };
358
366
  try {
359
367
  if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
360
368
  await this.readConversationEvents(sessionId);
@@ -380,7 +388,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
380
388
  return {
381
389
  ...result,
382
390
  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)
391
+ advice: (result.advice + " \u6682\u4E0D\u80FD\u81EA\u52A8\u4FEE\u590D\uFF1A" + errorMessage(error)).slice(0, 1e3)
384
392
  };
385
393
  }
386
394
  }
@@ -417,7 +425,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
417
425
  const match = findContentMatch(messages, query);
418
426
  if (match) items.push({ sessionId, ...match });
419
427
  } catch (error) {
420
- failures.push({ sessionId, message: String(error?.message ?? error).slice(0, 500) });
428
+ failures.push({ sessionId, message: errorMessage(error).slice(0, 500) });
421
429
  }
422
430
  }
423
431
  return { items, failures };
@@ -431,7 +439,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
431
439
  const match = findContentMatch(await this.readArchivedConversation(sessionId), query);
432
440
  if (match) items.push({ sessionId, ...match });
433
441
  } catch (error) {
434
- failures.push({ sessionId, message: String(error?.message ?? error).slice(0, 500) });
442
+ failures.push({ sessionId, message: errorMessage(error).slice(0, 500) });
435
443
  }
436
444
  }
437
445
  return { items, failures };
@@ -491,10 +499,10 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
491
499
  const archived = new Set(state.archivedSessionIds);
492
500
  const table = this.requireTable();
493
501
  for (const workspaceId of state.workspaceIds) {
494
- const record = table.get(workspaceId);
495
- if (record === void 0) continue;
496
- for (const id of record.sessionIds) {
497
- if (archived.has(id)) index.set(id, record.path);
502
+ const record2 = table.get(workspaceId);
503
+ if (record2 === void 0) continue;
504
+ for (const id of record2.sessionIds) {
505
+ if (archived.has(id)) index.set(id, record2.path);
498
506
  }
499
507
  }
500
508
  this.archivedSessionPathIndex = index;
@@ -713,9 +721,9 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
713
721
  if (path === void 0) return;
714
722
  const table = this.requireTable();
715
723
  for (const workspaceId of this.requireState().workspaceIds) {
716
- const record = table.get(workspaceId);
717
- if (record === void 0 || record.path !== path) continue;
718
- if (record.sessionIds.includes(sessionId)) return;
724
+ const record2 = table.get(workspaceId);
725
+ if (record2 === void 0 || record2.path !== path) continue;
726
+ if (record2.sessionIds.includes(sessionId)) return;
719
727
  const entity = this.entities.get(workspaceId);
720
728
  if (entity !== void 0) await entity.attachSession(sessionId);
721
729
  return;
@@ -774,8 +782,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
774
782
  }
775
783
  }
776
784
  /** 以归档集合顺序解析批量目标,避免依赖浏览器尚未加载完整的摘要投影。 */
777
- archivedSessionIdsForTarget(target) {
778
- target = archivedBatchTargetSchema.parse(target);
785
+ archivedSessionIdsForTarget(input) {
786
+ const target = archivedBatchTargetSchema.parse(input);
779
787
  const state = this.requireState();
780
788
  const archivedSessionIds = [...new Set(state.archivedSessionIds)];
781
789
  if (target.scope === "all") return archivedSessionIds;
@@ -931,11 +939,11 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
931
939
  /** 统一旧版头部数组与 0.1.3 的持久化快照,供父类索引和本插件枚举共用。 */
932
940
  async listStoredHeaders() {
933
941
  return (await this.ctx.sessionPersistence.list()).map(
934
- (item) => item.header ?? item
942
+ (item) => "header" in item && item.header ? item.header : item
935
943
  );
936
944
  }
937
945
  async indexHeaders(items) {
938
- for (const item of items) await this.indexHeader(item.header ?? item);
946
+ for (const item of items) await this.indexHeader("header" in item && item.header ? item.header : item);
939
947
  }
940
948
  /** 为未处于实时状态的持久化会话发布相同的移除事件。 */
941
949
  async publishColdSessionRemoval(sessionId, sessions) {
@@ -982,7 +990,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
982
990
  try {
983
991
  stat = await lstat(path);
984
992
  } catch (error) {
985
- if (error?.code === "ENOENT") continue;
993
+ if (errorCode(error) === "ENOENT") continue;
986
994
  throw error;
987
995
  }
988
996
  if (stat.isSymbolicLink())
@@ -1013,8 +1021,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
1013
1021
  const table = this.requireTable();
1014
1022
  const state = this.requireState();
1015
1023
  for (const workspaceId of state.workspaceIds) {
1016
- const record = table.get(workspaceId);
1017
- if (record === void 0 || !record.sessionIds.includes(sessionId)) continue;
1024
+ const record2 = table.get(workspaceId);
1025
+ if (record2 === void 0 || !record2.sessionIds.includes(sessionId)) continue;
1018
1026
  const next = await table.update(workspaceId, (current) => ({
1019
1027
  ...current,
1020
1028
  sessionIds: current.sessionIds.filter((id) => id !== sessionId),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
@@ -23,19 +23,20 @@
23
23
  "README.zh-CN.md",
24
24
  "LICENSE",
25
25
  "CHANGELOG.md",
26
- "CHANGELOG.zh-CN.md",
27
- "RELEASE_NOTES.md"
26
+ "CHANGELOG.zh-CN.md"
28
27
  ],
29
28
  "scripts": {
30
- "build": "node scripts/build.mjs && node scripts/check-package.mjs",
31
- "test": "pnpm build && node --test test/*.test.mjs",
29
+ "build": "pnpm typecheck && node scripts/build.mjs && node scripts/check-package.mjs",
30
+ "test": "pnpm build && node --import tsx --test test/*.test.mjs",
32
31
  "test:matrix": "pnpm build && node scripts/test-version-matrix.mjs",
33
32
  "test:latest": "pnpm build && node scripts/test-latest-compatibility.mjs",
34
- "test:compat": "node scripts/test-legacy-compatibility.mjs && pnpm test:matrix",
33
+ "test:compat": "pnpm build && node scripts/test-legacy-compatibility.mjs && pnpm test:matrix",
35
34
  "pack:check": "pnpm pack --dry-run",
36
35
  "verify": "pnpm test && pnpm test:compat && pnpm pack:check && node scripts/check-generated.mjs",
37
36
  "release:preflight": "pwsh -NoProfile -File scripts/release-preflight.ps1",
38
- "prepublishOnly": "pnpm verify"
37
+ "prepublishOnly": "pnpm verify",
38
+ "prepack": "pnpm build",
39
+ "typecheck": "tsc --noEmit"
39
40
  },
40
41
  "peerDependencies": {
41
42
  "@deepseek-ai/cordis": "^4.0.1",
@@ -90,12 +91,17 @@
90
91
  "@deepseek-ai/dsh-typert-protocol": "0.1.6-alpha.2",
91
92
  "@deepseek-ai/dsh-typert-registry": "0.1.6-alpha.2",
92
93
  "@deepseek-ai/dsh-workspace": "0.1.6-alpha.2",
93
- "react": "18.3.1",
94
- "react-dom": "18.3.1",
94
+ "@types/node": "^22.20.4",
95
+ "@types/react": "^18.3.31",
96
+ "@types/react-dom": "^18.0.0",
95
97
  "esbuild": "0.25.0",
96
- "zustand": "4.4.7",
97
98
  "immer": "10.1.1",
98
- "semver": "7.7.2"
99
+ "react": "18.3.1",
100
+ "react-dom": "18.3.1",
101
+ "semver": "7.7.2",
102
+ "tsx": "^4.23.13",
103
+ "typescript": "5.9.3",
104
+ "zustand": "4.4.7"
99
105
  },
100
106
  "license": "Apache-2.0",
101
107
  "dsh": {
package/RELEASE_NOTES.md DELETED
@@ -1,113 +0,0 @@
1
- ## 中文说明
2
-
3
- 从 0.1.44 升级到首个 1.0 正式版本,集中完善会话整理、检索、预览与异常修复。沿用原有安装包名和数据,不要求迁移会话;现有归档、恢复、永久删除及原生会话导航继续保留。
4
-
5
- ### 会话列表与统一筛选
6
-
7
- - 已归档、未归档两页统一支持标题/正文搜索、项目、收藏和更新时间范围筛选;默认只搜索标题,搜索框左侧切换范围,右侧打开日期选择器。结束日期包含本地当天,支持独立清除日期及点击外部关闭。
8
- - 使用紧凑项目分组列表,项目可折叠;标题点击打开原生会话,星标、恢复/归档使用右侧图标,更多菜单提供恢复打开、预览、复制与删除等适用操作。
9
- - 支持更新时间、创建时间、标题和轮次升降序排序;分组计数跟随筛选,空态区分当前页签。
10
- - 全选只覆盖当前筛选结果,包含折叠组中的匹配项;切页签清空选择,改变筛选保留选择并提示隐藏选中项。项目菜单仍操作整个项目,不受筛选影响。
11
- - 移除重复的页面级「全部恢复/全部删除」按钮;保留勾选批量操作及项目菜单,操作结果按页签隔离。
12
-
13
- ### 收藏、闲置整理与批量操作
14
-
15
- - 两页均可收藏/取消收藏,通过下拉菜单只看收藏;收藏保存于宿主,归档、恢复和更换浏览器后保留。索引暂时未知或读取失败不会清除收藏,只有确证工件缺失时才清理。
16
- - 未归档页支持设置 1~36500 个整数闲置天数,结合当前筛选预览候选;确认清单可逐条取消。排除收藏、当前打开、正在执行及等待交互的会话,并考虑子代理活动;无有效活动时间不参与。
17
- - 归档前再次检查收藏和活动状态。批量归档、恢复、删除显示进度及成功/跳过/失败/未处理明细;遇到首个失败停止,允许只重试剩余项,删除重试仍需确认。
18
- - 支持「撤回本次归档」,恢复最近一批实际成功归档及重试成功的会话;仅当前管理页打开期间有效,不撤回永久删除。
19
-
20
- ### 正文检索与快速预览
21
-
22
- - 在用户与助手的持久化正文中查找关键词,显示命中片段和高亮;读取失败显示明细并支持重试,更改筛选后旧响应不会覆盖新结果。
23
- - 已归档会话可不恢复直接预览,展示首个命中附近上下文或最近八条消息;未归档命中片段打开完整会话。
24
- - 预览支持 Markdown 表格、引用、代码块和角色区分,可切换原文高亮;加宽弹窗并补齐操作图标。
25
- - 修复大小写折叠引起的 Unicode 偏移错误、截断切开代理对、Esc 连带关闭设置,以及切换页签后预览自行重开。
26
-
27
- ### 轮次统计与会话定位
28
-
29
- - 两页显示用户提交轮次,包括图片消息及分支继承的用户消息,不把助手回复、工具调用或插件注入算作轮次。
30
- - 提供轮次最多/最少排序,未知轮次始终置后;详情读取失败可查看并重试。
31
- - 更多菜单支持分别复制会话 ID 和路径。官方 JSONL 后端复制会话目录,其他后端使用宿主返回的有效路径;路径缺失和剪贴板失败均明确提示。
32
-
33
- ### 异常诊断与定向修复
34
-
35
- - 已归档页汇总异常会话,包括缺少摘要的归档 ID;诊断区使用可折叠状态卡片,提供分类建议及技术详情。
36
- - 缺文件、权限、损坏等分类优先使用稳定错误码、兼容文案兜底;诊断发现不能自动修复时仍保留原分类,追加受限原因。
37
- - 对支持的旧自动化消息来源提供「诊断 → 确认修复」流程。以实际工件、宿主格式校验和写锁判断资格,不仅依赖错误措辞;要求已归档且未被任何 DSH 进程打开。
38
- - 保留原始日志、正文、任务归属和继承信息,经宿主重新编码与校验后发布新代际;修复文件使用私有权限。发布采用禁止覆盖策略,硬链接不可用时安全失败,不覆盖现有日志。
39
-
40
- ### 界面、国际化与兼容性
41
-
42
- - 对齐官方主题的选中、复选框、焦点、警告、更新按钮及高亮配色;完善日期下拉、诊断卡片、星标和操作图标。
43
- - 补齐诊断、预览、更新入口和错误提示的中英文;切换语言不再额外触发详情读取,修复复制错误和空态文案。
44
- - 更新已归档/未归档截图及双语 README,完整说明筛选、撤回、修复与检索边界。
45
- - 保持既有 DSH 支持范围: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;与 Codex UI 共存,不新增侧栏置顶或收藏功能。
46
- - 正式检索接口为 searchSessionContent,支持两种归档状态;searchArchivedContent 保留为旧宿主/客户端的归档专用兼容入口,不重复调用。
47
-
48
- ### 使用边界与验证
49
-
50
- - 正文检索和轮次没有持久化索引;每批最多 20 条会话、批内逐条读取。大量或超长日志建议先缩小范围,尚无规模或时延承诺;在途批次不能中止,取消只阻止后续批次及旧结果回写。
51
- - 检索关键词最多 200 字符;不搜索工具输出、附件、推理、系统和插件注入。预览每条最多 2000 个 UTF-16 码元,保留完整 Unicode 字符;完整内容通过原生会话查看。
52
- - 没有后台定时归档;收藏保护仅适用于闲置整理。其他页面的收藏变更需重开管理页读取;撤回记录在关闭或刷新后消失。
53
- - 自动修复不是通用日志恢复工具:缺失、权限及不支持的损坏只提供建议,不删除或截断消息。永久删除不可撤销;本轮不新增任务摘要和 Markdown 导出。
54
- - 功能候选已通过本地回归、支持宿主隔离矩阵及真实 DSH 0.1.6-alpha.2 浏览器冒烟,覆盖检索、预览、收藏、批量操作、诊断与修复。真实模型发送、流式回复、工具审批和未知宿主版本不在本次验收承诺内。
55
- - 宿主设置窗口在极窄屏下的遮挡仍需宿主侧修复;未承诺所有移动端布局均可用。
56
-
57
- ---
58
-
59
- ## English
60
-
61
- The first 1.0 release expands session organization, search, previews, and targeted repair since 0.1.44. The package name and existing data remain unchanged; no session migration is required. Existing archive, restore, permanent deletion, and native conversation navigation remain available.
62
-
63
- ### Lists and shared filters
64
-
65
- - Archived and Unarchived share title/content search, project, favorites, and updated-date filters. Titles-only remains the default; switch scope on the left of the search box and open dates on the right. The end date includes the entire local day; dates can be cleared independently and the popover closes on outside clicks.
66
- - Compact collapsible project groups provide clickable titles, right-side stars and archive/restore icons, and applicable More actions for restore-and-open, preview, copying, and deletion.
67
- - Sort by update time, creation time, title, or most/fewest turns. Group counts follow filters and empty states match the active tab.
68
- - Select all covers filtered results, including collapsed groups. Switching tabs clears selection; filtering preserves it and reports hidden selections. Project-wide actions remain independent of filters.
69
- - Remove redundant page-wide Restore all / Delete all buttons while retaining selection-based and project actions. Operation results remain scoped to their tab.
70
-
71
- ### Favorites, idle cleanup, and batch actions
72
-
73
- - Star or unstar on either tab and filter to favorites. Host-persisted favorites survive archive, restore, and browser changes. Unknown indexes and read failures do not remove favorites; cleanup requires confirmed artifact absence.
74
- - On Unarchived, preview idle candidates using 1–36,500 whole days and the current filters; deselect individual candidates before confirmation. Exclude favorites, the current conversation, running sessions, pending interactions, and sessions with active subagents or no valid activity time.
75
- - Recheck favorites and activity before archiving. Batch archive, restore, and delete show progress and successful/skipped/failed/remaining results. Stop at the first failure and retry only remaining items; delete retries still require confirmation.
76
- - Undo the most recent successful archive batch, including successful retries, while the management page remains open. Permanent deletion cannot be undone.
77
-
78
- ### Content search and quick preview
79
-
80
- - Search persisted user and assistant text with snippets and highlights. Read failures remain visible and retryable; obsolete responses cannot replace results after filters change.
81
- - Preview archived sessions without restoring them, showing context around the first match or up to eight recent messages. Unarchived snippets open the full conversation.
82
- - Render Markdown tables, quotes, code blocks, and role labels, with a source/highlight mode, a wider dialog, and action icons.
83
- - Fix Unicode offsets after case folding, split surrogate pairs on truncation, Escape closing Settings as well as preview, and previews reopening after tab switches.
84
-
85
- ### Turn counts and session location
86
-
87
- - Show user submission counts on both tabs, including image submissions and inherited branch messages; exclude assistant replies, tool calls, and injected messages.
88
- - Sort by most/fewest turns with unknown counts always last; inspect and retry detail failures.
89
- - Copy session IDs and paths separately from More. Official JSONL storage exposes the session directory; other backends use a valid host-provided path. Missing paths and clipboard failures are reported explicitly.
90
-
91
- ### Diagnosis and targeted repair
92
-
93
- - Collect archived read failures, including IDs without summaries, in collapsible diagnostic cards with classified advice and technical details.
94
- - Prefer stable error codes for missing files, permissions, and corruption, with message-based compatibility fallbacks. Unavailable repair preserves the original classification and appends its limitation.
95
- - Diagnose and explicitly confirm repair for supported legacy automation message sources. Eligibility depends on artifacts, host format validation, and a write lease, not just error wording. Sessions must be archived and closed in every DSH process.
96
- - Retain original logs, text, automation attribution, and inheritance data. Publish a new generation only after host re-encoding and validation; create private repair files. Publication never overwrites existing logs and fails safely if hard links are unavailable.
97
-
98
- ### UI, localization, and compatibility
99
-
100
- - Align selection, checkboxes, focus, warnings, updates, and highlights with official theme tokens; refine date controls, diagnostic cards, stars, and action icons.
101
- - Complete Chinese/English diagnosis, preview, update, and error messages. Language changes no longer cause extra detail reads; fix copy-error and empty-state text.
102
- - Refresh Archived/Unarchived screenshots and both READMEs, including filter, undo, repair, and search boundaries.
103
- - Retain support for DSH 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, and 0.1.6-alpha.2. Coexist with Codex UI; no new sidebar pinning or favorites features are introduced.
104
- - searchSessionContent is the canonical API for both archive states. Keep searchArchivedContent as the archived-only compatibility entry for older hosts/clients; do not dispatch duplicate searches.
105
-
106
- ### Limits and validation
107
-
108
- - Content search and turn counts have no persistent index. Read at most 20 sessions per batch, sequentially. Narrow filters for large or long logs; no scale or latency guarantee is established. In-flight batches cannot be interrupted; cancellation prevents subsequent batches and stale results.
109
- - Queries accept up to 200 characters. Search excludes tools, attachments, reasoning, system messages, and plugin injections. Preview limits each message to 2,000 UTF-16 code units without splitting Unicode characters; open the native conversation for full content.
110
- - No scheduled background archiving. Favorite protection applies only to idle cleanup. Reopen management to see favorites changed elsewhere; closing or refreshing clears undo history.
111
- - Repair is not a general recovery tool: missing files, permissions, and unsupported corruption receive advice, never forced deletion or truncation. Permanent deletion is irreversible. Task summaries and Markdown export are not added.
112
- - The functional candidate passed local regressions, isolated supported-host matrices, and real DSH 0.1.6-alpha.2 browser smoke tests covering search, preview, favorites, batches, diagnosis, and repair. Real model submission, streaming, tool approvals, and unknown host versions are outside this validation claim.
113
- - The host Settings shell still has an extremely narrow-screen obstruction; full mobile layout support is not claimed.
Binary file
Binary file