@michengai/dsh-archive-manager 0.1.44 → 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.
- package/CHANGELOG.md +63 -0
- package/CHANGELOG.zh-CN.md +63 -0
- package/README.md +65 -17
- package/README.zh-CN.md +69 -17
- package/assets/branding/banner.txt +4 -2
- package/assets/branding/dsh-banner-en.webp +0 -0
- package/assets/branding/dsh-banner.webp +0 -0
- package/assets/screenshots/archived-sessions.webp +0 -0
- package/assets/screenshots/unarchived-sessions.webp +0 -0
- package/lib/archive-discovery.js +252 -0
- package/lib/archive-experience.js +12 -6
- package/lib/archive-organizer.js +108 -0
- package/lib/client.js +1402 -207
- package/lib/contracts.js +15 -0
- package/lib/plugin-updater.js +8 -5
- package/lib/projcache.js +11 -7
- package/lib/session-repair.js +135 -0
- package/lib/workspace.js +250 -20
- package/package.json +16 -9
- package/assets/branding/dsh-banner.png +0 -0
- package/assets/screenshots/archive-session-menu.png +0 -0
- package/assets/screenshots/archived-sessions.png +0 -0
package/lib/contracts.js
ADDED
|
@@ -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
|
+
};
|
package/lib/plugin-updater.js
CHANGED
|
@@ -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
|
|
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, [
|
|
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,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { record } from "./contracts.js";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { lstat, readFile, readdir, open, link, unlink, realpath, rename } from "node:fs/promises";
|
|
4
|
+
import { join, resolve, basename } from "node:path";
|
|
5
|
+
import * as zlib from "node:zlib";
|
|
6
|
+
import { classifySessionError } from "./archive-discovery.js";
|
|
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
|
+
}
|
|
14
|
+
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
15
|
+
function decodeRepairLog(bytes, compressed) {
|
|
16
|
+
if (bytes.length > LIMIT) throw new Error("\u65E5\u5FD7\u8D85\u8FC7 32 MB\uFF0C\u9700\u79BB\u7EBF\u5904\u7406");
|
|
17
|
+
const chunks = [];
|
|
18
|
+
let offset = 0, size = 0;
|
|
19
|
+
if (compressed && typeof zlib.zstdDecompressSync !== "function") throw new Error("\u5F53\u524D Node.js \u4E0D\u652F\u6301\u538B\u7F29\u65E5\u5FD7\u8BCA\u65AD");
|
|
20
|
+
while (compressed && offset < bytes.length) {
|
|
21
|
+
const result = decompressFrame(bytes.subarray(offset), LIMIT - size);
|
|
22
|
+
if (!result.engine.bytesWritten) throw new Error("\u538B\u7F29\u65E5\u5FD7\u6CA1\u6709\u5B8C\u6574\u5E27");
|
|
23
|
+
offset += result.engine.bytesWritten;
|
|
24
|
+
size += result.buffer.length;
|
|
25
|
+
chunks.push(result.buffer);
|
|
26
|
+
if (size > LIMIT) throw new Error("\u89E3\u538B\u65E5\u5FD7\u8D85\u8FC7 32 MB\uFF0C\u9700\u79BB\u7EBF\u5904\u7406");
|
|
27
|
+
}
|
|
28
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(compressed ? Buffer.concat(chunks) : bytes);
|
|
29
|
+
if (!text.endsWith("\n")) throw new Error("\u65E5\u5FD7\u672B\u5C3E\u4E0D\u5B8C\u6574\uFF0C\u4E0D\u80FD\u81EA\u52A8\u4FEE\u590D");
|
|
30
|
+
return text.trimEnd().split("\n").map((line) => record(JSON.parse(line)));
|
|
31
|
+
}
|
|
32
|
+
function normalizeAutomationSources(input) {
|
|
33
|
+
const rows = structuredClone(input);
|
|
34
|
+
let count = 0;
|
|
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);
|
|
41
|
+
if (source?.kind !== "automation") return;
|
|
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)))) {
|
|
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");
|
|
44
|
+
}
|
|
45
|
+
message.source = { kind: "plugin", plugin: "dsh-automation", form: "notice", summary: JSON.stringify(source) };
|
|
46
|
+
count++;
|
|
47
|
+
};
|
|
48
|
+
for (const row of rows.slice(1)) {
|
|
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);
|
|
54
|
+
}
|
|
55
|
+
return { rows, count };
|
|
56
|
+
}
|
|
57
|
+
async function regularFile(path) {
|
|
58
|
+
const stat = await lstat(path);
|
|
59
|
+
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");
|
|
60
|
+
}
|
|
61
|
+
async function prepareAutomationRepair({ directory, target, sessionId, format }) {
|
|
62
|
+
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");
|
|
63
|
+
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");
|
|
64
|
+
const names = (await readdir(directory)).filter((name) => /^session(?:\.v[1-9][0-9]*)?\.jsonl(?:\.zstd)?$/.test(name));
|
|
65
|
+
const sources = names.filter((name) => /^session\.jsonl(?:\.zstd)?$/.test(name));
|
|
66
|
+
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");
|
|
67
|
+
const source = join(directory, sources[0]);
|
|
68
|
+
await regularFile(source);
|
|
69
|
+
const bytes = await readFile(source);
|
|
70
|
+
const rows = decodeRepairLog(bytes, source.endsWith(".zstd"));
|
|
71
|
+
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");
|
|
72
|
+
const normalized = normalizeAutomationSources(rows);
|
|
73
|
+
if (!normalized.count) throw new Error("\u672A\u627E\u5230\u7B26\u5408\u4FEE\u590D\u89C4\u5219\u7684\u81EA\u52A8\u5316\u6765\u6E90");
|
|
74
|
+
const restore = format.createRestore(normalized.rows[0]);
|
|
75
|
+
normalized.rows.slice(1).forEach((row) => restore.decodeRow(row));
|
|
76
|
+
const artifact = restore.finish();
|
|
77
|
+
const lines = [format.encodeHeader(artifact.header, artifact.inheritedEventCount), ...artifact.events.map((event) => format.encodeEvent(event))];
|
|
78
|
+
const verify = format.createRestore(lines[0]);
|
|
79
|
+
lines.slice(1).forEach((row) => verify.decodeRow(row));
|
|
80
|
+
const checked = verify.finish();
|
|
81
|
+
await format.validate?.(checked);
|
|
82
|
+
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");
|
|
83
|
+
const plain = Buffer.from(lines.map((row) => JSON.stringify(row)).join("\n") + "\n", "utf8");
|
|
84
|
+
if (plain.length > LIMIT) throw new Error("\u4FEE\u590D\u7ED3\u679C\u8D85\u8FC7\u5904\u7406\u4E0A\u9650");
|
|
85
|
+
const compress = (value) => zlib.zstdCompressSync(value, { params: { [zlib.constants.ZSTD_c_checksumFlag]: 1 } });
|
|
86
|
+
const headerBytes = Buffer.from(JSON.stringify(lines[0]) + "\n", "utf8");
|
|
87
|
+
const output = target.endsWith(".zstd") ? Buffer.concat([compress(headerBytes), ...lines.length > 1 ? [compress(plain.subarray(headerBytes.length))] : []]) : plain;
|
|
88
|
+
await format.validateBytes?.(output);
|
|
89
|
+
let previousTarget;
|
|
90
|
+
if (names.includes(basename(target))) {
|
|
91
|
+
await regularFile(target);
|
|
92
|
+
previousTarget = await readFile(target);
|
|
93
|
+
if (!target.endsWith(".zstd")) throw new Error("\u5DF2\u6709\u65B0\u7248\u65E5\u5FD7\uFF0C\u4E0D\u80FD\u8986\u76D6");
|
|
94
|
+
const first = decompressFrame(previousTarget, LIMIT);
|
|
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");
|
|
96
|
+
}
|
|
97
|
+
const token = digest(previousTarget ? Buffer.concat([bytes, previousTarget]) : bytes);
|
|
98
|
+
const unchanged = async () => {
|
|
99
|
+
if (digest(await readFile(source)) !== digest(bytes)) throw new Error("\u65E5\u5FD7\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BCA\u65AD");
|
|
100
|
+
if (previousTarget) {
|
|
101
|
+
await regularFile(target);
|
|
102
|
+
if (digest(await readFile(target)) !== digest(previousTarget)) throw new Error("\u4FEE\u590D\u4EA7\u7269\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BCA\u65AD");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
return { token, correctingFrame: Boolean(previousTarget), count: normalized.count, source, async publish(expectedToken, ensureIdle) {
|
|
106
|
+
if (expectedToken !== token) throw new Error("\u65E5\u5FD7\u5DF2\u53D8\u5316\uFF0C\u8BF7\u91CD\u65B0\u8BCA\u65AD");
|
|
107
|
+
await ensureIdle();
|
|
108
|
+
await regularFile(source);
|
|
109
|
+
await unchanged();
|
|
110
|
+
const temp = join(directory, ".archive-repair-" + randomUUID());
|
|
111
|
+
let handle;
|
|
112
|
+
try {
|
|
113
|
+
handle = await open(temp, "wx", 384);
|
|
114
|
+
await handle.writeFile(output);
|
|
115
|
+
await handle.sync();
|
|
116
|
+
await handle.close();
|
|
117
|
+
handle = void 0;
|
|
118
|
+
await ensureIdle();
|
|
119
|
+
await unchanged();
|
|
120
|
+
if (previousTarget) await rename(temp, target);
|
|
121
|
+
else await link(temp, target);
|
|
122
|
+
} finally {
|
|
123
|
+
if (handle) await handle.close();
|
|
124
|
+
await unlink(temp).catch((error) => {
|
|
125
|
+
if (error.code !== "ENOENT") throw error;
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} };
|
|
129
|
+
}
|
|
130
|
+
export {
|
|
131
|
+
classifySessionError,
|
|
132
|
+
decodeRepairLog,
|
|
133
|
+
normalizeAutomationSources,
|
|
134
|
+
prepareAutomationRepair
|
|
135
|
+
};
|
package/lib/workspace.js
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
import { record, errorMessage, errorCode } from "./contracts.js";
|
|
2
|
+
import * as SessionRuntime from "@deepseek-ai/dsh-session";
|
|
3
|
+
import { prepareAutomationRepair } from "./session-repair.js";
|
|
4
|
+
import { repairInputSchema, classifySessionError, detailsInputSchema, countConversationTurns, discoveryInvocations, searchInputSchema, previewInputSchema, extractConversation, findContentMatch, previewConversation } from "./archive-discovery.js";
|
|
1
5
|
import { lstat, rm } from "node:fs/promises";
|
|
2
6
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
7
|
import { WorkspaceRegistry } from "@deepseek-ai/dsh-workspace";
|
|
4
8
|
import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
|
|
5
9
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
6
10
|
import { trackTombstone } from "./tombstone.js";
|
|
11
|
+
import { defineDomain } from "@deepseek-ai/dsh-storage-domain";
|
|
12
|
+
import { favoriteInputSchema, favoriteStateSchema, favoriteInvocations } from "./archive-organizer.js";
|
|
13
|
+
const favoriteDomainSpec = defineDomain({
|
|
14
|
+
name: "archive_manager_favorites",
|
|
15
|
+
version: 1,
|
|
16
|
+
tables: {},
|
|
17
|
+
// 宿主运行时仅调用 parse/safeParse;兼容早期同构 schema,不新增 Zod 运行依赖。
|
|
18
|
+
global: { schema: favoriteStateSchema, initial: { favoriteSessionIds: [] } }
|
|
19
|
+
});
|
|
7
20
|
function jsonlSessionDirectory(persistence, header, location) {
|
|
8
21
|
if (persistence.name !== "session-persistence-jsonl" || location.kind !== "jsonl")
|
|
9
22
|
return;
|
|
@@ -47,7 +60,8 @@ function markRemoteMethod(instance, method) {
|
|
|
47
60
|
fn.call(instance);
|
|
48
61
|
}
|
|
49
62
|
};
|
|
50
|
-
Remote(method)(
|
|
63
|
+
Remote(method)(function() {
|
|
64
|
+
}, { ...context, kind: "method", access: { has: (value) => method in value, get: (value) => Reflect.get(value, method) }, metadata: void 0 });
|
|
51
65
|
}
|
|
52
66
|
function headerIdentity(header) {
|
|
53
67
|
return {
|
|
@@ -65,7 +79,8 @@ const sessionIdSchema = {
|
|
|
65
79
|
}
|
|
66
80
|
};
|
|
67
81
|
const archivedSetSchema = {
|
|
68
|
-
parse(
|
|
82
|
+
parse(input) {
|
|
83
|
+
const value = record(input);
|
|
69
84
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
70
85
|
throw new TypeError("result must be an object");
|
|
71
86
|
const ids = value.archivedSessionIds;
|
|
@@ -75,14 +90,16 @@ const archivedSetSchema = {
|
|
|
75
90
|
}
|
|
76
91
|
};
|
|
77
92
|
const deletedSchema = {
|
|
78
|
-
parse(
|
|
93
|
+
parse(input) {
|
|
94
|
+
const value = record(input);
|
|
79
95
|
if (typeof value !== "object" || value === null || Array.isArray(value) || value.deleted !== true)
|
|
80
96
|
throw new TypeError("deleted must be true");
|
|
81
97
|
return value;
|
|
82
98
|
}
|
|
83
99
|
};
|
|
84
100
|
const archivedBatchTargetSchema = {
|
|
85
|
-
parse(
|
|
101
|
+
parse(input) {
|
|
102
|
+
const value = record(input);
|
|
86
103
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
87
104
|
throw new TypeError("target must be an object");
|
|
88
105
|
if (value.scope === "all" || value.scope === "ungrouped") return value;
|
|
@@ -96,7 +113,8 @@ const archivedBatchTargetSchema = {
|
|
|
96
113
|
}
|
|
97
114
|
};
|
|
98
115
|
const deletedBatchSchema = {
|
|
99
|
-
parse(
|
|
116
|
+
parse(input) {
|
|
117
|
+
const value = record(input);
|
|
100
118
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
101
119
|
throw new TypeError("result must be an object");
|
|
102
120
|
for (const key of [
|
|
@@ -115,7 +133,8 @@ const deletedBatchSchema = {
|
|
|
115
133
|
}
|
|
116
134
|
};
|
|
117
135
|
const archivedSessionMetadataSchema = {
|
|
118
|
-
parse(
|
|
136
|
+
parse(input) {
|
|
137
|
+
const value = record(input);
|
|
119
138
|
if (typeof value !== "object" || value === null || Array.isArray(value) || !Array.isArray(value.items))
|
|
120
139
|
throw new TypeError("result.items must be an array");
|
|
121
140
|
if (value.items.some(
|
|
@@ -128,6 +147,8 @@ const archivedSessionMetadataSchema = {
|
|
|
128
147
|
}
|
|
129
148
|
};
|
|
130
149
|
const ARCHIVE_MANAGER_INVOCATIONS = [
|
|
150
|
+
...favoriteInvocations(),
|
|
151
|
+
...discoveryInvocations(),
|
|
131
152
|
{
|
|
132
153
|
id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSession",
|
|
133
154
|
service: "workspaceRegistry",
|
|
@@ -258,8 +279,217 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
258
279
|
markRemoteMethod(this, "deleteSession");
|
|
259
280
|
markRemoteMethod(this, "deleteArchivedSessions");
|
|
260
281
|
markRemoteMethod(this, "archivedSessionMetadata");
|
|
282
|
+
markRemoteMethod(this, "favoriteSessions");
|
|
283
|
+
markRemoteMethod(this, "setSessionFavorite");
|
|
284
|
+
markRemoteMethod(this, "searchArchivedContent");
|
|
285
|
+
markRemoteMethod(this, "searchSessionContent");
|
|
286
|
+
markRemoteMethod(this, "previewArchivedSession");
|
|
287
|
+
markRemoteMethod(this, "sessionDetails");
|
|
288
|
+
markRemoteMethod(this, "diagnoseSession");
|
|
289
|
+
markRemoteMethod(this, "repairSession");
|
|
261
290
|
registerHostRemote(this.ctx);
|
|
262
291
|
}
|
|
292
|
+
/** 只读取归档日志;已有实时实例读取快照,不调用 prepare、enter 或恢复接口。 */
|
|
293
|
+
async readArchivedConversation(sessionId) {
|
|
294
|
+
const ensureArchived = () => {
|
|
295
|
+
if (!this.requireState().archivedSessionIds.includes(sessionId)) throw new Error("\u4F1A\u8BDD\u5DF2\u4E0D\u5728\u5F52\u6863\u4E2D\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
|
|
296
|
+
};
|
|
297
|
+
ensureArchived();
|
|
298
|
+
const events = await this.readConversationEvents(sessionId);
|
|
299
|
+
ensureArchived();
|
|
300
|
+
return extractConversation(events);
|
|
301
|
+
}
|
|
302
|
+
/** 复用宿主只读接口读取事件,不激活会话。 */
|
|
303
|
+
async readConversationEvents(sessionId) {
|
|
304
|
+
const live = this.ctx.get("sessions")?.get(sessionId);
|
|
305
|
+
if (typeof live?.snapshotEvents === "function") return live.snapshotEvents();
|
|
306
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
307
|
+
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");
|
|
308
|
+
return (await this.readStoredProjectionSource(persistence, sessionId)).events;
|
|
309
|
+
}
|
|
310
|
+
/** 只读取当前宿主已知会话;路径由宿主定位,不根据错误文本或 ID 拼接。 */
|
|
311
|
+
async sessionDetails(input) {
|
|
312
|
+
const { sessionIds } = detailsInputSchema.parse(input);
|
|
313
|
+
const items = [];
|
|
314
|
+
for (const sessionId of sessionIds) {
|
|
315
|
+
const item = { sessionId, turnCount: null, path: null, error: "" };
|
|
316
|
+
try {
|
|
317
|
+
if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
|
|
318
|
+
const header = await this.readSessionHeader(sessionId);
|
|
319
|
+
if (Number.isSafeInteger(header.createdAt) && header.createdAt >= 0) item.createdAt = header.createdAt;
|
|
320
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
321
|
+
try {
|
|
322
|
+
const location = typeof persistence?.locate === "function" ? await persistence.locate(header) : void 0;
|
|
323
|
+
if (typeof location?.path === "string") {
|
|
324
|
+
const path = jsonlSessionDirectory(persistence, header, location) ?? location.path;
|
|
325
|
+
await lstat(path);
|
|
326
|
+
item.path = path;
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
item.error = "\u8DEF\u5F84\u4E0D\u53EF\u7528\uFF1A" + errorMessage(error).slice(0, 350);
|
|
330
|
+
}
|
|
331
|
+
item.turnCount = countConversationTurns(await this.readConversationEvents(sessionId));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
item.error = errorMessage(error).slice(0, 500);
|
|
334
|
+
}
|
|
335
|
+
items.push(item);
|
|
336
|
+
}
|
|
337
|
+
return { items };
|
|
338
|
+
}
|
|
339
|
+
/** 诊断与执行共用同一工件校验;客户端不能提供文件路径。 */
|
|
340
|
+
async sessionRepairPlan(sessionId) {
|
|
341
|
+
if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
|
|
342
|
+
await this.ensureSessionRepairIdle(sessionId);
|
|
343
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
344
|
+
const header = await this.readSessionHeader(sessionId);
|
|
345
|
+
const location = await persistence?.locate?.(header);
|
|
346
|
+
const directory = location && jsonlSessionDirectory(persistence, header, location);
|
|
347
|
+
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");
|
|
348
|
+
return prepareAutomationRepair({ directory, target: location.path, sessionId, format: { ...persistence.generationFormat, validateBytes: async (bytes) => {
|
|
349
|
+
if (location.path.endsWith(".zstd")) {
|
|
350
|
+
if (typeof persistence.readZstdPrefix !== "function") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u7F3A\u5C11\u538B\u7F29\u65E5\u5FD7\u6821\u9A8C\u80FD\u529B");
|
|
351
|
+
const parsed = await persistence.readZstdPrefix(bytes);
|
|
352
|
+
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");
|
|
353
|
+
}
|
|
354
|
+
}, validate: (artifact) => {
|
|
355
|
+
if (typeof SessionRuntime.Session?.fromRestore !== "function") throw new Error("\u5F53\u524D\u5BBF\u4E3B\u7F3A\u5C11\u5B8C\u6574\u4F1A\u8BDD\u6821\u9A8C\u80FD\u529B");
|
|
356
|
+
SessionRuntime.Session.fromRestore(artifact.header.id, artifact.events, artifact.header, artifact.inheritedEventCount, "detached");
|
|
357
|
+
} } });
|
|
358
|
+
}
|
|
359
|
+
async ensureSessionRepairIdle(sessionId) {
|
|
360
|
+
if (!this.requireState().archivedSessionIds.includes(sessionId)) throw new Error("\u8BF7\u5148\u5F52\u6863\u4F1A\u8BDD\uFF0C\u518D\u8FDB\u884C\u4FEE\u590D");
|
|
361
|
+
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");
|
|
362
|
+
}
|
|
363
|
+
async diagnoseSession(input) {
|
|
364
|
+
const { sessionId } = repairInputSchema.parse(input);
|
|
365
|
+
const result = { code: "unknown", sessionId, repairable: false, repaired: false, token: "", count: 0, reason: "", advice: "" };
|
|
366
|
+
try {
|
|
367
|
+
if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
|
|
368
|
+
await this.readConversationEvents(sessionId);
|
|
369
|
+
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" };
|
|
370
|
+
} catch (error) {
|
|
371
|
+
const diagnostic = classifySessionError(error);
|
|
372
|
+
result.code = diagnostic.code;
|
|
373
|
+
result.reason = diagnostic.reason;
|
|
374
|
+
result.advice = diagnostic.advice;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const plan = await this.sessionRepairPlan(sessionId);
|
|
378
|
+
return {
|
|
379
|
+
...result,
|
|
380
|
+
code: plan.correctingFrame ? "ready-frame" : "ready",
|
|
381
|
+
repairable: true,
|
|
382
|
+
token: plan.token,
|
|
383
|
+
count: plan.count,
|
|
384
|
+
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`,
|
|
385
|
+
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"
|
|
386
|
+
};
|
|
387
|
+
} catch (error) {
|
|
388
|
+
return {
|
|
389
|
+
...result,
|
|
390
|
+
code: result.code === "unknown" ? "blocked" : result.code,
|
|
391
|
+
advice: (result.advice + " \u6682\u4E0D\u80FD\u81EA\u52A8\u4FEE\u590D\uFF1A" + errorMessage(error)).slice(0, 1e3)
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
async repairSession(input) {
|
|
396
|
+
const { sessionId, token } = repairInputSchema.parse(input);
|
|
397
|
+
if (!token) throw new Error("\u8BF7\u5148\u8BCA\u65AD\uFF0C\u518D\u786E\u8BA4\u4FEE\u590D");
|
|
398
|
+
return this.enqueueOperation(async () => {
|
|
399
|
+
const plan = await this.sessionRepairPlan(sessionId);
|
|
400
|
+
const persistence = this.ctx.get("sessionPersistence");
|
|
401
|
+
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");
|
|
402
|
+
const lease = await persistence.acquireLease(sessionId, void 0, dirname(plan.source));
|
|
403
|
+
try {
|
|
404
|
+
await plan.publish(token, () => this.ensureSessionRepairIdle(sessionId));
|
|
405
|
+
} finally {
|
|
406
|
+
await lease.release();
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
await this.readConversationEvents(sessionId);
|
|
410
|
+
} catch {
|
|
411
|
+
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" };
|
|
412
|
+
}
|
|
413
|
+
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" };
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/** 已归档与未归档共用正文检索;只读已知会话,不激活或改变归档状态。 */
|
|
417
|
+
async searchSessionContent(input) {
|
|
418
|
+
const { sessionIds, query } = searchInputSchema.parse(input);
|
|
419
|
+
const items = [], failures = [];
|
|
420
|
+
for (const sessionId of sessionIds) {
|
|
421
|
+
try {
|
|
422
|
+
if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
|
|
423
|
+
const messages = extractConversation(await this.readConversationEvents(sessionId));
|
|
424
|
+
if (!await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u5DF2\u79FB\u9664\uFF0C\u8BF7\u5237\u65B0\u5217\u8868");
|
|
425
|
+
const match = findContentMatch(messages, query);
|
|
426
|
+
if (match) items.push({ sessionId, ...match });
|
|
427
|
+
} catch (error) {
|
|
428
|
+
failures.push({ sessionId, message: errorMessage(error).slice(0, 500) });
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { items, failures };
|
|
432
|
+
}
|
|
433
|
+
/** 输入为限定批次的归档 ID 和关键词,逐条返回命中或失败。 */
|
|
434
|
+
async searchArchivedContent(input) {
|
|
435
|
+
const { sessionIds, query } = searchInputSchema.parse(input);
|
|
436
|
+
const items = [], failures = [];
|
|
437
|
+
for (const sessionId of sessionIds) {
|
|
438
|
+
try {
|
|
439
|
+
const match = findContentMatch(await this.readArchivedConversation(sessionId), query);
|
|
440
|
+
if (match) items.push({ sessionId, ...match });
|
|
441
|
+
} catch (error) {
|
|
442
|
+
failures.push({ sessionId, message: errorMessage(error).slice(0, 500) });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { items, failures };
|
|
446
|
+
}
|
|
447
|
+
/** 返回最近八条对话或关键词附近的八条,单条最多 2000 字,不写入宿主数据。 */
|
|
448
|
+
async previewArchivedSession(input) {
|
|
449
|
+
const { sessionId, query } = previewInputSchema.parse(input);
|
|
450
|
+
return { sessionId, ...previewConversation(await this.readArchivedConversation(sessionId), query) };
|
|
451
|
+
}
|
|
452
|
+
/** 独立收藏域不改变官方工作区数据结构;旧版本回退时可保留收藏。 */
|
|
453
|
+
async favoriteDomain() {
|
|
454
|
+
if (!this.favoriteDomainPromise) {
|
|
455
|
+
this.favoriteDomainPromise = this.ctx.storageDomain.open(favoriteDomainSpec).then((domain) => {
|
|
456
|
+
this.ctx.effect(() => () => domain.close(), "archive-manager: \u6536\u85CF\u5B58\u50A8\u5173\u95ED");
|
|
457
|
+
return domain;
|
|
458
|
+
}).catch((error) => {
|
|
459
|
+
this.favoriteDomainPromise = void 0;
|
|
460
|
+
throw error;
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return this.favoriteDomainPromise;
|
|
464
|
+
}
|
|
465
|
+
/** 返回宿主保存的收藏;读取时清理已确认不存在的会话。 */
|
|
466
|
+
async favoriteSessions() {
|
|
467
|
+
return this.enqueueOperation(async () => {
|
|
468
|
+
const domain = await this.favoriteDomain();
|
|
469
|
+
const state = favoriteStateSchema.parse(domain.global.get());
|
|
470
|
+
const kept = [];
|
|
471
|
+
for (const id of state.favoriteSessionIds) {
|
|
472
|
+
if (this.ctx.get("sessions")?.get(id) !== void 0 || !await this.sessionArtifactMissing(id)) kept.push(id);
|
|
473
|
+
}
|
|
474
|
+
if (kept.length !== state.favoriteSessionIds.length) await domain.global.set({ favoriteSessionIds: kept });
|
|
475
|
+
return { favoriteSessionIds: kept };
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
/** 设置单条收藏,串行写入避免不同浏览器相互覆盖。 */
|
|
479
|
+
async setSessionFavorite(input) {
|
|
480
|
+
const { sessionId, favorite } = favoriteInputSchema.parse(input);
|
|
481
|
+
return this.enqueueOperation(async () => {
|
|
482
|
+
if (favorite && !await this.sessionKnown(sessionId)) throw new Error("\u4F1A\u8BDD\u4E0D\u5B58\u5728\uFF0C\u65E0\u6CD5\u6536\u85CF");
|
|
483
|
+
const domain = await this.favoriteDomain();
|
|
484
|
+
const state = favoriteStateSchema.parse(domain.global.get());
|
|
485
|
+
const ids = new Set(state.favoriteSessionIds);
|
|
486
|
+
if (favorite) ids.add(sessionId);
|
|
487
|
+
else ids.delete(sessionId);
|
|
488
|
+
const next = favoriteStateSchema.parse({ favoriteSessionIds: [...ids] });
|
|
489
|
+
await domain.global.set(next);
|
|
490
|
+
return next;
|
|
491
|
+
});
|
|
492
|
+
}
|
|
263
493
|
/** 归档集合未变时复用反查表,避免每次 sessionPath 都扫全部工作区记账。 */
|
|
264
494
|
archivedWorkspacePath(sessionId) {
|
|
265
495
|
const state = this.requireState();
|
|
@@ -269,10 +499,10 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
269
499
|
const archived = new Set(state.archivedSessionIds);
|
|
270
500
|
const table = this.requireTable();
|
|
271
501
|
for (const workspaceId of state.workspaceIds) {
|
|
272
|
-
const
|
|
273
|
-
if (
|
|
274
|
-
for (const id of
|
|
275
|
-
if (archived.has(id)) index.set(id,
|
|
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);
|
|
276
506
|
}
|
|
277
507
|
}
|
|
278
508
|
this.archivedSessionPathIndex = index;
|
|
@@ -491,9 +721,9 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
491
721
|
if (path === void 0) return;
|
|
492
722
|
const table = this.requireTable();
|
|
493
723
|
for (const workspaceId of this.requireState().workspaceIds) {
|
|
494
|
-
const
|
|
495
|
-
if (
|
|
496
|
-
if (
|
|
724
|
+
const record2 = table.get(workspaceId);
|
|
725
|
+
if (record2 === void 0 || record2.path !== path) continue;
|
|
726
|
+
if (record2.sessionIds.includes(sessionId)) return;
|
|
497
727
|
const entity = this.entities.get(workspaceId);
|
|
498
728
|
if (entity !== void 0) await entity.attachSession(sessionId);
|
|
499
729
|
return;
|
|
@@ -552,8 +782,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
552
782
|
}
|
|
553
783
|
}
|
|
554
784
|
/** 以归档集合顺序解析批量目标,避免依赖浏览器尚未加载完整的摘要投影。 */
|
|
555
|
-
archivedSessionIdsForTarget(
|
|
556
|
-
target = archivedBatchTargetSchema.parse(
|
|
785
|
+
archivedSessionIdsForTarget(input) {
|
|
786
|
+
const target = archivedBatchTargetSchema.parse(input);
|
|
557
787
|
const state = this.requireState();
|
|
558
788
|
const archivedSessionIds = [...new Set(state.archivedSessionIds)];
|
|
559
789
|
if (target.scope === "all") return archivedSessionIds;
|
|
@@ -709,11 +939,11 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
709
939
|
/** 统一旧版头部数组与 0.1.3 的持久化快照,供父类索引和本插件枚举共用。 */
|
|
710
940
|
async listStoredHeaders() {
|
|
711
941
|
return (await this.ctx.sessionPersistence.list()).map(
|
|
712
|
-
(item) => item.header
|
|
942
|
+
(item) => "header" in item && item.header ? item.header : item
|
|
713
943
|
);
|
|
714
944
|
}
|
|
715
945
|
async indexHeaders(items) {
|
|
716
|
-
for (const item of items) await this.indexHeader(item.header
|
|
946
|
+
for (const item of items) await this.indexHeader("header" in item && item.header ? item.header : item);
|
|
717
947
|
}
|
|
718
948
|
/** 为未处于实时状态的持久化会话发布相同的移除事件。 */
|
|
719
949
|
async publishColdSessionRemoval(sessionId, sessions) {
|
|
@@ -760,7 +990,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
760
990
|
try {
|
|
761
991
|
stat = await lstat(path);
|
|
762
992
|
} catch (error) {
|
|
763
|
-
if (error
|
|
993
|
+
if (errorCode(error) === "ENOENT") continue;
|
|
764
994
|
throw error;
|
|
765
995
|
}
|
|
766
996
|
if (stat.isSymbolicLink())
|
|
@@ -791,8 +1021,8 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
791
1021
|
const table = this.requireTable();
|
|
792
1022
|
const state = this.requireState();
|
|
793
1023
|
for (const workspaceId of state.workspaceIds) {
|
|
794
|
-
const
|
|
795
|
-
if (
|
|
1024
|
+
const record2 = table.get(workspaceId);
|
|
1025
|
+
if (record2 === void 0 || !record2.sessionIds.includes(sessionId)) continue;
|
|
796
1026
|
const next = await table.update(workspaceId, (current) => ({
|
|
797
1027
|
...current,
|
|
798
1028
|
sessionIds: current.sessionIds.filter((id) => id !== sessionId),
|