dsh-session-manager 0.5.2 → 0.5.4
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 +264 -247
- package/README.md +163 -139
- package/README.zh.md +163 -137
- package/lib/annotation-store.js +198 -198
- package/lib/client.js +2793 -2772
- package/lib/clipboard-parser.js +207 -207
- package/lib/index.js +13 -2
- package/lib/session-files.js +212 -193
- package/package.json +9 -3
package/lib/clipboard-parser.js
CHANGED
|
@@ -1,208 +1,208 @@
|
|
|
1
|
-
// Clipboard parser for AI-generated annotation payloads. Accepts a raw
|
|
2
|
-
// string from navigator.clipboard.readText() and extracts a structured
|
|
3
|
-
// {tags, note, priority?} payload. Tolerates fenced code blocks and
|
|
4
|
-
// conversational wrappers, then enforces the same normalization rules
|
|
5
|
-
// as the annotation store so the import button can call
|
|
6
|
-
// annotationStore.save without re-validating.
|
|
7
|
-
|
|
8
|
-
import { ANNOTATION_LIMITS, normalizeAnnotationPatch } from "./annotation-store.js";
|
|
9
|
-
|
|
10
|
-
const NUL = String.fromCharCode(0);
|
|
11
|
-
const BACKSLASH = String.fromCharCode(92);
|
|
12
|
-
const QUOTE = String.fromCharCode(34);
|
|
13
|
-
const NEWLINE = String.fromCharCode(10);
|
|
14
|
-
const TAG_REJECT = /[,,\x00-\x1f]/;
|
|
15
|
-
|
|
16
|
-
const tagRejectReason = (key, reason) => ({ field: "tags", kind: "rejected", key, reason });
|
|
17
|
-
|
|
18
|
-
const trimLength = (value, max) => value.length > max ? value.slice(0, max) : value;
|
|
19
|
-
|
|
20
|
-
const rejectNonPositive = value => value !== null && (!Number.isInteger(value) || value < 1 || value > 5);
|
|
21
|
-
|
|
22
|
-
const normalizeStringArray = (value, warnings) => {
|
|
23
|
-
if (!Array.isArray(value)) throw new Error("tags must be an array of strings");
|
|
24
|
-
const seen = new Set();
|
|
25
|
-
const tags = [];
|
|
26
|
-
let dropped = 0;
|
|
27
|
-
for (const item of value) {
|
|
28
|
-
if (typeof item !== "string") { dropped++; continue; }
|
|
29
|
-
const trimmed = item.trim();
|
|
30
|
-
if (!trimmed) { dropped++; continue; }
|
|
31
|
-
if (trimmed.length > ANNOTATION_LIMITS.tagLength || TAG_REJECT.test(trimmed)) {
|
|
32
|
-
warnings.push(tagRejectReason(trimmed, "每个标签最多 32 字符,不能包含逗号或控制字符"));
|
|
33
|
-
dropped++;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
const key = trimmed.toLowerCase();
|
|
37
|
-
if (seen.has(key)) continue;
|
|
38
|
-
seen.add(key);
|
|
39
|
-
tags.push(trimmed);
|
|
40
|
-
}
|
|
41
|
-
return { tags, dropped };
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const normalizeNote = (value, warnings) => {
|
|
45
|
-
if (typeof value !== "string") throw new Error("note must be a string");
|
|
46
|
-
if (value.includes(NUL)) throw new Error("note must not contain NUL characters");
|
|
47
|
-
if (value.length > ANNOTATION_LIMITS.noteLength) {
|
|
48
|
-
warnings.push({ field: "note", kind: "truncated", from: value.length, to: ANNOTATION_LIMITS.noteLength });
|
|
49
|
-
return trimLength(value, ANNOTATION_LIMITS.noteLength);
|
|
50
|
-
}
|
|
51
|
-
return value;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const normalizePriority = (value, warnings) => {
|
|
55
|
-
if (value === undefined) return undefined;
|
|
56
|
-
// Treat AI-returned null as the default priority (3 = Normal) so the UI
|
|
57
|
-
// never has to special-case unset entries.
|
|
58
|
-
if (value === null) return 3;
|
|
59
|
-
if (rejectNonPositive(value)) {
|
|
60
|
-
warnings.push({ field: "priority", kind: "rejected", value, reason: "优先级必须为 1–5 的整数;省略或 null 会按 3(普通)保存" });
|
|
61
|
-
return undefined;
|
|
62
|
-
}
|
|
63
|
-
return value;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Find the first balanced top-level JSON object in the text. Handles
|
|
67
|
-
// strings, escapes and nested objects without relying on regular
|
|
68
|
-
// expressions.
|
|
69
|
-
const scanForJsonObject = text => {
|
|
70
|
-
const len = text.length;
|
|
71
|
-
for (let i = 0; i < len; i++) {
|
|
72
|
-
const ch = text[i];
|
|
73
|
-
if (ch !== "{") continue;
|
|
74
|
-
let depth = 0;
|
|
75
|
-
let inString = false;
|
|
76
|
-
let escape = false;
|
|
77
|
-
for (let j = i; j < len; j++) {
|
|
78
|
-
const c = text[j];
|
|
79
|
-
if (inString) {
|
|
80
|
-
if (escape) escape = false;
|
|
81
|
-
else if (c === BACKSLASH) escape = true;
|
|
82
|
-
else if (c === QUOTE) inString = false;
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
if (c === QUOTE) inString = true;
|
|
86
|
-
else if (c === "{") depth++;
|
|
87
|
-
else if (c === "}") {
|
|
88
|
-
depth--;
|
|
89
|
-
if (depth === 0) return text.slice(i, j + 1);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return null;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
// Strip common conversational wrappers (Markdown fences, leading prose)
|
|
97
|
-
// before scanning for JSON. The fence language tag is honored when
|
|
98
|
-
// present so we still find JSON inside triple-backtick fences.
|
|
99
|
-
const stripWrappers = text => {
|
|
100
|
-
// Strip UTF-8 BOM (U+FEFF) if present so JSON.parse doesn't reject it,
|
|
101
|
-
// then trim leading and trailing whitespace (the prior /s+/g was a literal
|
|
102
|
-
// "s+" regex and stripped only literal "s" characters, not whitespace).
|
|
103
|
-
let trimmed = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
|
|
104
|
-
trimmed = trimmed.replace(/^\s+|\s+$/g, "");
|
|
105
|
-
const fence = trimmed.match(/^```(?:json|JSON)?\s*([\s\S]*?)\s*```$/);
|
|
106
|
-
if (fence) return fence[1];
|
|
107
|
-
const braceStart = trimmed.indexOf("{");
|
|
108
|
-
const braceEnd = trimmed.lastIndexOf("}");
|
|
109
|
-
if (braceStart >= 0 && braceEnd > braceStart) return trimmed.slice(braceStart, braceEnd + 1);
|
|
110
|
-
return trimmed;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const ALLOWED_KEYS = new Set(["tags", "note", "priority"]);
|
|
114
|
-
|
|
115
|
-
// Parse a clipboard string into an annotation patch. The returned patch
|
|
116
|
-
// is compatible with annotationStore.save: tags/note/priority are
|
|
117
|
-
// normalized the same way as the server-side validation. Surfaces a
|
|
118
|
-
// list of warnings for truncated notes and dropped tags instead of
|
|
119
|
-
// failing the import.
|
|
120
|
-
export function parseClipboardAnnotation(raw) {
|
|
121
|
-
if (typeof raw !== "string" || raw.trim() === "") {
|
|
122
|
-
return { ok: false, error: "剪贴板为空" };
|
|
123
|
-
}
|
|
124
|
-
const stripped = stripWrappers(raw);
|
|
125
|
-
let parsed;
|
|
126
|
-
try { parsed = JSON.parse(stripped); }
|
|
127
|
-
catch {
|
|
128
|
-
const fallback = scanForJsonObject(stripped);
|
|
129
|
-
if (fallback === null) return { ok: false, error: "无法在剪贴板内容中找到合法的 JSON 对象" };
|
|
130
|
-
try { parsed = JSON.parse(fallback); }
|
|
131
|
-
catch { return { ok: false, error: "剪贴板中的 JSON 无法解析" }; }
|
|
132
|
-
}
|
|
133
|
-
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
134
|
-
return { ok: false, error: "剪贴板内容不是 JSON 对象" };
|
|
135
|
-
}
|
|
136
|
-
const warnings = [];
|
|
137
|
-
const unknown = Object.keys(parsed).filter(key => !ALLOWED_KEYS.has(key));
|
|
138
|
-
for (const key of unknown) warnings.push({ field: key, kind: "ignored", reason: "未知的标记字段,已忽略" });
|
|
139
|
-
|
|
140
|
-
let tags = [];
|
|
141
|
-
let note = "";
|
|
142
|
-
let priority;
|
|
143
|
-
if ("tags" in parsed) {
|
|
144
|
-
try { const result = normalizeStringArray(parsed.tags, warnings); tags = result.tags; }
|
|
145
|
-
catch (error) { return { ok: false, error: error.message }; }
|
|
146
|
-
}
|
|
147
|
-
if ("note" in parsed) {
|
|
148
|
-
try { note = normalizeNote(parsed.note, warnings); }
|
|
149
|
-
catch (error) { return { ok: false, error: error.message }; }
|
|
150
|
-
}
|
|
151
|
-
if ("priority" in parsed) {
|
|
152
|
-
const normalized = normalizePriority(parsed.priority, warnings);
|
|
153
|
-
if (normalized !== undefined) priority = normalized;
|
|
154
|
-
}
|
|
155
|
-
const patch = { tags, note, ...(priority !== undefined ? { priority } : {}) };
|
|
156
|
-
try { normalizeAnnotationPatch(patch); }
|
|
157
|
-
catch (error) { return { ok: false, error: error.message }; }
|
|
158
|
-
return { ok: true, annotation: patch, warnings };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Static prompt templates copied to the clipboard. The Chinese version
|
|
162
|
-
// is the default; English is exposed for users who prefer it. Both ask
|
|
163
|
-
// the model to return strict JSON so the importer can validate it.
|
|
164
|
-
export const CLIPBOARD_PROMPTS = Object.freeze({
|
|
165
|
-
zh: [
|
|
166
|
-
"请根据当前对话内容,为这个会话生成整理标记。",
|
|
167
|
-
"",
|
|
168
|
-
"要求:",
|
|
169
|
-
"1. 生成最多 20 个标签,每个标签最多 32 个字符。",
|
|
170
|
-
"2. 标签应简洁、具体,便于之后搜索。",
|
|
171
|
-
"3. 标签不要重复,不要生成无意义的标签。",
|
|
172
|
-
"4. 生成一段不超过 2000 个字符的备注,总结本次对话的主题、结论、未完成事项或下一步行动。",
|
|
173
|
-
"5. 不要编造对话中没有出现的信息。",
|
|
174
|
-
"6. 只返回合法 JSON,不要使用 Markdown 代码块,不要附加解释。",
|
|
175
|
-
"",
|
|
176
|
-
"返回格式:",
|
|
177
|
-
"{",
|
|
178
|
-
" ", "tags", ": [", "标签1", ", ", "标签2", "],",
|
|
179
|
-
" ", "note", ": ", "会话备注", ",",
|
|
180
|
-
"}"
|
|
181
|
-
].join(NEWLINE),
|
|
182
|
-
en: [
|
|
183
|
-
"Summarize this conversation into session annotations.",
|
|
184
|
-
"",
|
|
185
|
-
"Requirements:",
|
|
186
|
-
"1. Up to 20 tags, at most 32 characters each.",
|
|
187
|
-
"2. Tags should be concise and help future searches.",
|
|
188
|
-
"3. Do not repeat tags or invent meaningless ones.",
|
|
189
|
-
"4. Write a note under 2000 characters summarizing the topic, conclusions, outstanding items and next steps.",
|
|
190
|
-
"5. Do not fabricate details that are not present in the conversation.",
|
|
191
|
-
"6. Return only valid JSON, no Markdown fences, no extra commentary.",
|
|
192
|
-
"",
|
|
193
|
-
"Return shape:",
|
|
194
|
-
"{",
|
|
195
|
-
" ", "tags", ": [", "tag1", ", ", "tag2", "],",
|
|
196
|
-
" ", "note", ": ", "session note", ",",
|
|
197
|
-
"}"
|
|
198
|
-
].join(NEWLINE)
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Resolve the locale key from the active dictionary. Falls back to zh.
|
|
202
|
-
export function clipboardPromptFor(dictionaries, language) {
|
|
203
|
-
const available = dictionaries ? Object.keys(dictionaries) : [];
|
|
204
|
-
const key = available.includes(language) ? language
|
|
205
|
-
: available.includes("zh") ? "zh"
|
|
206
|
-
: available[0] || "zh";
|
|
207
|
-
return { key, text: CLIPBOARD_PROMPTS[key] ?? CLIPBOARD_PROMPTS.zh };
|
|
1
|
+
// Clipboard parser for AI-generated annotation payloads. Accepts a raw
|
|
2
|
+
// string from navigator.clipboard.readText() and extracts a structured
|
|
3
|
+
// {tags, note, priority?} payload. Tolerates fenced code blocks and
|
|
4
|
+
// conversational wrappers, then enforces the same normalization rules
|
|
5
|
+
// as the annotation store so the import button can call
|
|
6
|
+
// annotationStore.save without re-validating.
|
|
7
|
+
|
|
8
|
+
import { ANNOTATION_LIMITS, normalizeAnnotationPatch } from "./annotation-store.js";
|
|
9
|
+
|
|
10
|
+
const NUL = String.fromCharCode(0);
|
|
11
|
+
const BACKSLASH = String.fromCharCode(92);
|
|
12
|
+
const QUOTE = String.fromCharCode(34);
|
|
13
|
+
const NEWLINE = String.fromCharCode(10);
|
|
14
|
+
const TAG_REJECT = /[,,\x00-\x1f]/;
|
|
15
|
+
|
|
16
|
+
const tagRejectReason = (key, reason) => ({ field: "tags", kind: "rejected", key, reason });
|
|
17
|
+
|
|
18
|
+
const trimLength = (value, max) => value.length > max ? value.slice(0, max) : value;
|
|
19
|
+
|
|
20
|
+
const rejectNonPositive = value => value !== null && (!Number.isInteger(value) || value < 1 || value > 5);
|
|
21
|
+
|
|
22
|
+
const normalizeStringArray = (value, warnings) => {
|
|
23
|
+
if (!Array.isArray(value)) throw new Error("tags must be an array of strings");
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
const tags = [];
|
|
26
|
+
let dropped = 0;
|
|
27
|
+
for (const item of value) {
|
|
28
|
+
if (typeof item !== "string") { dropped++; continue; }
|
|
29
|
+
const trimmed = item.trim();
|
|
30
|
+
if (!trimmed) { dropped++; continue; }
|
|
31
|
+
if (trimmed.length > ANNOTATION_LIMITS.tagLength || TAG_REJECT.test(trimmed)) {
|
|
32
|
+
warnings.push(tagRejectReason(trimmed, "每个标签最多 32 字符,不能包含逗号或控制字符"));
|
|
33
|
+
dropped++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const key = trimmed.toLowerCase();
|
|
37
|
+
if (seen.has(key)) continue;
|
|
38
|
+
seen.add(key);
|
|
39
|
+
tags.push(trimmed);
|
|
40
|
+
}
|
|
41
|
+
return { tags, dropped };
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const normalizeNote = (value, warnings) => {
|
|
45
|
+
if (typeof value !== "string") throw new Error("note must be a string");
|
|
46
|
+
if (value.includes(NUL)) throw new Error("note must not contain NUL characters");
|
|
47
|
+
if (value.length > ANNOTATION_LIMITS.noteLength) {
|
|
48
|
+
warnings.push({ field: "note", kind: "truncated", from: value.length, to: ANNOTATION_LIMITS.noteLength });
|
|
49
|
+
return trimLength(value, ANNOTATION_LIMITS.noteLength);
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const normalizePriority = (value, warnings) => {
|
|
55
|
+
if (value === undefined) return undefined;
|
|
56
|
+
// Treat AI-returned null as the default priority (3 = Normal) so the UI
|
|
57
|
+
// never has to special-case unset entries.
|
|
58
|
+
if (value === null) return 3;
|
|
59
|
+
if (rejectNonPositive(value)) {
|
|
60
|
+
warnings.push({ field: "priority", kind: "rejected", value, reason: "优先级必须为 1–5 的整数;省略或 null 会按 3(普通)保存" });
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Find the first balanced top-level JSON object in the text. Handles
|
|
67
|
+
// strings, escapes and nested objects without relying on regular
|
|
68
|
+
// expressions.
|
|
69
|
+
const scanForJsonObject = text => {
|
|
70
|
+
const len = text.length;
|
|
71
|
+
for (let i = 0; i < len; i++) {
|
|
72
|
+
const ch = text[i];
|
|
73
|
+
if (ch !== "{") continue;
|
|
74
|
+
let depth = 0;
|
|
75
|
+
let inString = false;
|
|
76
|
+
let escape = false;
|
|
77
|
+
for (let j = i; j < len; j++) {
|
|
78
|
+
const c = text[j];
|
|
79
|
+
if (inString) {
|
|
80
|
+
if (escape) escape = false;
|
|
81
|
+
else if (c === BACKSLASH) escape = true;
|
|
82
|
+
else if (c === QUOTE) inString = false;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (c === QUOTE) inString = true;
|
|
86
|
+
else if (c === "{") depth++;
|
|
87
|
+
else if (c === "}") {
|
|
88
|
+
depth--;
|
|
89
|
+
if (depth === 0) return text.slice(i, j + 1);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Strip common conversational wrappers (Markdown fences, leading prose)
|
|
97
|
+
// before scanning for JSON. The fence language tag is honored when
|
|
98
|
+
// present so we still find JSON inside triple-backtick fences.
|
|
99
|
+
const stripWrappers = text => {
|
|
100
|
+
// Strip UTF-8 BOM (U+FEFF) if present so JSON.parse doesn't reject it,
|
|
101
|
+
// then trim leading and trailing whitespace (the prior /s+/g was a literal
|
|
102
|
+
// "s+" regex and stripped only literal "s" characters, not whitespace).
|
|
103
|
+
let trimmed = text.charCodeAt(0) === 0xFEFF ? text.slice(1) : text;
|
|
104
|
+
trimmed = trimmed.replace(/^\s+|\s+$/g, "");
|
|
105
|
+
const fence = trimmed.match(/^```(?:json|JSON)?\s*([\s\S]*?)\s*```$/);
|
|
106
|
+
if (fence) return fence[1];
|
|
107
|
+
const braceStart = trimmed.indexOf("{");
|
|
108
|
+
const braceEnd = trimmed.lastIndexOf("}");
|
|
109
|
+
if (braceStart >= 0 && braceEnd > braceStart) return trimmed.slice(braceStart, braceEnd + 1);
|
|
110
|
+
return trimmed;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const ALLOWED_KEYS = new Set(["tags", "note", "priority"]);
|
|
114
|
+
|
|
115
|
+
// Parse a clipboard string into an annotation patch. The returned patch
|
|
116
|
+
// is compatible with annotationStore.save: tags/note/priority are
|
|
117
|
+
// normalized the same way as the server-side validation. Surfaces a
|
|
118
|
+
// list of warnings for truncated notes and dropped tags instead of
|
|
119
|
+
// failing the import.
|
|
120
|
+
export function parseClipboardAnnotation(raw) {
|
|
121
|
+
if (typeof raw !== "string" || raw.trim() === "") {
|
|
122
|
+
return { ok: false, error: "剪贴板为空" };
|
|
123
|
+
}
|
|
124
|
+
const stripped = stripWrappers(raw);
|
|
125
|
+
let parsed;
|
|
126
|
+
try { parsed = JSON.parse(stripped); }
|
|
127
|
+
catch {
|
|
128
|
+
const fallback = scanForJsonObject(stripped);
|
|
129
|
+
if (fallback === null) return { ok: false, error: "无法在剪贴板内容中找到合法的 JSON 对象" };
|
|
130
|
+
try { parsed = JSON.parse(fallback); }
|
|
131
|
+
catch { return { ok: false, error: "剪贴板中的 JSON 无法解析" }; }
|
|
132
|
+
}
|
|
133
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
134
|
+
return { ok: false, error: "剪贴板内容不是 JSON 对象" };
|
|
135
|
+
}
|
|
136
|
+
const warnings = [];
|
|
137
|
+
const unknown = Object.keys(parsed).filter(key => !ALLOWED_KEYS.has(key));
|
|
138
|
+
for (const key of unknown) warnings.push({ field: key, kind: "ignored", reason: "未知的标记字段,已忽略" });
|
|
139
|
+
|
|
140
|
+
let tags = [];
|
|
141
|
+
let note = "";
|
|
142
|
+
let priority;
|
|
143
|
+
if ("tags" in parsed) {
|
|
144
|
+
try { const result = normalizeStringArray(parsed.tags, warnings); tags = result.tags; }
|
|
145
|
+
catch (error) { return { ok: false, error: error.message }; }
|
|
146
|
+
}
|
|
147
|
+
if ("note" in parsed) {
|
|
148
|
+
try { note = normalizeNote(parsed.note, warnings); }
|
|
149
|
+
catch (error) { return { ok: false, error: error.message }; }
|
|
150
|
+
}
|
|
151
|
+
if ("priority" in parsed) {
|
|
152
|
+
const normalized = normalizePriority(parsed.priority, warnings);
|
|
153
|
+
if (normalized !== undefined) priority = normalized;
|
|
154
|
+
}
|
|
155
|
+
const patch = { tags, note, ...(priority !== undefined ? { priority } : {}) };
|
|
156
|
+
try { normalizeAnnotationPatch(patch); }
|
|
157
|
+
catch (error) { return { ok: false, error: error.message }; }
|
|
158
|
+
return { ok: true, annotation: patch, warnings };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Static prompt templates copied to the clipboard. The Chinese version
|
|
162
|
+
// is the default; English is exposed for users who prefer it. Both ask
|
|
163
|
+
// the model to return strict JSON so the importer can validate it.
|
|
164
|
+
export const CLIPBOARD_PROMPTS = Object.freeze({
|
|
165
|
+
zh: [
|
|
166
|
+
"请根据当前对话内容,为这个会话生成整理标记。",
|
|
167
|
+
"",
|
|
168
|
+
"要求:",
|
|
169
|
+
"1. 生成最多 20 个标签,每个标签最多 32 个字符。",
|
|
170
|
+
"2. 标签应简洁、具体,便于之后搜索。",
|
|
171
|
+
"3. 标签不要重复,不要生成无意义的标签。",
|
|
172
|
+
"4. 生成一段不超过 2000 个字符的备注,总结本次对话的主题、结论、未完成事项或下一步行动。",
|
|
173
|
+
"5. 不要编造对话中没有出现的信息。",
|
|
174
|
+
"6. 只返回合法 JSON,不要使用 Markdown 代码块,不要附加解释。",
|
|
175
|
+
"",
|
|
176
|
+
"返回格式:",
|
|
177
|
+
"{",
|
|
178
|
+
" ", "tags", ": [", "标签1", ", ", "标签2", "],",
|
|
179
|
+
" ", "note", ": ", "会话备注", ",",
|
|
180
|
+
"}"
|
|
181
|
+
].join(NEWLINE),
|
|
182
|
+
en: [
|
|
183
|
+
"Summarize this conversation into session annotations.",
|
|
184
|
+
"",
|
|
185
|
+
"Requirements:",
|
|
186
|
+
"1. Up to 20 tags, at most 32 characters each.",
|
|
187
|
+
"2. Tags should be concise and help future searches.",
|
|
188
|
+
"3. Do not repeat tags or invent meaningless ones.",
|
|
189
|
+
"4. Write a note under 2000 characters summarizing the topic, conclusions, outstanding items and next steps.",
|
|
190
|
+
"5. Do not fabricate details that are not present in the conversation.",
|
|
191
|
+
"6. Return only valid JSON, no Markdown fences, no extra commentary.",
|
|
192
|
+
"",
|
|
193
|
+
"Return shape:",
|
|
194
|
+
"{",
|
|
195
|
+
" ", "tags", ": [", "tag1", ", ", "tag2", "],",
|
|
196
|
+
" ", "note", ": ", "session note", ",",
|
|
197
|
+
"}"
|
|
198
|
+
].join(NEWLINE)
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Resolve the locale key from the active dictionary. Falls back to zh.
|
|
202
|
+
export function clipboardPromptFor(dictionaries, language) {
|
|
203
|
+
const available = dictionaries ? Object.keys(dictionaries) : [];
|
|
204
|
+
const key = available.includes(language) ? language
|
|
205
|
+
: available.includes("zh") ? "zh"
|
|
206
|
+
: available[0] || "zh";
|
|
207
|
+
return { key, text: CLIPBOARD_PROMPTS[key] ?? CLIPBOARD_PROMPTS.zh };
|
|
208
208
|
}
|
package/lib/index.js
CHANGED
|
@@ -23,11 +23,11 @@
|
|
|
23
23
|
* Optional live-writer/coordinator internals are guarded before mutation.
|
|
24
24
|
*/
|
|
25
25
|
import { lstat, mkdir, readdir, realpath, rename, rm } from "node:fs/promises";
|
|
26
|
-
import { dirname, join } from "node:path";
|
|
26
|
+
import { basename, dirname, join } from "node:path";
|
|
27
27
|
import { randomBytes } from "node:crypto";
|
|
28
28
|
import { createAnnotationStore } from "./annotation-store.js";
|
|
29
29
|
import { createDshAdapter, normalizeSessionHeader } from "./compat/dsh-adapter.js";
|
|
30
|
-
import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, encodeSessionSegment, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
|
|
30
|
+
import { ARTIFACT_NAMES, assertSessionId, assertSessionDirectory, encodeSessionSegment, parseSessionGenerationFromFilename, readSessionHeader, readSessionFile, writeTempFile, replaceSessionFile } from "./session-files.js";
|
|
31
31
|
|
|
32
32
|
export const name = "dsh-session-manager";
|
|
33
33
|
|
|
@@ -411,6 +411,17 @@ ${rest}`, "utf8");
|
|
|
411
411
|
throw new Error("持久化后端无法定位会话工件路径");
|
|
412
412
|
}
|
|
413
413
|
const newPath = newLocation.path;
|
|
414
|
+
// Keep the stored generation in sync with the filename selected by
|
|
415
|
+
// persistence.locate(). persistence.locate() always returns the
|
|
416
|
+
// SESSION_FORMAT_VERSION path (DSH 0.1.7+ ships v4 by default),
|
|
417
|
+
// regardless of oldHeader.version. Without the stamp below, the
|
|
418
|
+
// rewritten file is `session.v4.jsonl.zstd` carrying a v0/v3 header,
|
|
419
|
+
// which JsonlSessionPersistence.listArtifacts rejects on the next
|
|
420
|
+
// startup (`session generation filename identifies v4, but its
|
|
421
|
+
// header identifies v0`), breaking persistence.list() and crashing
|
|
422
|
+
// the workspace registry while it boots. (issue #18)
|
|
423
|
+
const expectedVersion = parseSessionGenerationFromFilename(basename(newPath));
|
|
424
|
+
if (typeof expectedVersion === "number") newHeader.version = expectedVersion;
|
|
414
425
|
const root = typeof dshHomePath === "function" ? dshHomePath("sessions") : undefined;
|
|
415
426
|
await assertSessionDirectory(root, dirname(oldPath), sessionId);
|
|
416
427
|
await assertSessionDirectory(root, dirname(newPath), sessionId, { allowMissing: true });
|