@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
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { record, errorMessage, errorCode } from "./contracts.js";
|
|
2
|
+
const id = (value) => {
|
|
3
|
+
if (typeof value !== "string" || !value.length || value.length > 1024) throw new TypeError("\u4F1A\u8BDD ID \u65E0\u6548");
|
|
4
|
+
return value;
|
|
5
|
+
};
|
|
6
|
+
const queryText = (value, required = false) => {
|
|
7
|
+
if (typeof value !== "string" || value.length > 200 || required && !value.trim()) throw new TypeError("\u5173\u952E\u8BCD\u987B\u4E3A 1\uFF5E200 \u4E2A\u5B57\u7B26");
|
|
8
|
+
return value.trim();
|
|
9
|
+
};
|
|
10
|
+
const searchInputSchema = { parse(input) {
|
|
11
|
+
const value = record(input);
|
|
12
|
+
if (!value || !Array.isArray(value.sessionIds) || !value.sessionIds.length || value.sessionIds.length > 20) throw new TypeError("\u6BCF\u6B21\u68C0\u7D22\u987B\u5305\u542B 1\uFF5E20 \u4E2A\u4F1A\u8BDD");
|
|
13
|
+
return { sessionIds: [...new Set(value.sessionIds.map(id))], query: queryText(value.query, true) };
|
|
14
|
+
} };
|
|
15
|
+
const previewInputSchema = { parse(input) {
|
|
16
|
+
const value = record(input);
|
|
17
|
+
if (!value) throw new TypeError("\u9884\u89C8\u53C2\u6570\u65E0\u6548");
|
|
18
|
+
return { sessionId: id(value.sessionId), query: queryText(value.query ?? "") };
|
|
19
|
+
} };
|
|
20
|
+
const boundedString = (value, max) => {
|
|
21
|
+
if (typeof value !== "string" || value.length > max) throw new TypeError("\u8FD4\u56DE\u6587\u672C\u65E0\u6548");
|
|
22
|
+
return value;
|
|
23
|
+
};
|
|
24
|
+
const sequence = (value) => {
|
|
25
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new TypeError("\u6D88\u606F\u5E8F\u53F7\u65E0\u6548");
|
|
26
|
+
return value;
|
|
27
|
+
};
|
|
28
|
+
const searchResultSchema = { parse(input) {
|
|
29
|
+
const value = record(input);
|
|
30
|
+
if (!value || !Array.isArray(value.items) || !Array.isArray(value.failures) || value.items.length > 20 || value.failures.length > 20) throw new TypeError("\u68C0\u7D22\u7ED3\u679C\u65E0\u6548");
|
|
31
|
+
return {
|
|
32
|
+
items: value.items.map(record).map((row) => ({ sessionId: id(row.sessionId), seq: sequence(row.seq), snippet: boundedString(row.snippet, 400) })),
|
|
33
|
+
failures: value.failures.map(record).map((row) => ({ sessionId: id(row.sessionId), message: boundedString(row.message, 500) }))
|
|
34
|
+
};
|
|
35
|
+
} };
|
|
36
|
+
const previewResultSchema = { parse(input) {
|
|
37
|
+
const value = record(input);
|
|
38
|
+
if (!value || !Array.isArray(value.messages) || value.messages.length > 8 || typeof value.hasEarlier !== "boolean" || typeof value.hasLater !== "boolean") throw new TypeError("\u9884\u89C8\u7ED3\u679C\u65E0\u6548");
|
|
39
|
+
return {
|
|
40
|
+
sessionId: id(value.sessionId),
|
|
41
|
+
hasEarlier: value.hasEarlier,
|
|
42
|
+
hasLater: value.hasLater,
|
|
43
|
+
messages: value.messages.map(record).map((row) => {
|
|
44
|
+
if (typeof row.role !== "string" || !["user", "assistant"].includes(row.role) || typeof row.truncated !== "boolean") throw new TypeError("\u6D88\u606F\u89D2\u8272\u6216\u622A\u65AD\u72B6\u6001\u65E0\u6548");
|
|
45
|
+
return { seq: sequence(row.seq), role: row.role, text: boundedString(row.text, 2e3), truncated: row.truncated };
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
} };
|
|
49
|
+
const detailsInputSchema = { parse(input) {
|
|
50
|
+
const value = record(input);
|
|
51
|
+
if (!value || !Array.isArray(value.sessionIds) || !value.sessionIds.length || value.sessionIds.length > 20) throw new TypeError("\u6BCF\u6B21\u987B\u5305\u542B 1\uFF5E20 \u4E2A\u4F1A\u8BDD");
|
|
52
|
+
return { sessionIds: [...new Set(value.sessionIds.map(id))] };
|
|
53
|
+
} };
|
|
54
|
+
const detailsResultSchema = { parse(input) {
|
|
55
|
+
const value = record(input);
|
|
56
|
+
if (!value || !Array.isArray(value.items) || value.items.length > 20) throw new TypeError("\u4F1A\u8BDD\u8BE6\u60C5\u65E0\u6548");
|
|
57
|
+
return { items: value.items.map(record).map((row) => ({ sessionId: id(row.sessionId), ...row.createdAt !== void 0 ? { createdAt: sequence(row.createdAt) } : {}, turnCount: row.turnCount === null ? null : sequence(row.turnCount), path: row.path === null ? null : boundedString(row.path, 32768), error: boundedString(row.error, 500) })) };
|
|
58
|
+
} };
|
|
59
|
+
function countConversationTurns(events) {
|
|
60
|
+
if (!Array.isArray(events)) throw new TypeError("\u4F1A\u8BDD\u65E5\u5FD7\u65E0\u6548");
|
|
61
|
+
return events.filter((value) => {
|
|
62
|
+
if (!value || typeof value !== "object" || !("type" in value) || value.type !== "user/message") return false;
|
|
63
|
+
const data = "data" in value ? value.data : void 0;
|
|
64
|
+
const message = (data && typeof data === "object" && "message" in data ? data.message : void 0) ?? data;
|
|
65
|
+
if (!message) return false;
|
|
66
|
+
const source = typeof message === "object" && "source" in message ? message.source : void 0;
|
|
67
|
+
const kind = source && typeof source === "object" && "kind" in source ? source.kind : void 0;
|
|
68
|
+
return !kind || kind === "user";
|
|
69
|
+
}).length;
|
|
70
|
+
}
|
|
71
|
+
function compareTurnCounts(left, right, order) {
|
|
72
|
+
const a = typeof left === "number" && Number.isSafeInteger(left) && left >= 0;
|
|
73
|
+
const b = typeof right === "number" && Number.isSafeInteger(right) && right >= 0;
|
|
74
|
+
if (a !== b) return a ? -1 : 1;
|
|
75
|
+
return a && b ? order === "turnsAsc" ? left - right : right - left : 0;
|
|
76
|
+
}
|
|
77
|
+
async function copySessionText(text, clipboard = globalThis.navigator?.clipboard) {
|
|
78
|
+
if (typeof text !== "string" || !text) throw Object.assign(new Error("\u6CA1\u6709\u53EF\u590D\u5236\u7684\u5185\u5BB9"), { code: "copy.empty" });
|
|
79
|
+
if (!clipboard?.writeText) throw Object.assign(new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301\u526A\u8D34\u677F\uFF0C\u8BF7\u5728 HTTPS \u6216\u672C\u673A\u5730\u5740\u4E2D\u6253\u5F00\u9875\u9762"), { code: "copy.unavailable" });
|
|
80
|
+
try {
|
|
81
|
+
await clipboard.writeText(text);
|
|
82
|
+
} catch {
|
|
83
|
+
throw Object.assign(new Error("\u590D\u5236\u5931\u8D25\uFF0C\u8BF7\u5141\u8BB8\u6D4F\u89C8\u5668\u8BBF\u95EE\u526A\u8D34\u677F\u540E\u91CD\u8BD5"), { code: "copy.denied" });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function discoveryInvocations() {
|
|
87
|
+
const codec = (name, schema) => ({ mode: "strict", typeSymbol: `@michengai/dsh-archive-manager/types#${name}`, create: () => schema, schema });
|
|
88
|
+
const methods = [["searchSessionContent", searchInputSchema, searchResultSchema], ["diagnoseSession", repairInputSchema, repairResultSchema], ["repairSession", repairInputSchema, repairResultSchema], ["sessionDetails", detailsInputSchema, detailsResultSchema], ["searchArchivedContent", searchInputSchema, searchResultSchema], ["previewArchivedSession", previewInputSchema, previewResultSchema]];
|
|
89
|
+
return methods.map(([method, input, output]) => ({
|
|
90
|
+
id: `@michengai/dsh-archive-manager#workspaceRegistry/${method}`,
|
|
91
|
+
service: "workspaceRegistry",
|
|
92
|
+
namespace: "workspaceRegistry",
|
|
93
|
+
method,
|
|
94
|
+
invocation: { kind: "direct" },
|
|
95
|
+
parameters: [{ name: "input", wire: "input", source: "json", codec: codec(method + "Input", input) }],
|
|
96
|
+
result: codec(method + "Result", output),
|
|
97
|
+
sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
function extractConversation(events) {
|
|
101
|
+
if (!Array.isArray(events)) throw new TypeError("\u4F1A\u8BDD\u65E5\u5FD7\u65E0\u6548");
|
|
102
|
+
const messages = [];
|
|
103
|
+
for (const event of events.map(record)) {
|
|
104
|
+
if (event.type !== "user/message" && event.type !== "assistant/message") continue;
|
|
105
|
+
const data = event.data && typeof event.data === "object" ? record(event.data) : {};
|
|
106
|
+
const message = data.message && typeof data.message === "object" ? record(data.message) : data;
|
|
107
|
+
const role = event.type === "user/message" ? "user" : "assistant";
|
|
108
|
+
const source = message.source && typeof message.source === "object" ? record(message.source) : {};
|
|
109
|
+
if (role === "user" && source.kind && source.kind !== "user") continue;
|
|
110
|
+
const content = message?.content;
|
|
111
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.filter((block) => block?.type === "text" && typeof block.text === "string").map((block) => block.text).join("\n") : "";
|
|
112
|
+
if (text.trim()) messages.push({ seq: sequence(event.seq), role, text });
|
|
113
|
+
}
|
|
114
|
+
return messages;
|
|
115
|
+
}
|
|
116
|
+
function findContentMatch(messages, query) {
|
|
117
|
+
const needle = query.trim();
|
|
118
|
+
if (!needle) return null;
|
|
119
|
+
for (const message of messages) {
|
|
120
|
+
const match = findTextRange(message.text, needle);
|
|
121
|
+
if (!match) continue;
|
|
122
|
+
const start = Math.max(0, match.start - 70);
|
|
123
|
+
return { seq: message.seq, snippet: (start ? "\u2026" : "") + sliceWholeCharacters(message.text, start, start + 300) + (message.text.length > start + 300 ? "\u2026" : "") };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
function previewConversation(messages, query) {
|
|
128
|
+
const needle = query.trim();
|
|
129
|
+
const hit = needle ? messages.findIndex((row) => findTextRange(row.text, needle)) : -1;
|
|
130
|
+
const start = hit >= 0 ? Math.max(0, hit - 2) : Math.max(0, messages.length - 8);
|
|
131
|
+
return { hasEarlier: start > 0, hasLater: start + 8 < messages.length, messages: messages.slice(start, start + 8).map((row) => {
|
|
132
|
+
const match = findTextRange(row.text, needle)?.start ?? -1;
|
|
133
|
+
const offset = match >= 2e3 ? Math.max(0, match - 300) : 0;
|
|
134
|
+
return { ...row, text: sliceWholeCharacters(row.text, offset, offset + 2e3), truncated: row.text.length > 2e3 };
|
|
135
|
+
}) };
|
|
136
|
+
}
|
|
137
|
+
function findTextRange(text, query) {
|
|
138
|
+
const needle = query?.trim().toLowerCase();
|
|
139
|
+
if (!needle) return null;
|
|
140
|
+
const position = text.toLowerCase().indexOf(needle);
|
|
141
|
+
if (position < 0) return null;
|
|
142
|
+
let original = 0, folded = 0, start;
|
|
143
|
+
for (const character of text) {
|
|
144
|
+
const next = folded + character.toLowerCase().length;
|
|
145
|
+
if (start === void 0 && next > position) start = original;
|
|
146
|
+
original += character.length;
|
|
147
|
+
if (next >= position + needle.length) return { start: start ?? original, end: original };
|
|
148
|
+
folded = next;
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
function sliceWholeCharacters(text, start, end) {
|
|
153
|
+
const splitsPair = (index) => index > 0 && /[\uD800-\uDBFF]/.test(text[index - 1]) && /[\uDC00-\uDFFF]/.test(text[index] ?? "");
|
|
154
|
+
if (splitsPair(start)) start++;
|
|
155
|
+
if (splitsPair(end)) end--;
|
|
156
|
+
return text.slice(start, end);
|
|
157
|
+
}
|
|
158
|
+
function sessionDetailCandidates(ids, byId) {
|
|
159
|
+
return [...new Set(ids)].map((id2) => byId[id2] ?? { id: id2 });
|
|
160
|
+
}
|
|
161
|
+
function dayBoundary(value, nextDay = false) {
|
|
162
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return NaN;
|
|
163
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
164
|
+
const date = new Date(year, month - 1, day);
|
|
165
|
+
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return NaN;
|
|
166
|
+
if (nextDay) date.setDate(date.getDate() + 1);
|
|
167
|
+
return date.getTime();
|
|
168
|
+
}
|
|
169
|
+
function validUpdatedRange(from, to) {
|
|
170
|
+
return (!from || Number.isFinite(dayBoundary(from))) && (!to || Number.isFinite(dayBoundary(to))) && (!from || !to || from <= to);
|
|
171
|
+
}
|
|
172
|
+
function matchesUpdatedRange(value, from, to) {
|
|
173
|
+
if (!validUpdatedRange(from, to)) return false;
|
|
174
|
+
if (!from && !to) return true;
|
|
175
|
+
const time = typeof value === "number" ? value : Date.parse(value ?? "");
|
|
176
|
+
return Number.isFinite(time) && (!from || time >= dayBoundary(from)) && (!to || time < dayBoundary(to, true));
|
|
177
|
+
}
|
|
178
|
+
async function searchArchiveBatches(ids, query, call, { signal, onProgress } = {}) {
|
|
179
|
+
const items = [], failures = [];
|
|
180
|
+
const unique = [...new Set(ids)];
|
|
181
|
+
const check = () => {
|
|
182
|
+
if (signal?.aborted) throw new Error("\u641C\u7D22\u5DF2\u53D6\u6D88");
|
|
183
|
+
};
|
|
184
|
+
for (let start = 0; start < unique.length; start += 20) {
|
|
185
|
+
check();
|
|
186
|
+
const result = searchResultSchema.parse(await call({ sessionIds: unique.slice(start, start + 20), query }));
|
|
187
|
+
check();
|
|
188
|
+
items.push(...result.items);
|
|
189
|
+
failures.push(...result.failures);
|
|
190
|
+
onProgress?.({ done: Math.min(start + 20, unique.length), total: unique.length, items: [...items], failures: [...failures] });
|
|
191
|
+
}
|
|
192
|
+
check();
|
|
193
|
+
return { items, failures };
|
|
194
|
+
}
|
|
195
|
+
function classifySessionError(error) {
|
|
196
|
+
const message = errorMessage(error);
|
|
197
|
+
const descriptions = {
|
|
198
|
+
"repair-frame": ["\u65E7\u7248\u4FEE\u590D\u751F\u6210\u7684\u538B\u7F29\u683C\u5F0F\u4E0D\u7B26\u5408\u5BBF\u4E3B\u8981\u6C42", "\u91CD\u65B0\u8BCA\u65AD\u53EF\u7EA0\u6B63\u538B\u7F29\u5E27\u683C\u5F0F\uFF0C\u539F\u59CB\u65E5\u5FD7\u4FDD\u6301\u4E0D\u53D8\u3002"],
|
|
199
|
+
"legacy-source": ["\u65E7\u7248\u6D88\u606F\u6765\u6E90\u4E0D\u53D7\u65B0\u7248 DSH \u652F\u6301", "\u8FD0\u884C\u8BCA\u65AD\uFF0C\u68C0\u67E5\u662F\u5426\u4E3A\u53EF\u8F6C\u6362\u7684\u65E7\u81EA\u52A8\u5316\u6765\u6E90\u3002"],
|
|
200
|
+
missing: ["\u4F1A\u8BDD\u65E5\u5FD7\u6216\u9644\u4EF6\u8DEF\u5F84\u4E0D\u5B58\u5728", "\u68C0\u67E5\u539F\u76EE\u5F55\u6216\u78C1\u76D8\u662F\u5426\u53EF\u7528\uFF1B\u7F3A\u5931\u6B63\u6587\u4E0D\u80FD\u901A\u8FC7\u91CD\u5EFA\u7F13\u5B58\u6062\u590D\u3002"],
|
|
201
|
+
permission: ["\u6CA1\u6709\u6743\u9650\u8BFB\u53D6\u4F1A\u8BDD\u6587\u4EF6", "\u68C0\u67E5\u76EE\u5F55\u8BBF\u95EE\u6743\u9650\u3001\u6587\u4EF6\u5360\u7528\u4E0E\u5B89\u5168\u8F6F\u4EF6\u9650\u5236\uFF0C\u518D\u91CD\u8BD5\u3002"],
|
|
202
|
+
corrupt: ["\u4F1A\u8BDD\u65E5\u5FD7\u53EF\u80FD\u635F\u574F\u6216\u672A\u5B8C\u6574\u5199\u5165", "\u4FDD\u7559\u73B0\u6709\u65E5\u5FD7\uFF0C\u68C0\u67E5\u78C1\u76D8\u548C\u539F\u59CB\u6587\u4EF6\uFF1B\u4E0D\u81EA\u52A8\u622A\u65AD\u6216\u5220\u9664\u6D88\u606F\u3002"],
|
|
203
|
+
unknown: ["\u4F1A\u8BDD\u6682\u65F6\u65E0\u6CD5\u8BFB\u53D6", "\u5148\u91CD\u8BD5\uFF1B\u4ECD\u5931\u8D25\u65F6\u5C55\u5F00\u6280\u672F\u8BE6\u60C5\uFF0C\u6309\u5177\u4F53\u9519\u8BEF\u68C0\u67E5\u5BBF\u4E3B\u517C\u5BB9\u6027\u3002"]
|
|
204
|
+
};
|
|
205
|
+
const codes = { ENOENT: "missing", EACCES: "permission", EPERM: "permission" };
|
|
206
|
+
const patterns = [
|
|
207
|
+
["repair-frame", /first frame is not exactly one header line/],
|
|
208
|
+
["legacy-source", /unclassified message source/],
|
|
209
|
+
["missing", /ENOENT|不存在|not found/i],
|
|
210
|
+
["permission", /EACCES|EPERM|permission/i],
|
|
211
|
+
["corrupt", /corrupt|JSON|incomplete|truncat|损坏/i]
|
|
212
|
+
];
|
|
213
|
+
const rawCode = errorCode(error);
|
|
214
|
+
const explicit = typeof rawCode === "string" ? codes[rawCode] ?? rawCode : "";
|
|
215
|
+
const code = Object.hasOwn(descriptions, explicit) ? explicit : patterns.find(([, pattern]) => pattern.test(message))?.[0] ?? "unknown";
|
|
216
|
+
const [reason, advice] = descriptions[code];
|
|
217
|
+
return { code, reason, advice };
|
|
218
|
+
}
|
|
219
|
+
const repairInputSchema = { parse(input) {
|
|
220
|
+
const value = record(input);
|
|
221
|
+
const sessionId = id(value?.sessionId);
|
|
222
|
+
if (value?.token !== void 0 && (typeof value.token !== "string" || !/^[a-f0-9]{64}$/.test(value.token))) throw new TypeError("\u4FEE\u590D\u51ED\u636E\u65E0\u6548");
|
|
223
|
+
return { sessionId, ...value.token !== void 0 ? { token: value.token } : {} };
|
|
224
|
+
} };
|
|
225
|
+
const repairResultSchema = { parse(input) {
|
|
226
|
+
const value = record(input);
|
|
227
|
+
if (!value || typeof value.repairable !== "boolean" || typeof value.repaired !== "boolean") throw new TypeError("\u8BCA\u65AD\u7ED3\u679C\u65E0\u6548");
|
|
228
|
+
return { ...value.code !== void 0 ? { code: boundedString(value.code, 64) } : {}, sessionId: id(value.sessionId), repairable: value.repairable, repaired: value.repaired, reason: boundedString(value.reason, 1e3), advice: boundedString(value.advice, 1e3), token: boundedString(value.token ?? "", 64), count: sequence(value.count ?? 0) };
|
|
229
|
+
} };
|
|
230
|
+
export {
|
|
231
|
+
classifySessionError,
|
|
232
|
+
compareTurnCounts,
|
|
233
|
+
copySessionText,
|
|
234
|
+
countConversationTurns,
|
|
235
|
+
detailsInputSchema,
|
|
236
|
+
detailsResultSchema,
|
|
237
|
+
discoveryInvocations,
|
|
238
|
+
extractConversation,
|
|
239
|
+
findContentMatch,
|
|
240
|
+
findTextRange,
|
|
241
|
+
matchesUpdatedRange,
|
|
242
|
+
previewConversation,
|
|
243
|
+
previewInputSchema,
|
|
244
|
+
previewResultSchema,
|
|
245
|
+
repairInputSchema,
|
|
246
|
+
repairResultSchema,
|
|
247
|
+
searchArchiveBatches,
|
|
248
|
+
searchInputSchema,
|
|
249
|
+
searchResultSchema,
|
|
250
|
+
sessionDetailCandidates,
|
|
251
|
+
validUpdatedRange
|
|
252
|
+
};
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { errorMessage } from "./contracts.js";
|
|
1
2
|
class ArchiveNavigationError extends Error {
|
|
3
|
+
code;
|
|
2
4
|
constructor(code) {
|
|
3
5
|
super(code);
|
|
4
6
|
this.name = "ArchiveNavigationError";
|
|
@@ -6,18 +8,22 @@ class ArchiveNavigationError extends Error {
|
|
|
6
8
|
}
|
|
7
9
|
}
|
|
8
10
|
function formatArchiveNavigationError(error, t) {
|
|
9
|
-
return error instanceof ArchiveNavigationError ? t(`archives.${error.code}`) :
|
|
11
|
+
return error instanceof ArchiveNavigationError ? t(`archives.${error.code}`) : errorMessage(error);
|
|
10
12
|
}
|
|
11
13
|
function retainInfoSnapshot(sessions, id) {
|
|
12
14
|
if (typeof sessions?.retainInfo !== "function") return;
|
|
13
15
|
const source = sessions.retainInfo(id);
|
|
14
|
-
|
|
16
|
+
const value = source && typeof source === "object" && "getSnapshot" in source && typeof source.getSnapshot === "function" ? source.getSnapshot() : source;
|
|
17
|
+
if (!value || typeof value !== "object" || !("retainedBy" in value)) return;
|
|
18
|
+
const retainedBy = value.retainedBy;
|
|
19
|
+
if (!retainedBy || typeof retainedBy !== "object" || !("mainView" in retainedBy) || typeof retainedBy.mainView !== "number") return;
|
|
20
|
+
return { retainedBy: { mainView: retainedBy.mainView } };
|
|
15
21
|
}
|
|
16
22
|
function mainViewSessionId(list) {
|
|
17
23
|
const byId = list?.byId;
|
|
18
24
|
if (byId === void 0) return;
|
|
19
25
|
for (const session of Object.values(byId)) {
|
|
20
|
-
if ((session
|
|
26
|
+
if (session && (session.retainedBy?.mainView ?? 0) > 0) return session.id;
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
29
|
function currentSessionId(list) {
|
|
@@ -46,10 +52,10 @@ function allowArchivedNavigation(navigation, sessions, workspaces, { onOpened =
|
|
|
46
52
|
const wrapped = function(...args) {
|
|
47
53
|
if (allowed !== void 0 && viewing(allowed) && workspaces.list.getSnapshot().archivedSessionIds.includes(allowed)) return false;
|
|
48
54
|
allowed = void 0;
|
|
49
|
-
return original
|
|
55
|
+
return original?.apply(this, args);
|
|
50
56
|
};
|
|
51
57
|
const ownsWrapper = () => navigation && Object.getOwnPropertyDescriptor(navigation, "clearArchivedCurrent")?.value === wrapped;
|
|
52
|
-
if (typeof original === "function") {
|
|
58
|
+
if (navigation && typeof original === "function") {
|
|
53
59
|
try {
|
|
54
60
|
navigation.clearArchivedCurrent = wrapped;
|
|
55
61
|
if (!ownsWrapper()) throw new Error("\u5BFC\u822A\u65B9\u6CD5\u672A\u63A5\u7EB3\u5F52\u6863\u9002\u914D");
|
|
@@ -88,7 +94,7 @@ function allowArchivedNavigation(navigation, sessions, workspaces, { onOpened =
|
|
|
88
94
|
if (!navigation || Object.getOwnPropertyDescriptor(navigation, "clearArchivedCurrent")?.value !== wrapped) return;
|
|
89
95
|
if (descriptor) Object.defineProperty(navigation, "clearArchivedCurrent", descriptor);
|
|
90
96
|
else delete navigation.clearArchivedCurrent;
|
|
91
|
-
original
|
|
97
|
+
original?.call(navigation);
|
|
92
98
|
}
|
|
93
99
|
};
|
|
94
100
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { record } from "./contracts.js";
|
|
2
|
+
function checkedId(value) {
|
|
3
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 1024) throw new TypeError("\u4F1A\u8BDD ID \u957F\u5EA6\u987B\u4E3A 1\uFF5E1024 \u4E2A\u5B57\u7B26");
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
const favoriteInputSchema = {
|
|
7
|
+
parse(input) {
|
|
8
|
+
const value = record(input);
|
|
9
|
+
if (!value || typeof value !== "object" || typeof value.favorite !== "boolean") throw new TypeError("\u6536\u85CF\u72B6\u6001\u5FC5\u987B\u4E3A\u5E03\u5C14\u503C");
|
|
10
|
+
return { sessionId: checkedId(value.sessionId), favorite: value.favorite };
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const favoriteStateSchema = {
|
|
14
|
+
parse(input) {
|
|
15
|
+
const value = record(input);
|
|
16
|
+
if (!value || !Array.isArray(value.favoriteSessionIds) || value.favoriteSessionIds.length > 1e5) throw new TypeError("\u6536\u85CF\u96C6\u5408\u65E0\u6548");
|
|
17
|
+
return { favoriteSessionIds: [...new Set(value.favoriteSessionIds.map(checkedId))] };
|
|
18
|
+
},
|
|
19
|
+
safeParse(value) {
|
|
20
|
+
try {
|
|
21
|
+
return { success: true, data: this.parse(value) };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return { success: false, error };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
function favoriteInvocations() {
|
|
28
|
+
const codec = (name, schema) => ({ mode: "strict", typeSymbol: `@michengai/dsh-archive-manager/types#${name}`, create: () => schema, schema });
|
|
29
|
+
return ["favoriteSessions", "setSessionFavorite"].map((method) => ({
|
|
30
|
+
id: `@michengai/dsh-archive-manager#workspaceRegistry/${method}`,
|
|
31
|
+
service: "workspaceRegistry",
|
|
32
|
+
namespace: "workspaceRegistry",
|
|
33
|
+
method,
|
|
34
|
+
invocation: { kind: "direct" },
|
|
35
|
+
parameters: method === "favoriteSessions" ? [] : [{ name: "input", wire: "input", source: "json", codec: codec("FavoriteInput", favoriteInputSchema) }],
|
|
36
|
+
result: codec("FavoriteState", favoriteStateSchema),
|
|
37
|
+
sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
function idleArchiveCandidate(session, { days, now = Date.now(), favorites = /* @__PURE__ */ new Set(), currentId, pending = /* @__PURE__ */ new Map() }) {
|
|
41
|
+
if (!session || !Number.isInteger(days) || days < 1 || days > 36500) return false;
|
|
42
|
+
if (favorites.has(session.id) || session.id === currentId || (session.retainedBy?.mainView ?? 0) > 0) return false;
|
|
43
|
+
if (session.running || (session.runningSubagentCount ?? 0) > 0 || session.pendingInteraction != null || pending.has(session.id)) return false;
|
|
44
|
+
const updated = typeof session.updatedAt === "number" ? session.updatedAt : Date.parse(session.updatedAt ?? "");
|
|
45
|
+
return Number.isFinite(updated) && updated <= now - days * 864e5;
|
|
46
|
+
}
|
|
47
|
+
async function runSessionBatch(ids, operate, { onProgress, refresh } = {}) {
|
|
48
|
+
const requested = [...new Set(ids.map(checkedId))];
|
|
49
|
+
const result = { requested, succeeded: [], skipped: [], failures: [], unprocessed: [], remaining: [], refreshError: null };
|
|
50
|
+
const report = () => onProgress?.({ total: requested.length, done: result.succeeded.length + result.skipped.length + result.failures.length, succeeded: result.succeeded.length, skipped: result.skipped.length, failed: result.failures.length });
|
|
51
|
+
report();
|
|
52
|
+
for (const [index, id] of requested.entries()) {
|
|
53
|
+
try {
|
|
54
|
+
(await operate(id) ? result.succeeded : result.skipped).push(id);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
result.failures.push({ sessionId: id, message: error instanceof Error ? error.message : String(error) });
|
|
57
|
+
result.unprocessed = requested.slice(index + 1);
|
|
58
|
+
result.remaining = requested.slice(index);
|
|
59
|
+
report();
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
report();
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
await refresh?.();
|
|
66
|
+
} catch (error) {
|
|
67
|
+
result.refreshError = error instanceof Error ? error.message : String(error);
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
function createSessionOrganizer({ workspaces, sessions, archive, restore, deleteOne, getFavorites, refresh, currentSessionId }) {
|
|
72
|
+
return async (kind, ids, options = {}) => {
|
|
73
|
+
if (!["archive", "restore", "undo", "delete"].includes(kind)) throw new TypeError("\u672A\u77E5\u6279\u91CF\u64CD\u4F5C");
|
|
74
|
+
return runSessionBatch(ids, async (id) => {
|
|
75
|
+
const before = workspaces.getSnapshot();
|
|
76
|
+
const archived = before.archivedSessionIds.includes(id);
|
|
77
|
+
if (kind === "archive") {
|
|
78
|
+
if (archived) return false;
|
|
79
|
+
const snapshot = sessions.getSnapshot();
|
|
80
|
+
const session = snapshot.byId[id];
|
|
81
|
+
if (!session) return false;
|
|
82
|
+
if (options.idleDays !== void 0) {
|
|
83
|
+
const favorites = new Set((await getFavorites()).favoriteSessionIds);
|
|
84
|
+
const latest = sessions.getSnapshot();
|
|
85
|
+
if (!idleArchiveCandidate(latest.byId[id], { days: options.idleDays, favorites, currentId: currentSessionId(latest), pending: options.getPending?.() ?? options.pending })) return false;
|
|
86
|
+
}
|
|
87
|
+
await archive(id);
|
|
88
|
+
return workspaces.getSnapshot().archivedSessionIds.includes(id);
|
|
89
|
+
}
|
|
90
|
+
if (!archived) return false;
|
|
91
|
+
if (kind === "delete") {
|
|
92
|
+
await deleteOne(id);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
const value = await restore(id);
|
|
96
|
+
const remaining = value?.archivedSessionIds ?? workspaces.getSnapshot().archivedSessionIds;
|
|
97
|
+
return !remaining.includes(id);
|
|
98
|
+
}, { onProgress: options.onProgress, refresh });
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
createSessionOrganizer,
|
|
103
|
+
favoriteInputSchema,
|
|
104
|
+
favoriteInvocations,
|
|
105
|
+
favoriteStateSchema,
|
|
106
|
+
idleArchiveCandidate,
|
|
107
|
+
runSessionBatch
|
|
108
|
+
};
|