@michengai/dsh-archive-manager 1.0.11 → 1.0.13

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,41 @@
1
+ import { bindArchiveManagerRemote, installArchiveWorkspace, tolerateStaleFileUploadResolver } from "./workspace.js";
2
+ import { installArchiveProjectionCache, installArchiveProjectionCacheGuards } from "./projcache.js";
3
+ const inject = ["workspaceRegistry", "sessionProjectionCache", "typert"];
4
+ async function apply(ctx) {
5
+ tolerateStaleFileUploadResolver(ctx);
6
+ const registry = ctx.workspaceRegistry;
7
+ const cache = ctx.sessionProjectionCache;
8
+ const uninstallWorkspace = installArchiveWorkspace(registry);
9
+ let uninstallCache = async () => {
10
+ };
11
+ let unbindRemote = () => {
12
+ };
13
+ const dispose = async () => {
14
+ try {
15
+ unbindRemote();
16
+ } finally {
17
+ try {
18
+ await uninstallCache();
19
+ } finally {
20
+ uninstallWorkspace();
21
+ }
22
+ }
23
+ };
24
+ try {
25
+ try {
26
+ uninstallCache = await installArchiveProjectionCache(cache);
27
+ } catch (error) {
28
+ ctx.logger?.warn?.(`archive-manager: projection cache overlay skipped: ${String(error)}`);
29
+ uninstallCache = installArchiveProjectionCacheGuards(cache);
30
+ }
31
+ unbindRemote = bindArchiveManagerRemote(ctx);
32
+ } catch (error) {
33
+ await dispose();
34
+ throw error;
35
+ }
36
+ return dispose;
37
+ }
38
+ export {
39
+ apply,
40
+ inject
41
+ };
package/lib/index.js CHANGED
@@ -1,11 +1,32 @@
1
1
  import { registerPluginUpdater } from "./plugin-updater.js";
2
- const inject = ["webServer"];
3
- function apply(ctx) {
4
- return registerPluginUpdater(ctx, {
5
- endpoint: "/api/michengai/dsh-archive-manager/update",
6
- packageName: "@michengai/dsh-archive-manager",
7
- manifestUrl: new URL("../package.json", import.meta.url)
8
- });
2
+ import { apply as installHost, inject as hostInject } from "./host-install.js";
3
+ const inject = ["webServer", ...hostInject];
4
+ async function apply(ctx) {
5
+ let disposeUpdater = () => {
6
+ };
7
+ try {
8
+ disposeUpdater = registerPluginUpdater(ctx, {
9
+ endpoint: "/api/michengai/dsh-archive-manager/update",
10
+ packageName: "@michengai/dsh-archive-manager",
11
+ manifestUrl: new URL("../package.json", import.meta.url)
12
+ });
13
+ } catch (error) {
14
+ ctx.logger?.warn?.(`archive-manager: update route skipped: ${String(error)}`);
15
+ }
16
+ let disposeHost;
17
+ try {
18
+ disposeHost = await installHost(ctx);
19
+ } catch (error) {
20
+ disposeUpdater();
21
+ throw error;
22
+ }
23
+ return async () => {
24
+ try {
25
+ disposeUpdater();
26
+ } finally {
27
+ await disposeHost();
28
+ }
29
+ };
9
30
  }
10
31
  export {
11
32
  apply,
@@ -1,3 +1,4 @@
1
+ import { symbols } from "@deepseek-ai/cordis";
1
2
  import { record } from "./contracts.js";
2
3
  import { spawn } from "node:child_process";
3
4
  import { existsSync, readFileSync } from "node:fs";
@@ -194,10 +195,34 @@ function publicError(error) {
194
195
  const message = error instanceof Error ? error.message : "\u66F4\u65B0\u6682\u4E0D\u53EF\u7528\u3002";
195
196
  return /[A-Za-z]:[\\/]|\/(?:home|root|Users|var|tmp)\//.test(message) ? "\u66F4\u65B0\u5931\u8D25\uFF0C\u8BF7\u67E5\u770B\u670D\u52A1\u7AEF\u65E5\u5FD7\u3002" : message;
196
197
  }
198
+ const updateRoutes = Symbol.for("dsh-archive-manager.update-routes");
199
+ function updateRouteBag(server) {
200
+ const owner = server;
201
+ return owner[updateRoutes] ?? (owner[updateRoutes] = /* @__PURE__ */ new Map());
202
+ }
203
+ function rawService(service) {
204
+ const original = Reflect.get(service, symbols.original);
205
+ return original !== null && typeof original === "object" ? original : service;
206
+ }
207
+ function releaseUpdateRoute(server, path) {
208
+ const raw = rawService(server);
209
+ const bag = updateRouteBag(raw);
210
+ const owned = bag.get(path);
211
+ if (owned !== void 0) {
212
+ bag.delete(path);
213
+ owned();
214
+ }
215
+ for (const key of ["exact", "prefixes"]) {
216
+ const table = Reflect.get(raw, key);
217
+ if (table instanceof Map && table.has(path)) table.delete(path);
218
+ }
219
+ }
197
220
  function registerPluginUpdater(ctx, options) {
198
221
  const host = ctx;
199
222
  let installing = false;
200
- return host.webServer.register({
223
+ const server = rawService(host.webServer);
224
+ releaseUpdateRoute(server, options.endpoint);
225
+ const dispose = host.webServer.register({
201
226
  kind: "exact",
202
227
  path: options.endpoint,
203
228
  handler: async (request, response) => {
@@ -249,6 +274,14 @@ function registerPluginUpdater(ctx, options) {
249
274
  }
250
275
  }
251
276
  });
277
+ const wrapped = () => {
278
+ const bag = updateRouteBag(server);
279
+ if (bag.get(options.endpoint) !== wrapped) return;
280
+ bag.delete(options.endpoint);
281
+ dispose();
282
+ };
283
+ updateRouteBag(server).set(options.endpoint, wrapped);
284
+ return wrapped;
252
285
  }
253
286
  export {
254
287
  PLUGIN_UPDATE_HEADER,
package/lib/projcache.js CHANGED
@@ -1,6 +1,6 @@
1
1
  const CacheBase = SessionProjectionCache;
2
2
  import { createHash } from "node:crypto";
3
- import { Service } from "@deepseek-ai/cordis";
3
+ import { Service, symbols } from "@deepseek-ai/cordis";
4
4
  import {
5
5
  SessionProjectionCache,
6
6
  checkpointIdentity,
@@ -169,16 +169,16 @@ async function importMissingRecords(ctx, target, records, label) {
169
169
  }
170
170
  return imported;
171
171
  }
172
- async function importPreviousProjectionCache(ctx, target) {
172
+ async function importPreviousProjectionCache(ctx, target, currentTable) {
173
+ const sameSpec = projectionCacheDomainSpec.version === legacyProjectionCacheDomainSpec.version && projectionCacheDomainSpec.layout === legacyProjectionCacheDomainSpec.layout;
173
174
  const legacySafeV2 = await readLegacySafeRecords(ctx, legacySafeV2ProjectionCacheDomainSpec, "archive-manager v2");
174
175
  const legacySafe = await readLegacySafeRecords(ctx, legacySafeProjectionCacheDomainSpec, "archive-manager v1");
175
- const legacy = await readSourceRecords(ctx, legacyProjectionCacheDomainSpec, "legacy v3");
176
- const sameSpec = projectionCacheDomainSpec.version === legacyProjectionCacheDomainSpec.version && projectionCacheDomainSpec.layout === legacyProjectionCacheDomainSpec.layout;
176
+ const legacy = sameSpec && currentTable !== void 0 ? { opened: true, records: [...currentTable.entries()] } : await readSourceRecords(ctx, legacyProjectionCacheDomainSpec, "legacy v3");
177
177
  let imported = 0;
178
178
  if (legacySafeV2.opened) imported += await importMissingRecords(ctx, target, legacySafeV2.records, "archive-manager v2");
179
179
  if (legacySafe.opened) imported += await importMissingRecords(ctx, target, legacySafe.records, "archive-manager v1");
180
180
  if (!sameSpec) {
181
- const current = await readSourceRecords(ctx, projectionCacheDomainSpec, "current");
181
+ const current = currentTable !== void 0 ? { opened: true, records: [...currentTable.entries()] } : await readSourceRecords(ctx, projectionCacheDomainSpec, "current");
182
182
  if (current.opened) imported += await importMissingRecords(ctx, target, current.records, "current");
183
183
  }
184
184
  if (legacy.opened) imported += await importMissingRecords(ctx, target, legacy.records, "legacy v3");
@@ -269,11 +269,107 @@ var ArchiveProjectionCache = class extends CacheBase {
269
269
  if (idx !== -1) this.deletedSessionOrder.splice(idx, 1);
270
270
  }
271
271
  };
272
+ const projectionGuards = /* @__PURE__ */ new WeakMap();
273
+ function installArchiveProjectionCacheGuards(cache, table) {
274
+ const target = Reflect.get(cache, symbols.original) ?? cache;
275
+ const existing = projectionGuards.get(target);
276
+ if (existing) return existing;
277
+ const fields = {
278
+ table: table ?? target.table,
279
+ deletedSessionIds: /* @__PURE__ */ new Set(),
280
+ deletedSessionOrder: [],
281
+ deletedSessionTombstoneLimit: 4096,
282
+ writeTail: Promise.resolve()
283
+ };
284
+ const receiver = Object.create(target, Object.fromEntries(Object.entries(fields).map(([name, value]) => [name, { value, writable: true, configurable: true }])));
285
+ const restoredMethods = [];
286
+ let active = true;
287
+ const restore = () => {
288
+ active = false;
289
+ for (const item of [...restoredMethods].reverse()) {
290
+ if (item.descriptor) Object.defineProperty(target, item.name, item.descriptor);
291
+ else delete target[item.name];
292
+ }
293
+ };
294
+ try {
295
+ for (const name of Object.getOwnPropertyNames(ArchiveProjectionCache.prototype)) {
296
+ if (name === "constructor") continue;
297
+ const method = Reflect.get(ArchiveProjectionCache.prototype, name);
298
+ if (typeof method !== "function") continue;
299
+ Object.defineProperty(receiver, name, { value: method, writable: true, configurable: true });
300
+ const previous = target[name];
301
+ restoredMethods.push({ name, descriptor: Object.getOwnPropertyDescriptor(target, name) });
302
+ target[name] = (...args) => {
303
+ if (!active) {
304
+ if (typeof previous === "function") return Reflect.apply(previous, target, args);
305
+ throw new Error("archive-manager: projection cache guards are stopped");
306
+ }
307
+ const result = Reflect.apply(method, receiver, args);
308
+ if (name === "delete") receiver.writeTail = Promise.allSettled([receiver.writeTail, result]).then(() => void 0);
309
+ return result;
310
+ };
311
+ }
312
+ } catch (error) {
313
+ restore();
314
+ throw error;
315
+ }
316
+ let disposal;
317
+ const uninstall = () => {
318
+ if (disposal) return disposal;
319
+ const drain = receiver.writeTail;
320
+ restore();
321
+ projectionGuards.delete(target);
322
+ return disposal = drain;
323
+ };
324
+ projectionGuards.set(target, uninstall);
325
+ return uninstall;
326
+ }
327
+ const projectionOverlay = Symbol.for("dsh-archive-manager.projcache-overlay");
328
+ async function installArchiveProjectionCache(cache) {
329
+ const target = cache;
330
+ if (target[projectionOverlay] === true) return async () => {
331
+ };
332
+ const domain = await target.ctx.storageDomain.open(safeProjectionCacheDomainSpec);
333
+ let installed = false;
334
+ const previousTable = target.table;
335
+ let uninstallGuards;
336
+ try {
337
+ const table = new SafeSessionTable(domain.table("sessions"));
338
+ await importPreviousProjectionCache(target.ctx, table, previousTable);
339
+ uninstallGuards = installArchiveProjectionCacheGuards(cache, table);
340
+ target.table = table;
341
+ target[projectionOverlay] = true;
342
+ installed = true;
343
+ let disposal;
344
+ return () => disposal ??= (async () => {
345
+ if (target[projectionOverlay] !== true) return;
346
+ const drain = uninstallGuards();
347
+ target.table = previousTable;
348
+ delete target[projectionOverlay];
349
+ await drain;
350
+ await domain.close();
351
+ })();
352
+ } catch (error) {
353
+ if (!installed) {
354
+ const drain = uninstallGuards?.();
355
+ target.table = previousTable;
356
+ delete target[projectionOverlay];
357
+ try {
358
+ await drain;
359
+ } finally {
360
+ await domain.close();
361
+ }
362
+ }
363
+ throw error;
364
+ }
365
+ }
272
366
  export {
273
367
  ArchiveProjectionCache,
274
368
  SafeSessionTable,
275
369
  ArchiveProjectionCache as default,
276
370
  importPreviousProjectionCache,
371
+ installArchiveProjectionCache,
372
+ installArchiveProjectionCacheGuards,
277
373
  legacyProjectionCacheDomainSpec,
278
374
  legacySafeProjectionCacheDomainSpec,
279
375
  legacySafeV2ProjectionCacheDomainSpec,
package/lib/workspace.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { symbols } from "@deepseek-ai/cordis";
1
2
  import { record, errorMessage, errorCode } from "./contracts.js";
2
3
  import * as SessionRuntime from "@deepseek-ai/dsh-session";
3
4
  import { childCatalogFact, prepareAutomationRepair } from "./session-repair.js";
@@ -51,6 +52,20 @@ function strictCodec(typeSymbol, schema) {
51
52
  schema
52
53
  };
53
54
  }
55
+ const ARCHIVE_REMOTE_METHODS = [
56
+ "unarchiveSession",
57
+ "deleteSession",
58
+ "deleteArchivedSessions",
59
+ "archivedSessionMetadata",
60
+ "favoriteSessions",
61
+ "setSessionFavorite",
62
+ "searchArchivedContent",
63
+ "searchSessionContent",
64
+ "previewArchivedSession",
65
+ "sessionDetails",
66
+ "diagnoseSession",
67
+ "repairSession"
68
+ ];
54
69
  function markRemoteMethod(instance, method) {
55
70
  const context = {
56
71
  private: false,
@@ -244,6 +259,24 @@ function registerHostRemote(ctx) {
244
259
  typertCtx.typert.register(ARCHIVE_MANAGER_TYPERT);
245
260
  });
246
261
  }
262
+ function bindArchiveManagerRemote(ctx) {
263
+ const typert = ctx.get("typert");
264
+ if (typert === void 0 || typeof typert.register !== "function") {
265
+ throw new Error("archive-manager: typert is unavailable");
266
+ }
267
+ try {
268
+ const dispose = typert.register(ARCHIVE_MANAGER_TYPERT);
269
+ return typeof dispose === "function" ? () => {
270
+ dispose();
271
+ } : () => {
272
+ };
273
+ } catch (error) {
274
+ const message = error instanceof Error ? error.message : String(error);
275
+ if (!message.includes("already registered")) throw error;
276
+ return () => {
277
+ };
278
+ }
279
+ }
247
280
  async function hostRepairFormat(format, children = []) {
248
281
  if (!format || !Number.isInteger(format.currentVersion) || format.currentVersion < 3) throw new Error("\u5F53\u524D\u5BBF\u4E3B\u4E0D\u652F\u6301\u6B64\u4FEE\u590D\uFF0C\u8BF7\u5347\u7EA7 DSH \u540E\u91CD\u8BD5");
249
282
  if (typeof format.createRestore === "function") return format;
@@ -311,30 +344,9 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
311
344
  constructor(ctx) {
312
345
  super(ctx);
313
346
  tolerateStaleFileUploadResolver(ctx);
314
- const indexedPath = this.host.sessionPath;
315
- this.host.sessionPath = (id) => {
316
- const path = indexedPath(id);
317
- if (path !== void 0) return path;
318
- if (this.invalidSessionPaths.has(id)) return void 0;
319
- try {
320
- return this.archivedWorkspacePath(id);
321
- } catch {
322
- return void 0;
323
- }
324
- };
347
+ protectArchivedSessionPath(this);
325
348
  this.typertRemote = bindTypertRemote(this, this.name);
326
- markRemoteMethod(this, "unarchiveSession");
327
- markRemoteMethod(this, "deleteSession");
328
- markRemoteMethod(this, "deleteArchivedSessions");
329
- markRemoteMethod(this, "archivedSessionMetadata");
330
- markRemoteMethod(this, "favoriteSessions");
331
- markRemoteMethod(this, "setSessionFavorite");
332
- markRemoteMethod(this, "searchArchivedContent");
333
- markRemoteMethod(this, "searchSessionContent");
334
- markRemoteMethod(this, "previewArchivedSession");
335
- markRemoteMethod(this, "sessionDetails");
336
- markRemoteMethod(this, "diagnoseSession");
337
- markRemoteMethod(this, "repairSession");
349
+ for (const method of ARCHIVE_REMOTE_METHODS) markRemoteMethod(this, method);
338
350
  registerHostRemote(this.ctx);
339
351
  }
340
352
  /** 只读取归档日志;已有实时实例读取快照,不调用 prepare、enter 或恢复接口。 */
@@ -1204,7 +1216,107 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
1204
1216
  }
1205
1217
  }
1206
1218
  };
1219
+ const workspaceOverlay = Symbol.for("dsh-archive-manager.workspace-overlay");
1220
+ function protectArchivedSessionPath(registry) {
1221
+ const indexedPath = registry.host.sessionPath;
1222
+ registry.host.sessionPath = (id) => {
1223
+ const path = indexedPath(id);
1224
+ if (path !== void 0) return path;
1225
+ if (registry.invalidSessionPaths.has(id)) return void 0;
1226
+ try {
1227
+ return registry.archivedWorkspacePath(id);
1228
+ } catch {
1229
+ return void 0;
1230
+ }
1231
+ };
1232
+ return () => {
1233
+ registry.host.sessionPath = indexedPath;
1234
+ };
1235
+ }
1236
+ const remoteMethodDescriptor = "@deepseek-ai/dsh-typert-protocol/remote-methods";
1237
+ function markOverlayMethods(registry, methods) {
1238
+ const raw = Reflect.get(registry, symbols.original) ?? registry;
1239
+ const prototype = Object.getPrototypeOf(raw);
1240
+ if (prototype === null) return () => {
1241
+ };
1242
+ const before = Object.getOwnPropertyDescriptor(prototype, remoteMethodDescriptor);
1243
+ const marked = /* @__PURE__ */ new Set();
1244
+ for (const marker of Reflect.get(before?.value ?? {}, "methods") ?? []) {
1245
+ if (typeof marker.method === "string") marked.add(marker.method);
1246
+ }
1247
+ for (const method of methods) {
1248
+ if (marked.has(method)) continue;
1249
+ markRemoteMethod(raw, method);
1250
+ }
1251
+ return () => {
1252
+ if (before === void 0) delete prototype[remoteMethodDescriptor];
1253
+ else Object.defineProperty(prototype, remoteMethodDescriptor, before);
1254
+ };
1255
+ }
1256
+ function bindOverlayRemote(registry) {
1257
+ const raw = Reflect.get(registry, symbols.original) ?? registry;
1258
+ if (Reflect.get(raw, "typertRemote") !== void 0) return () => {
1259
+ };
1260
+ const name = Reflect.get(raw, "name");
1261
+ const serviceKey = typeof name === "string" ? name : "workspaceRegistry";
1262
+ Reflect.set(raw, "typertRemote", bindTypertRemote(raw, serviceKey));
1263
+ return () => {
1264
+ Reflect.deleteProperty(raw, "typertRemote");
1265
+ };
1266
+ }
1267
+ function installArchiveWorkspace(registry) {
1268
+ const overlay = registry;
1269
+ if (overlay[workspaceOverlay] === true) return () => {
1270
+ };
1271
+ if (registry instanceof ArchiveWorkspaceRegistry) return () => {
1272
+ };
1273
+ const target = overlay;
1274
+ const fields = {
1275
+ deletedSessionIds: /* @__PURE__ */ new Set(),
1276
+ deletedSessionOrder: [],
1277
+ deletedSessionTombstoneLimit: 4096,
1278
+ deletedIdentities: /* @__PURE__ */ new Map(),
1279
+ archivedSessionPathIndex: /* @__PURE__ */ new Map(),
1280
+ archivedSessionPathIndexKey: void 0
1281
+ };
1282
+ const addedFields = [];
1283
+ for (const [key, value] of Object.entries(fields)) {
1284
+ if (!Object.hasOwn(target, key)) {
1285
+ target[key] = value;
1286
+ addedFields.push(key);
1287
+ }
1288
+ }
1289
+ const restorePath = protectArchivedSessionPath(overlay);
1290
+ const restoreRemote = bindOverlayRemote(registry);
1291
+ const restoredMethods = [];
1292
+ const copiedMethods = [];
1293
+ for (const name of Object.getOwnPropertyNames(ArchiveWorkspaceRegistry.prototype)) {
1294
+ if (name === "constructor") continue;
1295
+ const method = Reflect.get(ArchiveWorkspaceRegistry.prototype, name);
1296
+ if (typeof method !== "function") continue;
1297
+ restoredMethods.push({ name, hadOwn: Object.hasOwn(target, name), previous: target[name] });
1298
+ target[name] = method;
1299
+ copiedMethods.push(name);
1300
+ }
1301
+ const restoreMarkers = markOverlayMethods(registry, ARCHIVE_REMOTE_METHODS.filter((method) => copiedMethods.includes(method)));
1302
+ target[workspaceOverlay] = true;
1303
+ return () => {
1304
+ if (target[workspaceOverlay] !== true) return;
1305
+ restorePath();
1306
+ restoreMarkers();
1307
+ restoreRemote();
1308
+ for (const item of restoredMethods.reverse()) {
1309
+ if (item.hadOwn) target[item.name] = item.previous;
1310
+ else delete target[item.name];
1311
+ }
1312
+ for (const key of addedFields) delete target[key];
1313
+ delete target[workspaceOverlay];
1314
+ };
1315
+ }
1207
1316
  export {
1208
1317
  ArchiveWorkspaceRegistry,
1209
- ArchiveWorkspaceRegistry as default
1318
+ bindArchiveManagerRemote,
1319
+ ArchiveWorkspaceRegistry as default,
1320
+ installArchiveWorkspace,
1321
+ tolerateStaleFileUploadResolver
1210
1322
  };
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Archive Manager",
4
+ "description": "Archive, restore, favorite, and permanently delete sessions, and diagnose damaged session logs."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Archive Manager",
4
+ "description": "归档、恢复、收藏和永久删除会话,并诊断损坏的会话日志。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "1.0.11",
4
- "description": "NPM-installable DSH Web plugin for managing archived sessions.",
3
+ "version": "1.0.13",
4
+ "description": "归档、恢复、收藏和永久删除会话,并诊断损坏的会话日志。 · Archive, restore, favorite, and permanently delete sessions, and diagnose damaged session logs.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.22.0",
7
7
  "engines": {
@@ -12,12 +12,15 @@
12
12
  ".": "./lib/index.js",
13
13
  "./workspace": "./lib/workspace.js",
14
14
  "./projcache": "./lib/projcache.js",
15
+ "./host": "./lib/host-install.js",
15
16
  "./client": "./lib/client.js",
16
- "./package.json": "./package.json"
17
+ "./package.json": "./package.json",
18
+ "./locale/*.json": "./locale/*.json"
17
19
  },
18
20
  "files": [
19
21
  "lib",
20
22
  "assets",
23
+ "locale",
21
24
  "cordis.patch.yml",
22
25
  "README.md",
23
26
  "README.zh-CN.md",