@max-null/dsh-memory 0.5.2 → 0.6.0
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/README.md +5 -0
- package/client.js +295 -0
- package/dist/engine.d.ts +68 -0
- package/dist/engine.js +196 -30
- package/dist/index.js +160 -0
- package/dist/prompt-files.d.ts +77 -0
- package/dist/prompt-files.js +188 -0
- package/dist/routes.js +78 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,11 @@ npm install @max-null/dsh-memory
|
|
|
46
46
|
|
|
47
47
|
## 使用流程(人工确认闸门)
|
|
48
48
|
|
|
49
|
+
提示词模板库(0.6.0):`prompt_search / prompt_get / prompt_list / prompt_add` 四个工具管理**模板库**——
|
|
50
|
+
md 文件是唯一事实源(`~/.dsh/prompt-library/*.md` 为 global;`<workspace>/.dsh/prompt-library/` 随工作区分享),
|
|
51
|
+
前端(记忆面板「模板」tab / 模型工具)检索同一份索引;模板存在即生效(`source: agent` 角标标识模型新增),
|
|
52
|
+
**永不注入 system prompt**。
|
|
53
|
+
|
|
49
54
|
```
|
|
50
55
|
模型 memory_save → status: suggested(只是建议,未生效)
|
|
51
56
|
人 memory_confirm → status: approved(已审核;是否常驻注入由独立开关 injected 决定)
|
package/client.js
CHANGED
|
@@ -153,6 +153,26 @@ function registerSettingsNavIcon(label) {
|
|
|
153
153
|
var STRINGS = {
|
|
154
154
|
zh: {
|
|
155
155
|
tabMemory: "\u8BB0\u5FC6",
|
|
156
|
+
tabPrompt: "\u6A21\u677F",
|
|
157
|
+
promptEmpty: "\u6682\u65E0\u6A21\u677F\uFF08\u70B9\u300C\u65B0\u589E\u6A21\u677F\u300D\u6216\u5F80 ~/.dsh/prompt-library \u653E md \u6587\u4EF6\uFF09",
|
|
158
|
+
promptRefresh: "\u5237\u65B0\u7D22\u5F15",
|
|
159
|
+
promptAdd: "\u65B0\u589E\u6A21\u677F",
|
|
160
|
+
promptInsert: "\u63D2\u5165\u8F93\u5165\u6846",
|
|
161
|
+
promptInserted: "\u5DF2\u63D2\u5165",
|
|
162
|
+
promptInsertUnavailable: "\u65E0\u6D3B\u52A8\u4F1A\u8BDD/\u8F93\u5165\u6846",
|
|
163
|
+
promptDelete: "\u5220\u9664",
|
|
164
|
+
promptDeleteConfirm: "\u5220\u9664\u6A21\u677F\uFF08md \u6587\u4EF6\u4E0E\u7D22\u5F15\u4E00\u5E76\u5220\u9664\uFF09\uFF1F",
|
|
165
|
+
promptFormName: "\u540D\u79F0",
|
|
166
|
+
promptFormContent: "\u6B63\u6587",
|
|
167
|
+
promptFormDimension: "\u7EF4\u5EA6\uFF08\u524D\u7AEF/\u540E\u7AEF/\u2026\uFF09",
|
|
168
|
+
promptFormDifficulty: "\u96BE\u5EA6\uFF08L1-L5/LX\uFF09",
|
|
169
|
+
promptFormTags: "\u6807\u7B7E\uFF08\u9017\u53F7\u5206\u9694\uFF09",
|
|
170
|
+
promptFormFallback: "\u5907\u7528\u63D0\u793A\u8BCD",
|
|
171
|
+
promptFormSubmit: "\u5199\u5165\u6A21\u677F\u5E93",
|
|
172
|
+
promptFormCancel: "\u53D6\u6D88",
|
|
173
|
+
promptSearch: "\u641C\u7D22\u6A21\u677F\u2026",
|
|
174
|
+
promptAgent: "agent",
|
|
175
|
+
promptFilterAll: "\u5168\u90E8",
|
|
156
176
|
memorySearch: "\u641C\u7D22\u8BB0\u5FC6\u2026",
|
|
157
177
|
empty: "\u9ED1\u6697\u4E2D\u672A\u89C1\u7075\u5149",
|
|
158
178
|
confirm: "\u786E\u8BA4",
|
|
@@ -178,6 +198,26 @@ var STRINGS = {
|
|
|
178
198
|
},
|
|
179
199
|
en: {
|
|
180
200
|
tabMemory: "Memory",
|
|
201
|
+
tabPrompt: "Templates",
|
|
202
|
+
promptEmpty: 'No templates yet (use "Add template" or drop md files into ~/.dsh/prompt-library)',
|
|
203
|
+
promptRefresh: "Refresh index",
|
|
204
|
+
promptAdd: "Add template",
|
|
205
|
+
promptInsert: "Insert into input",
|
|
206
|
+
promptInserted: "Inserted",
|
|
207
|
+
promptInsertUnavailable: "No active session/input",
|
|
208
|
+
promptDelete: "Delete",
|
|
209
|
+
promptDeleteConfirm: "Delete this template (md file + index)?",
|
|
210
|
+
promptFormName: "Name",
|
|
211
|
+
promptFormContent: "Body",
|
|
212
|
+
promptFormDimension: "Dimension (\u524D\u7AEF/\u540E\u7AEF/\u2026)",
|
|
213
|
+
promptFormDifficulty: "Difficulty (L1-L5/LX)",
|
|
214
|
+
promptFormTags: "Tags (comma separated)",
|
|
215
|
+
promptFormFallback: "Fallback prompt",
|
|
216
|
+
promptFormSubmit: "Write to library",
|
|
217
|
+
promptFormCancel: "Cancel",
|
|
218
|
+
promptSearch: "Search templates\u2026",
|
|
219
|
+
promptAgent: "agent",
|
|
220
|
+
promptFilterAll: "All",
|
|
181
221
|
memorySearch: "Search memory\u2026",
|
|
182
222
|
empty: "No spark in the dark",
|
|
183
223
|
confirm: "Confirm",
|
|
@@ -298,9 +338,240 @@ var memoryApi = {
|
|
|
298
338
|
},
|
|
299
339
|
injectionPreview(cwd) {
|
|
300
340
|
return api("injectionPreview", { ...cwd === void 0 ? {} : { cwd } });
|
|
341
|
+
},
|
|
342
|
+
// ── 提示词模板库(0.6.0)──
|
|
343
|
+
promptList(cwd) {
|
|
344
|
+
return api("prompt.list", { ...cwd === void 0 ? {} : { cwd } });
|
|
345
|
+
},
|
|
346
|
+
promptGet(nameOrId, cwd) {
|
|
347
|
+
return api("prompt.get", { name: nameOrId, ...cwd === void 0 ? {} : { cwd } });
|
|
348
|
+
},
|
|
349
|
+
promptAdd(payload, cwd) {
|
|
350
|
+
return api("prompt.add", { ...payload, ...cwd === void 0 ? {} : { cwd } });
|
|
351
|
+
},
|
|
352
|
+
promptRemove(nameOrId, cwd) {
|
|
353
|
+
return api("prompt.remove", { name: nameOrId, ...cwd === void 0 ? {} : { cwd } });
|
|
354
|
+
},
|
|
355
|
+
promptRefresh(cwd) {
|
|
356
|
+
return api("prompt.refresh", { ...cwd === void 0 ? {} : { cwd } });
|
|
301
357
|
}
|
|
302
358
|
};
|
|
303
359
|
var ORGANIZE_PROMPT = "\u8BF7\u6574\u7406\u6211\u7684\u8BB0\u5FC6\u5E93\uFF1A\u7528 memory_list \u67E5\u770B\u5168\u90E8\u8BB0\u5FC6\u3002\u6574\u7406\u89C4\u5219\uFF08\u5FC5\u987B\u6267\u884C\uFF09\uFF1A\u2460\u540C\u7C7B\u6761\u76EE\u5408\u5E76\u2014\u2014\u5DE5\u4F5C\u4E60\u60EF/\u7EA6\u5B9A\u7C7B\u591A\u6761\u5408\u5E76\u4E3A\u4E00\u6761\uFF08\u5185\u5BB9\u7528 \u2460\u2461\u2462 \u5E76\u5217\uFF0C\u907F\u514D\u788E\u7247\u5316\uFF09\uFF1B\u2461\u4E0E\u300C[\u8BB0\u5FC6\u7CFB\u7EDF\u81EA\u8FF0]\u300D\uFF08\u4F60\u4E0A\u4E0B\u6587\u4E2D\u7684\u8BB0\u5FC6\u673A\u5236\u8BF4\u660E\uFF09\u5185\u5BB9\u91CD\u590D\u7684\u63D2\u4EF6\u4ECB\u7ECD\u6761\u76EE\uFF08\u5982 dsh-memory \u63D2\u4EF6\u53D1\u5E03\u4FE1\u606F\uFF09\u5E94\u5220\u9664\u2014\u2014\u673A\u5236\u8BF4\u660E\u5DF2\u7531\u7CFB\u7EDF\u5E38\u9A7B\u63D0\u4F9B\uFF0C\u65E0\u9700\u7528\u6237\u5B58\u50A8\uFF1B\u2462\u5BF9\u8FC7\u65F6\u3001\u9519\u8BEF\u6216\u5DF2\u53D8\u5316\u7684\u5185\u5BB9\u7528 memory_update \u4FEE\u6B63\uFF08\u4F1A\u91CD\u7F6E\u4E3A\u5F85\u5BA1\u6838\uFF09\uFF1B\u2463\u7CBE\u7B80\u5197\u957F\u5185\u5BB9\uFF0C\u4E3A\u6BCF\u6761\u8865\u5145\u6216\u4FEE\u6B63 keywords\uFF1B\u2464\u9700\u8981\u5220\u9664\u7684\u7528 memory_forget\uFF0C\u9700\u8981\u65B0\u589E\u7684\u7528 memory_save\u3002\u5224\u65AD\u5185\u5BB9\u662F\u5426\u8FC7\u65F6\u7684\u65B9\u6CD5\uFF1A\u628A\u8BB0\u5FC6\u91CC\u63D0\u5230\u7684\u5DE5\u5177\u540D/\u6570\u91CF\u4E0E\u4F60\u5F53\u524D\u5B9E\u9645\u53EF\u7528\u7684\u8BB0\u5FC6\u5DE5\u5177\u5BF9\u7167\u2014\u2014\u4F60\u5F53\u524D\u53EF\u7528\uFF1Amemory_save / memory_list / memory_search / memory_confirm / memory_forget / memory_update\uFF08\u5171 6 \u4E2A\uFF09\uFF1B\u82E5\u8BB0\u5FC6\u4E2D\u7684\u5DE5\u5177\u5217\u8868\u3001\u6570\u91CF\u3001\u6D41\u7A0B\u4E0E\u6B64\u4E0D\u7B26\u5373\u4E3A\u8FC7\u65F6\uFF0C\u7528 memory_update \u4FEE\u6B63\u3002\u6539\u52A8\u5168\u90E8\u843D\u5728 suggested \u7B49\u5F85\u5BA1\u6838\uFF08\u4E0D\u8981\u8C03\u7528 memory_confirm\uFF09\uFF0C\u5B8C\u6210\u540E\u7528\u4E00\u53E5\u8BDD\u6C47\u62A5\u6574\u7406\u7ED3\u679C\u3002";
|
|
360
|
+
function insertIntoInput(ctx, text) {
|
|
361
|
+
const sessions = ctx.get?.("sessions");
|
|
362
|
+
const conversation = ctx.get?.("conversation");
|
|
363
|
+
if (sessions === void 0 || conversation?.input?.for === void 0) return { ok: false, reason: "unavailable" };
|
|
364
|
+
const current = sessions.list?.getSnapshot?.()?.current;
|
|
365
|
+
if (current === void 0 || current === "") return { ok: false, reason: "no-active-session" };
|
|
366
|
+
const actx = sessions.scope(current);
|
|
367
|
+
if (actx === void 0) return { ok: false, reason: "no-scope" };
|
|
368
|
+
const input = conversation.input.for(actx);
|
|
369
|
+
if (input === void 0) return { ok: false, reason: "no-input" };
|
|
370
|
+
try {
|
|
371
|
+
input.setDraft(text);
|
|
372
|
+
return { ok: true };
|
|
373
|
+
} catch {
|
|
374
|
+
return { ok: false, reason: "setDraft-failed" };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function PromptView(props) {
|
|
378
|
+
const t = useT();
|
|
379
|
+
const [items, setItems] = (0, import_react.useState)([]);
|
|
380
|
+
const [query, setQuery] = (0, import_react.useState)("");
|
|
381
|
+
const [expanded, setExpanded] = (0, import_react.useState)(null);
|
|
382
|
+
const [detail, setDetail] = (0, import_react.useState)(null);
|
|
383
|
+
const [note, setNote] = (0, import_react.useState)(null);
|
|
384
|
+
const [formOpen, setFormOpen] = (0, import_react.useState)(false);
|
|
385
|
+
const [form, setForm] = (0, import_react.useState)({});
|
|
386
|
+
const reload = async () => {
|
|
387
|
+
try {
|
|
388
|
+
setItems(await memoryApi.promptList(props.cwd));
|
|
389
|
+
} catch {
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
(0, import_react.useEffect)(() => {
|
|
393
|
+
if (props.visible) void reload();
|
|
394
|
+
}, [props.visible]);
|
|
395
|
+
const flash = (text) => {
|
|
396
|
+
setNote(text);
|
|
397
|
+
setTimeout(() => setNote(null), 2e3);
|
|
398
|
+
};
|
|
399
|
+
const q = query.trim().toLowerCase();
|
|
400
|
+
const filtered = items.filter((item) => {
|
|
401
|
+
if (q === "") return true;
|
|
402
|
+
const hay = [
|
|
403
|
+
String(item.name ?? ""),
|
|
404
|
+
String(item.dimension ?? ""),
|
|
405
|
+
String(item.difficulty ?? ""),
|
|
406
|
+
(item.tags ?? []).join(" "),
|
|
407
|
+
String(item.summary ?? "")
|
|
408
|
+
].join(" ").toLowerCase();
|
|
409
|
+
return hay.includes(q);
|
|
410
|
+
});
|
|
411
|
+
const openPrompt = async (item) => {
|
|
412
|
+
const key = String(item.id ?? item.name);
|
|
413
|
+
if (expanded === key) {
|
|
414
|
+
setExpanded(null);
|
|
415
|
+
setDetail(null);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
setExpanded(key);
|
|
419
|
+
try {
|
|
420
|
+
setDetail(await memoryApi.promptGet(String(item.name ?? item.id), props.cwd));
|
|
421
|
+
} catch {
|
|
422
|
+
setDetail(null);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const insert = (item) => {
|
|
426
|
+
const text = String(detail?.body ?? item.summary ?? "");
|
|
427
|
+
const result = insertIntoInput(props.ctx, text);
|
|
428
|
+
flash(result.ok ? t("promptInserted") : `${t("promptInsertUnavailable")} (${result.reason ?? "?"})`);
|
|
429
|
+
};
|
|
430
|
+
const remove = async (item) => {
|
|
431
|
+
if (!window.confirm(t("promptDeleteConfirm"))) return;
|
|
432
|
+
await memoryApi.promptRemove(String(item.name ?? item.id), props.cwd).catch(() => null);
|
|
433
|
+
await reload();
|
|
434
|
+
};
|
|
435
|
+
const submitForm = async () => {
|
|
436
|
+
if ((form.name ?? "").trim() === "" || (form.content ?? "").trim() === "") return;
|
|
437
|
+
await memoryApi.promptAdd({
|
|
438
|
+
name: form.name.trim(),
|
|
439
|
+
content: form.content,
|
|
440
|
+
...(form.dimension ?? "").trim() === "" ? {} : { dimension: form.dimension.trim() },
|
|
441
|
+
...(form.difficulty ?? "").trim() === "" ? {} : { difficulty: form.difficulty.trim() },
|
|
442
|
+
...(form.tags ?? "").trim() === "" ? {} : { tags: form.tags.split(",").map((tag) => tag.trim()).filter((tag) => tag !== "") },
|
|
443
|
+
...(form.fallback ?? "").trim() === "" ? {} : { fallback: form.fallback }
|
|
444
|
+
}, props.cwd);
|
|
445
|
+
setForm({});
|
|
446
|
+
setFormOpen(false);
|
|
447
|
+
await reload();
|
|
448
|
+
};
|
|
449
|
+
const field = (key, hint, multiline = false) => (0, import_react.createElement)(multiline ? "textarea" : "input", {
|
|
450
|
+
key,
|
|
451
|
+
value: form[key] ?? "",
|
|
452
|
+
placeholder: hint,
|
|
453
|
+
rows: multiline ? 4 : void 0,
|
|
454
|
+
onChange: (event) => {
|
|
455
|
+
setForm((prev) => ({ ...prev, [key]: event.target.value }));
|
|
456
|
+
},
|
|
457
|
+
style: {
|
|
458
|
+
width: "100%",
|
|
459
|
+
boxSizing: "border-box",
|
|
460
|
+
padding: "6px 10px",
|
|
461
|
+
fontSize: 12,
|
|
462
|
+
fontFamily: "inherit",
|
|
463
|
+
background: "var(--dsw-alias-bg-layer-1, #0f141d)",
|
|
464
|
+
border: "1px solid var(--dsw-alias-border-l2, #1e2836)",
|
|
465
|
+
borderRadius: 6,
|
|
466
|
+
color: "var(--dsw-alias-label-primary, #d8e0ea)",
|
|
467
|
+
outline: "none"
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
return (0, import_react.createElement)(
|
|
471
|
+
"div",
|
|
472
|
+
{ style: { display: "flex", flexDirection: "column", gap: 8, flex: 1, minHeight: 0, overflowY: "auto" } },
|
|
473
|
+
(0, import_react.createElement)(
|
|
474
|
+
"div",
|
|
475
|
+
{ style: { display: "flex", gap: 6 } },
|
|
476
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }, onClick: () => {
|
|
477
|
+
setFormOpen((open) => !open);
|
|
478
|
+
} }, t("promptAdd")),
|
|
479
|
+
(0, import_react.createElement)("button", { type: "button", style: ssid.btn, onClick: () => {
|
|
480
|
+
void reload();
|
|
481
|
+
} }, t("promptRefresh")),
|
|
482
|
+
(0, import_react.createElement)("input", {
|
|
483
|
+
value: query,
|
|
484
|
+
onChange: (event) => {
|
|
485
|
+
setQuery(event.target.value);
|
|
486
|
+
},
|
|
487
|
+
placeholder: t("promptSearch"),
|
|
488
|
+
style: {
|
|
489
|
+
flex: 1,
|
|
490
|
+
padding: "6px 10px",
|
|
491
|
+
fontSize: 12.5,
|
|
492
|
+
boxSizing: "border-box",
|
|
493
|
+
background: "var(--dsw-alias-bg-layer-1, #0f141d)",
|
|
494
|
+
border: "1px solid var(--dsw-alias-border-l2, #1e2836)",
|
|
495
|
+
borderRadius: 8,
|
|
496
|
+
color: "var(--dsw-alias-label-primary, #d8e0ea)",
|
|
497
|
+
outline: "none"
|
|
498
|
+
}
|
|
499
|
+
}),
|
|
500
|
+
note === null ? null : (0, import_react.createElement)("span", { style: { ...ssid.muted, alignSelf: "center" } }, note)
|
|
501
|
+
),
|
|
502
|
+
formOpen ? (0, import_react.createElement)(
|
|
503
|
+
"div",
|
|
504
|
+
{ style: ssid.card },
|
|
505
|
+
(0, import_react.createElement)(
|
|
506
|
+
"div",
|
|
507
|
+
{ style: { display: "flex", flexDirection: "column", gap: 6 } },
|
|
508
|
+
field("name", t("promptFormName")),
|
|
509
|
+
field("dimension", t("promptFormDimension")),
|
|
510
|
+
field("difficulty", t("promptFormDifficulty")),
|
|
511
|
+
field("tags", t("promptFormTags")),
|
|
512
|
+
field("content", t("promptFormContent"), true),
|
|
513
|
+
field("fallback", t("promptFormFallback"), true),
|
|
514
|
+
(0, import_react.createElement)(
|
|
515
|
+
"div",
|
|
516
|
+
{ style: { display: "flex", gap: 6, justifyContent: "flex-end" } },
|
|
517
|
+
(0, import_react.createElement)("button", { type: "button", style: ssid.btn, onClick: () => {
|
|
518
|
+
setFormOpen(false);
|
|
519
|
+
setForm({});
|
|
520
|
+
} }, t("promptFormCancel")),
|
|
521
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }, onClick: () => {
|
|
522
|
+
void submitForm();
|
|
523
|
+
} }, t("promptFormSubmit"))
|
|
524
|
+
)
|
|
525
|
+
)
|
|
526
|
+
) : null,
|
|
527
|
+
filtered.length === 0 ? (0, import_react.createElement)("div", { style: ssid.empty }, t("promptEmpty")) : filtered.map((item) => {
|
|
528
|
+
const key = String(item.id ?? item.name);
|
|
529
|
+
const open = expanded === key;
|
|
530
|
+
return (0, import_react.createElement)(
|
|
531
|
+
"div",
|
|
532
|
+
{ key, style: ssid.card },
|
|
533
|
+
(0, import_react.createElement)(
|
|
534
|
+
"div",
|
|
535
|
+
{ style: { cursor: "pointer", display: "flex", flexDirection: "column", gap: 4 }, onClick: () => {
|
|
536
|
+
void openPrompt(item);
|
|
537
|
+
} },
|
|
538
|
+
(0, import_react.createElement)(
|
|
539
|
+
"div",
|
|
540
|
+
{ style: { display: "flex", alignItems: "baseline", gap: 6 } },
|
|
541
|
+
(0, import_react.createElement)("span", { style: { ...ssid.text, fontWeight: 600 } }, String(item.name ?? "")),
|
|
542
|
+
item.source === "agent" ? (0, import_react.createElement)("span", { style: { fontSize: 10, color: "#f7c94f", fontWeight: 600 } }, t("promptAgent")) : null,
|
|
543
|
+
(0, import_react.createElement)(
|
|
544
|
+
"span",
|
|
545
|
+
{ style: { ...ssid.muted, fontSize: 10.5 } },
|
|
546
|
+
[String(item.dimension ?? ""), String(item.difficulty ?? "")].filter((part) => part !== "").join(" \xB7 ")
|
|
547
|
+
)
|
|
548
|
+
),
|
|
549
|
+
(item.tags ?? []).length > 0 ? (0, import_react.createElement)(
|
|
550
|
+
"div",
|
|
551
|
+
{ style: { display: "flex", gap: 4, flexWrap: "wrap" } },
|
|
552
|
+
item.tags.slice(0, 6).map((tag) => (0, import_react.createElement)("span", { key: tag, style: { fontSize: 10, padding: "1px 6px", borderRadius: 999, border: "1px solid var(--dsw-alias-border-l2, #1e2836)", color: "var(--dsw-alias-label-secondary, #67748a)" } }, tag))
|
|
553
|
+
) : null,
|
|
554
|
+
(0, import_react.createElement)("span", { style: ssid.muted }, String(item.summary ?? "").slice(0, 120))
|
|
555
|
+
),
|
|
556
|
+
open ? (0, import_react.createElement)(
|
|
557
|
+
"div",
|
|
558
|
+
{ style: { marginTop: 6 } },
|
|
559
|
+
detail === null ? (0, import_react.createElement)("div", { style: ssid.muted }, "\u2026") : (0, import_react.createElement)("pre", { style: { ...ssid.muted, whiteSpace: "pre-wrap", margin: 0, fontSize: 12, maxHeight: 180, overflowY: "auto" } }, String(detail.body ?? "")),
|
|
560
|
+
(0, import_react.createElement)(
|
|
561
|
+
"div",
|
|
562
|
+
{ style: { display: "flex", gap: 6, marginTop: 6, justifyContent: "flex-end" } },
|
|
563
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: ssid.accent, borderColor: ssid.accent }, onClick: () => {
|
|
564
|
+
insert(item);
|
|
565
|
+
} }, t("promptInsert")),
|
|
566
|
+
(0, import_react.createElement)("button", { type: "button", style: { ...ssid.btn, color: "#f76f4f", borderColor: "#f76f4f" }, onClick: () => {
|
|
567
|
+
void remove(item);
|
|
568
|
+
} }, t("promptDelete"))
|
|
569
|
+
)
|
|
570
|
+
) : null
|
|
571
|
+
);
|
|
572
|
+
})
|
|
573
|
+
);
|
|
574
|
+
}
|
|
304
575
|
var DAY_MS = 864e5;
|
|
305
576
|
var COLD_DAYS = 30;
|
|
306
577
|
function fmtDate(ms) {
|
|
@@ -312,6 +583,7 @@ function isCold(usedAt, now = Date.now()) {
|
|
|
312
583
|
}
|
|
313
584
|
function MemoryView(props) {
|
|
314
585
|
const t = useT();
|
|
586
|
+
const [mode, setMode] = (0, import_react.useState)("mem");
|
|
315
587
|
const [records, setRecords] = (0, import_react.useState)([]);
|
|
316
588
|
const [query, setQuery] = (0, import_react.useState)("");
|
|
317
589
|
const [namespace, setNamespace] = (0, import_react.useState)(null);
|
|
@@ -409,9 +681,32 @@ function MemoryView(props) {
|
|
|
409
681
|
{ key: "ondemand", label: t("groupOnDemand"), items: filtered.filter((record) => record.status === "approved" && !record.injected) },
|
|
410
682
|
{ key: "injected", label: t("groupInjected"), items: filtered.filter((record) => record.status === "approved" && record.injected) }
|
|
411
683
|
].filter((group) => group.items.length > 0);
|
|
684
|
+
const modeTabs = (0, import_react.createElement)(
|
|
685
|
+
"div",
|
|
686
|
+
{ style: { display: "flex", gap: 4 } },
|
|
687
|
+
["mem", "prompt"].map(
|
|
688
|
+
(m) => (0, import_react.createElement)("button", {
|
|
689
|
+
key: m,
|
|
690
|
+
type: "button",
|
|
691
|
+
onClick: () => {
|
|
692
|
+
setMode(m);
|
|
693
|
+
},
|
|
694
|
+
style: { flex: 1, ...ssid.btn, ...mode === m ? { color: ssid.accent, borderColor: ssid.accent } : {} }
|
|
695
|
+
}, m === "mem" ? t("tabMemory") : t("tabPrompt"))
|
|
696
|
+
)
|
|
697
|
+
);
|
|
698
|
+
if (mode === "prompt") {
|
|
699
|
+
return (0, import_react.createElement)(
|
|
700
|
+
"div",
|
|
701
|
+
{ style: ssid.wrap },
|
|
702
|
+
modeTabs,
|
|
703
|
+
(0, import_react.createElement)(PromptView, { visible: props.visible, cwd: props.cwd, ctx: props.ctx })
|
|
704
|
+
);
|
|
705
|
+
}
|
|
412
706
|
return (0, import_react.createElement)(
|
|
413
707
|
"div",
|
|
414
708
|
{ style: ssid.wrap },
|
|
709
|
+
modeTabs,
|
|
415
710
|
(0, import_react.createElement)(
|
|
416
711
|
"div",
|
|
417
712
|
{ style: ssid.header },
|
package/dist/engine.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* is always created `suggested` and becomes effective only through `setStatus`.
|
|
6
6
|
*/
|
|
7
7
|
import { Service } from '@deepseek-ai/cordis';
|
|
8
|
+
import { type PromptFile } from './prompt-files.ts';
|
|
8
9
|
declare const memoryIdBrand: unique symbol;
|
|
9
10
|
/** Opaque identity of one stored memory record. */
|
|
10
11
|
export type MemoryId = string & {
|
|
@@ -18,6 +19,27 @@ export type MemoryNamespace = 'global' | 'project';
|
|
|
18
19
|
* 审核通过。注入与否由独立维度 `injected` 控制(见 MemoryRecord)。
|
|
19
20
|
*/
|
|
20
21
|
export type MemoryStatus = 'suggested' | 'approved';
|
|
22
|
+
/**
|
|
23
|
+
* 记录类别(0.6.0 模板库):'fact' = 常规记忆(缺省,旧数据即此);
|
|
24
|
+
* 'prompt' = 提示词模板索引记录(文件是事实源,见 prompt-files.ts)。
|
|
25
|
+
*/
|
|
26
|
+
export type MemoryKind = 'fact' | 'prompt';
|
|
27
|
+
/** prompt 索引记录的附加元数据(与 md 文件 frontmatter 对齐,工具/UI 直接取用)。 */
|
|
28
|
+
export interface PromptMetaIndex {
|
|
29
|
+
seq?: number;
|
|
30
|
+
name: string;
|
|
31
|
+
dimension?: string;
|
|
32
|
+
difficulty?: string;
|
|
33
|
+
tags: string[];
|
|
34
|
+
/** 索引摘要(正文前 200 字)。 */
|
|
35
|
+
summary: string;
|
|
36
|
+
/** md 文件绝对路径(引擎内部使用;工具输出不暴露)。 */
|
|
37
|
+
path: string;
|
|
38
|
+
/** 索引时的文件 mtime(惰性刷新比对)。 */
|
|
39
|
+
mtime: number;
|
|
40
|
+
/** user=用户创建;agent=模型新增(UI 角标)。 */
|
|
41
|
+
source: 'user' | 'agent';
|
|
42
|
+
}
|
|
21
43
|
export interface MemoryRecord {
|
|
22
44
|
id: MemoryId;
|
|
23
45
|
namespace: MemoryNamespace;
|
|
@@ -30,6 +52,10 @@ export interface MemoryRecord {
|
|
|
30
52
|
updatedAt: number;
|
|
31
53
|
/** 最近一次被 memory_search 命中的时间(0.5.3 冷热追踪);无则未命中过。 */
|
|
32
54
|
lastUsedAt?: number;
|
|
55
|
+
/** 记录类别(缺省 'fact')。 */
|
|
56
|
+
kind?: MemoryKind;
|
|
57
|
+
/** prompt 记录元数据(仅 kind==='prompt')。 */
|
|
58
|
+
meta?: PromptMetaIndex;
|
|
33
59
|
}
|
|
34
60
|
export interface MemoryWrite {
|
|
35
61
|
content: string;
|
|
@@ -40,6 +66,8 @@ export interface MemoryFilter {
|
|
|
40
66
|
namespace?: MemoryNamespace;
|
|
41
67
|
status?: MemoryStatus;
|
|
42
68
|
injected?: boolean;
|
|
69
|
+
/** 记录类别过滤(0.6.0);缺省不过滤(工具层显式传 kind='fact' 保持旧行为)。 */
|
|
70
|
+
kind?: MemoryKind;
|
|
43
71
|
}
|
|
44
72
|
export interface MemoryHit {
|
|
45
73
|
record: MemoryRecord;
|
|
@@ -99,6 +127,9 @@ export interface MemoryConfig {
|
|
|
99
127
|
embeddings?: MemoryEmbeddings;
|
|
100
128
|
/** 语义侧参与融合的 topK(0.5.2);默认 5。 */
|
|
101
129
|
semanticTopK?: number;
|
|
130
|
+
/** 全局提示词模板根目录(0.6.0);默认 `$DSH_HOME/prompt-library`。
|
|
131
|
+
* project 模板随工作区(`<workspace>/.dsh/prompt-library`,与记忆同构)。 */
|
|
132
|
+
promptGlobalRoot?: string;
|
|
102
133
|
}
|
|
103
134
|
/**
|
|
104
135
|
* Cross-session plaintext memory over the storage hub, split by namespace:
|
|
@@ -184,5 +215,42 @@ export declare class MemoryEngine extends Service {
|
|
|
184
215
|
private recordsOf;
|
|
185
216
|
private tableFor;
|
|
186
217
|
private requireTable;
|
|
218
|
+
/** 全局模板根(默认 `$DSH_HOME/prompt-library`,可配置覆盖)。 */
|
|
219
|
+
private promptGlobalDir;
|
|
220
|
+
/** project 模板根(随工作区 `<workspace>/.dsh/prompt-library`,与记忆同构)。 */
|
|
221
|
+
private promptProjectDir;
|
|
222
|
+
/** 稳定索引 id(按路径 hash;文件删除后随刷新清理)。 */
|
|
223
|
+
private promptIdOf;
|
|
224
|
+
/**
|
|
225
|
+
* 扫描模板目录并同步索引(mtime 惰性:未变条目零写;目录缺失 = 空库)。
|
|
226
|
+
* @returns 统计(扫描数/变更数/清理数 + 坏文件警告)。
|
|
227
|
+
*/
|
|
228
|
+
refreshPromptIndex(projectCwd?: string): Promise<{
|
|
229
|
+
scan: number;
|
|
230
|
+
changed: number;
|
|
231
|
+
removed: number;
|
|
232
|
+
warnings: string[];
|
|
233
|
+
}>;
|
|
234
|
+
/**
|
|
235
|
+
* 取模板(id 或名称/路径片段定位),返回 md 全文(直读文件,忽略索引缓存)。
|
|
236
|
+
* @throws 未命中或文件解析失败(错误信息可读)。
|
|
237
|
+
*/
|
|
238
|
+
promptGet(nameOrId: string, projectCwd?: string): Promise<PromptFile>;
|
|
239
|
+
/**
|
|
240
|
+
* 新增模板:写 md 文件(序号自动分配/文件名安全化/同名防覆盖)+ 刷新索引。
|
|
241
|
+
* 返回索引记录(模型新增 source='agent',UI 角标提示)。
|
|
242
|
+
*/
|
|
243
|
+
promptAdd(input: {
|
|
244
|
+
name: string;
|
|
245
|
+
dimension?: string;
|
|
246
|
+
difficulty?: string;
|
|
247
|
+
tags?: string[];
|
|
248
|
+
content: string;
|
|
249
|
+
fallback?: string;
|
|
250
|
+
source: 'user' | 'agent';
|
|
251
|
+
namespace?: MemoryNamespace;
|
|
252
|
+
}, projectCwd?: string): Promise<MemoryRecord>;
|
|
253
|
+
/** 删除模板:md 文件与索引一并删除;未命中返回 false。 */
|
|
254
|
+
promptRemove(idOrName: string, projectCwd?: string): Promise<boolean>;
|
|
187
255
|
}
|
|
188
256
|
export {};
|
package/dist/engine.js
CHANGED
|
@@ -13,6 +13,7 @@ import { z } from 'zod';
|
|
|
13
13
|
import { defineDomain, domainTable, DomainFacility } from '@deepseek-ai/dsh-storage-domain';
|
|
14
14
|
import { JsonStorageBackend } from '@deepseek-ai/dsh-storage-json';
|
|
15
15
|
import { bm25FieldScores, cosineSimilarity, rrfFuse } from "./bm25.js";
|
|
16
|
+
import { parsePromptFile, scanPromptDir, writePromptFile } from "./prompt-files.js";
|
|
16
17
|
/** Brand a string as a {@link MemoryId} (compile-time only). */
|
|
17
18
|
export function MemoryId(id) {
|
|
18
19
|
return id;
|
|
@@ -27,6 +28,18 @@ const blockSchema = z.object({
|
|
|
27
28
|
updatedAt: z.number(),
|
|
28
29
|
lastUsedAt: z.number().optional(),
|
|
29
30
|
vector: z.array(z.number()).optional(),
|
|
31
|
+
kind: z.enum(['fact', 'prompt']).optional(),
|
|
32
|
+
meta: z.object({
|
|
33
|
+
seq: z.number().optional(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
dimension: z.string().optional(),
|
|
36
|
+
difficulty: z.string().optional(),
|
|
37
|
+
tags: z.array(z.string()),
|
|
38
|
+
summary: z.string(),
|
|
39
|
+
path: z.string(),
|
|
40
|
+
mtime: z.number(),
|
|
41
|
+
source: z.enum(['user', 'agent']),
|
|
42
|
+
}).optional(),
|
|
30
43
|
});
|
|
31
44
|
/** Shared table shape; the two domains differ only by name and backend route. */
|
|
32
45
|
function memorySpec(name) {
|
|
@@ -75,7 +88,7 @@ function normalizeBlock(block) {
|
|
|
75
88
|
return { status: block.status, injected: block.injected ?? false };
|
|
76
89
|
}
|
|
77
90
|
function toRecord(id, block) {
|
|
78
|
-
|
|
91
|
+
const base = {
|
|
79
92
|
id: MemoryId(id),
|
|
80
93
|
namespace: block.namespace,
|
|
81
94
|
...normalizeBlock(block),
|
|
@@ -85,6 +98,12 @@ function toRecord(id, block) {
|
|
|
85
98
|
updatedAt: block.updatedAt,
|
|
86
99
|
...block.lastUsedAt === undefined ? {} : { lastUsedAt: block.lastUsedAt },
|
|
87
100
|
};
|
|
101
|
+
// 0.6.0 模板库字段(缺省 fact;prompt 元数据透传)
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
...block.kind === undefined ? {} : { kind: block.kind },
|
|
105
|
+
...block.meta === undefined ? {} : { meta: block.meta },
|
|
106
|
+
};
|
|
88
107
|
}
|
|
89
108
|
/** Harness-home root for `global` memories. */
|
|
90
109
|
function globalRoot() {
|
|
@@ -234,7 +253,8 @@ export class MemoryEngine extends Service {
|
|
|
234
253
|
async list(filter, projectCwd) {
|
|
235
254
|
const records = await this.allRecords(filter?.namespace, projectCwd);
|
|
236
255
|
return records.filter(record => (filter?.status === undefined || record.status === filter.status)
|
|
237
|
-
&& (filter?.injected === undefined || record.injected === filter.injected)
|
|
256
|
+
&& (filter?.injected === undefined || record.injected === filter.injected)
|
|
257
|
+
&& (filter?.kind === undefined || (record.kind ?? 'fact') === filter.kind));
|
|
238
258
|
}
|
|
239
259
|
async search(query, filter, projectCwd) {
|
|
240
260
|
const records = await this.list(filter, projectCwd);
|
|
@@ -404,36 +424,35 @@ export class MemoryEngine extends Service {
|
|
|
404
424
|
* 「常驻注入」开关调用(remote.setInjected)。
|
|
405
425
|
*/
|
|
406
426
|
async setInjected(id, injected, projectCwd) {
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
const record = toRecord(id, updated);
|
|
417
|
-
this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
|
|
418
|
-
return record;
|
|
419
|
-
}
|
|
420
|
-
const project = await this.projectTableFor(projectCwd);
|
|
421
|
-
if (project !== undefined) {
|
|
422
|
-
const block = project.get(id);
|
|
423
|
-
if (block !== undefined) {
|
|
424
|
-
const updated = {
|
|
425
|
-
...block,
|
|
426
|
-
status: normalizeBlock(block).status,
|
|
427
|
-
injected,
|
|
428
|
-
updatedAt: Date.now(),
|
|
429
|
-
};
|
|
430
|
-
await project.put(id, updated);
|
|
431
|
-
const record = toRecord(id, updated);
|
|
432
|
-
this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
|
|
433
|
-
return record;
|
|
427
|
+
const locate = async () => {
|
|
428
|
+
const global = this.requireTable('global').get(id);
|
|
429
|
+
if (global !== undefined)
|
|
430
|
+
return { table: this.requireTable('global'), block: global };
|
|
431
|
+
const project = await this.projectTableFor(projectCwd);
|
|
432
|
+
if (project !== undefined) {
|
|
433
|
+
const block = project.get(id);
|
|
434
|
+
if (block !== undefined)
|
|
435
|
+
return { table: project, block };
|
|
434
436
|
}
|
|
437
|
+
return undefined;
|
|
438
|
+
};
|
|
439
|
+
const found = await locate();
|
|
440
|
+
if (found === undefined)
|
|
441
|
+
throw new Error(`cannot set injected of unknown memory '${id}'`);
|
|
442
|
+
// 模板永不注入(0.6.0):prompt 记录拒绝开关(UI 对该类不展示开关,此处兜底)
|
|
443
|
+
if ((found.block.kind ?? 'fact') === 'prompt') {
|
|
444
|
+
throw new Error(`cannot set injected of prompt record '${id}': prompt templates are never injected`);
|
|
435
445
|
}
|
|
436
|
-
|
|
446
|
+
const updated = {
|
|
447
|
+
...found.block,
|
|
448
|
+
status: normalizeBlock(found.block).status,
|
|
449
|
+
injected,
|
|
450
|
+
updatedAt: Date.now(),
|
|
451
|
+
};
|
|
452
|
+
await found.table.put(id, updated);
|
|
453
|
+
const record = toRecord(id, updated);
|
|
454
|
+
this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
|
|
455
|
+
return record;
|
|
437
456
|
}
|
|
438
457
|
/**
|
|
439
458
|
* 修改一条记忆的内容/关键词(0.3.1 整理记忆用)。内容被模型改动后
|
|
@@ -528,4 +547,151 @@ export class MemoryEngine extends Service {
|
|
|
528
547
|
throw new Error('memory engine is not started yet');
|
|
529
548
|
return table;
|
|
530
549
|
}
|
|
550
|
+
// ── 提示词模板库(0.6.0):md 文件是事实源,索引记录 kind='prompt' ──────
|
|
551
|
+
/** 全局模板根(默认 `$DSH_HOME/prompt-library`,可配置覆盖)。 */
|
|
552
|
+
promptGlobalDir() {
|
|
553
|
+
return this.config.promptGlobalRoot ?? join(process.env.DSH_HOME ?? join(homedir(), '.dsh'), 'prompt-library');
|
|
554
|
+
}
|
|
555
|
+
/** project 模板根(随工作区 `<workspace>/.dsh/prompt-library`,与记忆同构)。 */
|
|
556
|
+
promptProjectDir(projectCwd) {
|
|
557
|
+
if (projectCwd === undefined || projectCwd === '')
|
|
558
|
+
return '';
|
|
559
|
+
return join(projectCwd, '.dsh', 'prompt-library');
|
|
560
|
+
}
|
|
561
|
+
/** 稳定索引 id(按路径 hash;文件删除后随刷新清理)。 */
|
|
562
|
+
promptIdOf(path) {
|
|
563
|
+
let h = 5381;
|
|
564
|
+
const text = path.toLowerCase();
|
|
565
|
+
for (let i = 0; i < text.length; i++)
|
|
566
|
+
h = ((h << 5) + h + text.charCodeAt(i)) | 0;
|
|
567
|
+
return MemoryId(`prompt-${Math.abs(h).toString(36)}`);
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* 扫描模板目录并同步索引(mtime 惰性:未变条目零写;目录缺失 = 空库)。
|
|
571
|
+
* @returns 统计(扫描数/变更数/清理数 + 坏文件警告)。
|
|
572
|
+
*/
|
|
573
|
+
async refreshPromptIndex(projectCwd) {
|
|
574
|
+
const globalDir = this.promptGlobalDir();
|
|
575
|
+
const projectDir = this.promptProjectDir(projectCwd);
|
|
576
|
+
const globalScan = scanPromptDir(globalDir);
|
|
577
|
+
const projectScan = scanPromptDir(projectDir);
|
|
578
|
+
const scanned = [...globalScan.files, ...projectScan.files];
|
|
579
|
+
const warnings = [...globalScan.warnings, ...projectScan.warnings];
|
|
580
|
+
const globalTable = this.requireTable('global');
|
|
581
|
+
const projectTable = projectCwd !== undefined && projectCwd !== ''
|
|
582
|
+
? await this.projectTableFor(projectCwd)
|
|
583
|
+
: undefined;
|
|
584
|
+
const existing = await this.list({ kind: 'prompt' }, projectCwd);
|
|
585
|
+
const existingById = new Map(existing.map(record => [String(record.id), record]));
|
|
586
|
+
const seen = new Set();
|
|
587
|
+
let changed = 0;
|
|
588
|
+
let removed = 0;
|
|
589
|
+
const now = Date.now();
|
|
590
|
+
for (const file of scanned) {
|
|
591
|
+
const id = this.promptIdOf(file.path);
|
|
592
|
+
seen.add(String(id));
|
|
593
|
+
const isGlobal = file.path.startsWith(globalDir);
|
|
594
|
+
const table = isGlobal ? globalTable : projectTable;
|
|
595
|
+
if (table === undefined)
|
|
596
|
+
continue;
|
|
597
|
+
const prev = existingById.get(String(id));
|
|
598
|
+
if (prev?.meta !== undefined && prev.meta.mtime === file.mtime)
|
|
599
|
+
continue; // mtime 未变:零写
|
|
600
|
+
const keywords = [
|
|
601
|
+
file.meta.name,
|
|
602
|
+
...(file.meta.dimension !== undefined ? [file.meta.dimension] : []),
|
|
603
|
+
...(file.meta.difficulty !== undefined ? [file.meta.difficulty] : []),
|
|
604
|
+
...file.meta.tags,
|
|
605
|
+
].filter(keyword => keyword !== '').map(keyword => keyword.toLowerCase());
|
|
606
|
+
const block = {
|
|
607
|
+
namespace: isGlobal ? 'global' : 'project',
|
|
608
|
+
status: 'approved', // 文件存在即生效(设计文档 §三:模板不做审核流)
|
|
609
|
+
injected: false, // 模板永不注入
|
|
610
|
+
content: [file.meta.name, file.meta.dimension ?? '', file.meta.difficulty ?? '', file.meta.tags.join(' '), file.summary]
|
|
611
|
+
.filter(part => part !== '').join(' '),
|
|
612
|
+
keywords,
|
|
613
|
+
createdAt: prev?.createdAt ?? now,
|
|
614
|
+
updatedAt: now,
|
|
615
|
+
kind: 'prompt',
|
|
616
|
+
meta: {
|
|
617
|
+
seq: file.meta.seq,
|
|
618
|
+
name: file.meta.name,
|
|
619
|
+
dimension: file.meta.dimension,
|
|
620
|
+
difficulty: file.meta.difficulty,
|
|
621
|
+
tags: file.meta.tags,
|
|
622
|
+
summary: file.summary,
|
|
623
|
+
path: file.path,
|
|
624
|
+
mtime: file.mtime,
|
|
625
|
+
source: file.meta.source,
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
await table.put(id, block);
|
|
629
|
+
changed++;
|
|
630
|
+
}
|
|
631
|
+
for (const record of existing) {
|
|
632
|
+
if (seen.has(String(record.id)))
|
|
633
|
+
continue;
|
|
634
|
+
if (record.meta === undefined)
|
|
635
|
+
continue;
|
|
636
|
+
const isGlobal = record.meta.path.startsWith(globalDir);
|
|
637
|
+
const table = isGlobal ? globalTable : projectTable;
|
|
638
|
+
if (table !== undefined) {
|
|
639
|
+
await table.delete(record.id);
|
|
640
|
+
removed++;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { scan: scanned.length, changed, removed, warnings };
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 取模板(id 或名称/路径片段定位),返回 md 全文(直读文件,忽略索引缓存)。
|
|
647
|
+
* @throws 未命中或文件解析失败(错误信息可读)。
|
|
648
|
+
*/
|
|
649
|
+
async promptGet(nameOrId, projectCwd) {
|
|
650
|
+
const records = await this.list({ kind: 'prompt' }, projectCwd);
|
|
651
|
+
const needle = nameOrId.trim();
|
|
652
|
+
const hit = records.find(record => {
|
|
653
|
+
if (record.meta === undefined)
|
|
654
|
+
return false;
|
|
655
|
+
const stem = (record.meta.path.split(/[\\/]/).pop() ?? '').replace(/\.md$/i, '');
|
|
656
|
+
const afterSeq = stem.replace(/^\d+_/, ''); // 去掉序号前缀:36_名称 → 名称
|
|
657
|
+
return String(record.id) === needle || afterSeq === needle || afterSeq.includes(needle);
|
|
658
|
+
});
|
|
659
|
+
if (hit === undefined || hit.meta === undefined) {
|
|
660
|
+
throw new Error(`prompt '${nameOrId}' not found(可用 prompt_list 查看全部)`);
|
|
661
|
+
}
|
|
662
|
+
const text = readFileSync(hit.meta.path, 'utf8');
|
|
663
|
+
const parsed = parsePromptFile(hit.meta.path, text);
|
|
664
|
+
if (!parsed.ok)
|
|
665
|
+
throw new Error(`prompt file ${hit.meta.path} parse failed: ${parsed.error}`);
|
|
666
|
+
return parsed.file;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* 新增模板:写 md 文件(序号自动分配/文件名安全化/同名防覆盖)+ 刷新索引。
|
|
670
|
+
* 返回索引记录(模型新增 source='agent',UI 角标提示)。
|
|
671
|
+
*/
|
|
672
|
+
async promptAdd(input, projectCwd) {
|
|
673
|
+
const dir = input.namespace === 'project' ? this.promptProjectDir(projectCwd) : this.promptGlobalDir();
|
|
674
|
+
if (dir === '')
|
|
675
|
+
throw new Error('cannot write project prompt without a workspace cwd');
|
|
676
|
+
const path = writePromptFile(dir, input);
|
|
677
|
+
await this.refreshPromptIndex(projectCwd);
|
|
678
|
+
const found = (await this.list({ kind: 'prompt' }, projectCwd))
|
|
679
|
+
.find(record => String(record.id) === String(this.promptIdOf(path)));
|
|
680
|
+
if (found === undefined)
|
|
681
|
+
throw new Error('prompt written but index refresh failed');
|
|
682
|
+
return found;
|
|
683
|
+
}
|
|
684
|
+
/** 删除模板:md 文件与索引一并删除;未命中返回 false。 */
|
|
685
|
+
async promptRemove(idOrName, projectCwd) {
|
|
686
|
+
let file = null;
|
|
687
|
+
try {
|
|
688
|
+
file = await this.promptGet(idOrName, projectCwd);
|
|
689
|
+
}
|
|
690
|
+
catch {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
rmSync(file.path, { force: true });
|
|
694
|
+
await this.refreshPromptIndex(projectCwd);
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
531
697
|
}
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,21 @@ const HIT_SCHEMA = {
|
|
|
38
38
|
score: { type: 'number', required: true },
|
|
39
39
|
},
|
|
40
40
|
};
|
|
41
|
+
/** 模板记录(0.6.0):不暴露本地文件路径(meta.path 引擎内部使用)。 */
|
|
42
|
+
const PROMPT_RECORD_SCHEMA = {
|
|
43
|
+
type: 'object',
|
|
44
|
+
additionalProperties: false,
|
|
45
|
+
properties: {
|
|
46
|
+
id: { type: 'string', required: true },
|
|
47
|
+
name: { type: 'string', required: true },
|
|
48
|
+
dimension: { type: 'string' },
|
|
49
|
+
difficulty: { type: 'string' },
|
|
50
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
51
|
+
summary: { type: 'string', required: true },
|
|
52
|
+
source: { type: 'string', enum: ['user', 'agent'], required: true },
|
|
53
|
+
namespace: { type: 'string', enum: ['global', 'project'], required: true },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
41
56
|
function recordValue(record) {
|
|
42
57
|
return {
|
|
43
58
|
id: String(record.id),
|
|
@@ -54,6 +69,19 @@ function recordValue(record) {
|
|
|
54
69
|
function hitValue(hit) {
|
|
55
70
|
return { record: recordValue(hit.record), score: hit.score };
|
|
56
71
|
}
|
|
72
|
+
function promptValue(record) {
|
|
73
|
+
const meta = record.meta;
|
|
74
|
+
return {
|
|
75
|
+
id: String(record.id),
|
|
76
|
+
name: meta?.name ?? '',
|
|
77
|
+
...meta?.dimension === undefined ? {} : { dimension: meta.dimension },
|
|
78
|
+
...meta?.difficulty === undefined ? {} : { difficulty: meta.difficulty },
|
|
79
|
+
tags: meta?.tags ?? [],
|
|
80
|
+
summary: meta?.summary ?? record.content.slice(0, 200),
|
|
81
|
+
source: meta?.source ?? 'user',
|
|
82
|
+
namespace: record.namespace,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
57
85
|
// 0.3.0:审核语义双语(host 侧 GUIDANCE 无法跟随 DSH locale 动态切换,
|
|
58
86
|
// 采用中英双语都写、模型自取——设计文档「风险与注意」)。
|
|
59
87
|
const GUIDANCE = 'Use memory tools for cross-session preferences, habits, and project conventions. '
|
|
@@ -192,6 +220,7 @@ export async function apply(ctx, config) {
|
|
|
192
220
|
query: { type: 'string', required: true, description: 'Keyword query. 关键词查询.' },
|
|
193
221
|
namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace. 限定单个命名空间.' },
|
|
194
222
|
status: { type: 'string', enum: ['suggested', 'approved'], description: 'Restrict to one review status. 限定审核状态.' },
|
|
223
|
+
kind: { type: 'string', enum: ['fact', 'prompt'], description: 'Restrict by record kind — fact (memories, default) or prompt (templates; use prompt_search instead). 按记录类别过滤:fact=记忆(默认)/ prompt=模板(模板请用 prompt_search).' },
|
|
195
224
|
},
|
|
196
225
|
output: {
|
|
197
226
|
schema: { type: 'array', items: HIT_SCHEMA },
|
|
@@ -201,10 +230,141 @@ export async function apply(ctx, config) {
|
|
|
201
230
|
return memory.search(args.query, {
|
|
202
231
|
...args.namespace === undefined ? {} : { namespace: args.namespace },
|
|
203
232
|
...args.status === undefined ? {} : { status: args.status },
|
|
233
|
+
...args.kind === undefined ? {} : { kind: args.kind },
|
|
204
234
|
}, execProjectCwd(exec)).then(hits => hits.map(hitValue));
|
|
205
235
|
},
|
|
206
236
|
presentCall: args => present('Search memory', 'read', args.query),
|
|
207
237
|
}));
|
|
238
|
+
ctx.tools.register(defineTool({
|
|
239
|
+
name: 'prompt_search',
|
|
240
|
+
description: 'Search prompt templates by keyword plus dimension/difficulty/tag filters. Returns matching summaries; call prompt_get for the full text. 按关键词+维度/难度/标签检索提示词模板,返回匹配摘要;取全文用 prompt_get。',
|
|
241
|
+
parameters: {
|
|
242
|
+
query: { type: 'string', required: true, description: 'Keyword query (name/tag/body). 关键词查询(名称/标签/正文).' },
|
|
243
|
+
dimension: { type: 'string', description: 'Filter by dimension (前端/后端/…). 按维度过滤.' },
|
|
244
|
+
difficulty: { type: 'string', description: 'Filter by difficulty (L1-L5/LX). 按难度过滤.' },
|
|
245
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Filter by any of these tags. 按任一标签过滤.' },
|
|
246
|
+
namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace. 限定单个命名空间.' },
|
|
247
|
+
},
|
|
248
|
+
output: {
|
|
249
|
+
schema: { type: 'array', items: PROMPT_RECORD_SCHEMA },
|
|
250
|
+
render: (_args, value) => renderJson(value),
|
|
251
|
+
},
|
|
252
|
+
execute(args, exec) {
|
|
253
|
+
return memory.refreshPromptIndex(execProjectCwd(exec)).then(async () => {
|
|
254
|
+
const all = await memory.list({ kind: 'prompt', ...args.namespace === undefined ? {} : { namespace: args.namespace } }, execProjectCwd(exec));
|
|
255
|
+
const filtered = all.filter(record => {
|
|
256
|
+
const meta = record.meta;
|
|
257
|
+
if (meta === undefined)
|
|
258
|
+
return true;
|
|
259
|
+
if (args.dimension !== undefined && args.dimension !== '' && meta.dimension !== args.dimension)
|
|
260
|
+
return false;
|
|
261
|
+
if (args.difficulty !== undefined && args.difficulty !== '' && meta.difficulty !== args.difficulty)
|
|
262
|
+
return false;
|
|
263
|
+
if (args.tags !== undefined && args.tags.length > 0 && !args.tags.some(tag => meta.tags.includes(tag)))
|
|
264
|
+
return false;
|
|
265
|
+
return true;
|
|
266
|
+
});
|
|
267
|
+
const quoted = JSON.stringify(args.query ?? '');
|
|
268
|
+
const hits = await memory.search(quoted, { kind: 'prompt', ...args.namespace === undefined ? {} : { namespace: args.namespace } }, execProjectCwd(exec));
|
|
269
|
+
const hitIds = new Set(hits.map(hit => String(hit.record.id)));
|
|
270
|
+
// 过滤器命中列表按检索分排序在前,未命中关键词的过滤命中排后(按名称)
|
|
271
|
+
const byQuery = filtered.filter(record => hitIds.has(String(record.id)));
|
|
272
|
+
const rest = filtered
|
|
273
|
+
.filter(record => !hitIds.has(String(record.id)))
|
|
274
|
+
.sort((a, b) => (a.meta?.name ?? '').localeCompare(b.meta?.name ?? ''));
|
|
275
|
+
return [...byQuery, ...rest].map(promptValue);
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
presentCall: args => present('Search prompt library', 'read', args.query),
|
|
279
|
+
}));
|
|
280
|
+
ctx.tools.register(defineTool({
|
|
281
|
+
name: 'prompt_get',
|
|
282
|
+
description: 'Fetch a prompt template full text by id or name (name matches the file stem without the seq prefix). 按 id 或名称取模板全文(名称 = 文件名去序号部分)。',
|
|
283
|
+
parameters: {
|
|
284
|
+
nameOrId: { type: 'string', required: true, description: 'Template id or name. 模板 id 或名称.' },
|
|
285
|
+
},
|
|
286
|
+
output: {
|
|
287
|
+
schema: { type: 'object', additionalProperties: false, properties: {
|
|
288
|
+
name: { type: 'string', required: true },
|
|
289
|
+
dimension: { type: 'string' },
|
|
290
|
+
difficulty: { type: 'string' },
|
|
291
|
+
tags: { type: 'array', items: { type: 'string' } },
|
|
292
|
+
body: { type: 'string', required: true },
|
|
293
|
+
fallback: { type: 'string' },
|
|
294
|
+
} },
|
|
295
|
+
render: (_args, value) => renderJson(value),
|
|
296
|
+
},
|
|
297
|
+
execute(args, exec) {
|
|
298
|
+
return memory.promptGet(args.nameOrId, execProjectCwd(exec)).then(file => ({
|
|
299
|
+
name: file.meta.name,
|
|
300
|
+
...file.meta.dimension === undefined ? {} : { dimension: file.meta.dimension },
|
|
301
|
+
...file.meta.difficulty === undefined ? {} : { difficulty: file.meta.difficulty },
|
|
302
|
+
tags: file.meta.tags,
|
|
303
|
+
body: file.body,
|
|
304
|
+
...file.fallback === null ? {} : { fallback: file.fallback },
|
|
305
|
+
}));
|
|
306
|
+
},
|
|
307
|
+
presentCall: args => present('Get prompt template', 'read', args.nameOrId),
|
|
308
|
+
}));
|
|
309
|
+
ctx.tools.register(defineTool({
|
|
310
|
+
name: 'prompt_list',
|
|
311
|
+
description: 'List every prompt template index (no full text), optionally filtered by dimension/difficulty/tag. 列出全部模板索引(不含全文),可按维度/难度/标签过滤。',
|
|
312
|
+
parameters: {
|
|
313
|
+
dimension: { type: 'string', description: '按维度过滤.' },
|
|
314
|
+
difficulty: { type: 'string', description: '按难度过滤.' },
|
|
315
|
+
tags: { type: 'array', items: { type: 'string' }, description: '按任一标签过滤.' },
|
|
316
|
+
namespace: { type: 'string', enum: ['global', 'project'], description: '限定单个命名空间.' },
|
|
317
|
+
},
|
|
318
|
+
output: {
|
|
319
|
+
schema: { type: 'array', items: PROMPT_RECORD_SCHEMA },
|
|
320
|
+
render: (_args, value) => renderJson(value),
|
|
321
|
+
},
|
|
322
|
+
execute(args, exec) {
|
|
323
|
+
return memory.refreshPromptIndex(execProjectCwd(exec)).then(() => memory.list({ kind: 'prompt', ...args.namespace === undefined ? {} : { namespace: args.namespace } }, execProjectCwd(exec))).then(records => records.filter(record => {
|
|
324
|
+
const meta = record.meta;
|
|
325
|
+
if (meta === undefined)
|
|
326
|
+
return false;
|
|
327
|
+
if (args.dimension !== undefined && args.dimension !== '' && meta.dimension !== args.dimension)
|
|
328
|
+
return false;
|
|
329
|
+
if (args.difficulty !== undefined && args.difficulty !== '' && meta.difficulty !== args.difficulty)
|
|
330
|
+
return false;
|
|
331
|
+
if (args.tags !== undefined && args.tags.length > 0 && !args.tags.some(tag => meta.tags.includes(tag)))
|
|
332
|
+
return false;
|
|
333
|
+
return true;
|
|
334
|
+
}).sort((a, b) => (a.meta?.seq ?? 0) - (b.meta?.seq ?? 0)).map(promptValue));
|
|
335
|
+
},
|
|
336
|
+
presentCall: () => present('List prompt templates', 'read'),
|
|
337
|
+
}));
|
|
338
|
+
ctx.tools.register(defineTool({
|
|
339
|
+
name: 'prompt_add',
|
|
340
|
+
description: 'Add one prompt template to the library (writes a metadata-marked md file; source=agent is surfaced in the UI). 新增一条提示词模板(写元数据 md 文件;source=agent 在 UI 角标提示)。',
|
|
341
|
+
parameters: {
|
|
342
|
+
name: { type: 'string', required: true, description: 'Template name (also the file stem). 模板名称(也是文件名).' },
|
|
343
|
+
content: { type: 'string', required: true, description: 'The prompt body. 提示词正文.' },
|
|
344
|
+
dimension: { type: 'string', description: '维度(前端/后端/…).' },
|
|
345
|
+
difficulty: { type: 'string', description: '难度(L1-L5/LX).' },
|
|
346
|
+
tags: { type: 'array', items: { type: 'string' }, description: '检索用标签.' },
|
|
347
|
+
fallback: { type: 'string', description: '可选备用提示词.' },
|
|
348
|
+
namespace: { type: 'string', enum: ['global', 'project'], description: '默认 global.' },
|
|
349
|
+
},
|
|
350
|
+
output: {
|
|
351
|
+
schema: PROMPT_RECORD_SCHEMA,
|
|
352
|
+
render: (_args, value) => renderJson(value),
|
|
353
|
+
},
|
|
354
|
+
execute(args, exec) {
|
|
355
|
+
return memory.promptAdd({
|
|
356
|
+
name: args.name,
|
|
357
|
+
content: args.content,
|
|
358
|
+
...args.dimension === undefined ? {} : { dimension: args.dimension },
|
|
359
|
+
...args.difficulty === undefined ? {} : { difficulty: args.difficulty },
|
|
360
|
+
...args.tags === undefined ? {} : { tags: args.tags },
|
|
361
|
+
...args.fallback === undefined ? {} : { fallback: args.fallback },
|
|
362
|
+
...args.namespace === undefined ? {} : { namespace: args.namespace },
|
|
363
|
+
source: 'agent',
|
|
364
|
+
}, execProjectCwd(exec)).then(promptValue);
|
|
365
|
+
},
|
|
366
|
+
presentCall: args => present('Add prompt template', 'other', args.name),
|
|
367
|
+
}));
|
|
208
368
|
ctx.tools.register(defineTool({
|
|
209
369
|
name: 'memory_forget',
|
|
210
370
|
description: 'Delete one stored memory by id. The human owner may remove any memory.',
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-files.ts — 提示词模板 md 文件解析与目录扫描(纯 Node,零依赖可测)。
|
|
3
|
+
*
|
|
4
|
+
* 文件是模板的唯一事实源(设计文档 §2):用户可直接放入/编辑/删除 md,
|
|
5
|
+
* 索引层按 mtime 惰性刷新。格式:
|
|
6
|
+
*
|
|
7
|
+
* ```md
|
|
8
|
+
* ---
|
|
9
|
+
* seq: 36
|
|
10
|
+
* name: "样例" # 必填;缺 name 的文件跳过(坏文件不崩不索引)
|
|
11
|
+
* dimension: "前端"
|
|
12
|
+
* difficulty: "L3"
|
|
13
|
+
* tags: ["Three.js"]
|
|
14
|
+
* source: user # user | agent(模型新增打 agent 角标)
|
|
15
|
+
* createdAt: 2026-08-26
|
|
16
|
+
* ---
|
|
17
|
+
* (主提示词全文)
|
|
18
|
+
* ---
|
|
19
|
+
* (备用提示词,可选)
|
|
20
|
+
* ```
|
|
21
|
+
* 解析失败/缺 name 的文件记 warnings,不影响整体扫描。
|
|
22
|
+
*/
|
|
23
|
+
export interface PromptMeta {
|
|
24
|
+
seq?: number;
|
|
25
|
+
name: string;
|
|
26
|
+
dimension?: string;
|
|
27
|
+
difficulty?: string;
|
|
28
|
+
tags: string[];
|
|
29
|
+
source: 'user' | 'agent';
|
|
30
|
+
createdAt?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface PromptFile {
|
|
33
|
+
path: string;
|
|
34
|
+
mtime: number;
|
|
35
|
+
meta: PromptMeta;
|
|
36
|
+
/** 主提示词全文。 */
|
|
37
|
+
body: string;
|
|
38
|
+
/** 备用提示词(可选;md 中以独立 `---` 行与正文分隔)。 */
|
|
39
|
+
fallback: string | null;
|
|
40
|
+
/** 索引摘要(正文前 200 字,替换换行)。 */
|
|
41
|
+
summary: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 解析一份 md(frontmatter + 正文 + 可选备用段)。纯文本解析,不碰文件系统;
|
|
45
|
+
* mtime 由调用方传入(scan 从 stat 取;纯解析测试传 0)。
|
|
46
|
+
* @returns ok:true 带结构化结果;ok:false 带人读错误(跳过不索引)。
|
|
47
|
+
*/
|
|
48
|
+
export declare function parsePromptFile(path: string, text: string, mtime?: number): {
|
|
49
|
+
ok: true;
|
|
50
|
+
file: PromptFile;
|
|
51
|
+
} | {
|
|
52
|
+
ok: false;
|
|
53
|
+
error: string;
|
|
54
|
+
};
|
|
55
|
+
/** 扫描一个目录的全部 *.md(目录不存在 = 空)。坏文件收集 warnings。 */
|
|
56
|
+
export declare function scanPromptDir(dir: string): {
|
|
57
|
+
files: PromptFile[];
|
|
58
|
+
warnings: string[];
|
|
59
|
+
};
|
|
60
|
+
/** 文件名安全化(Windows 保留字符替换;保留中文与常用符号)。 */
|
|
61
|
+
export declare function sanitizeFileName(name: string): string;
|
|
62
|
+
/** 下一个可用序号(现有文件 `\d+_` 前缀 + 1;目录空 = 1)。 */
|
|
63
|
+
export declare function nextSeq(dir: string): number;
|
|
64
|
+
export interface PromptWriteInput {
|
|
65
|
+
name: string;
|
|
66
|
+
dimension?: string;
|
|
67
|
+
difficulty?: string;
|
|
68
|
+
tags?: string[];
|
|
69
|
+
content: string;
|
|
70
|
+
fallback?: string;
|
|
71
|
+
source: 'user' | 'agent';
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 写一份模板 md(目录不存在自动创建;序号自动分配;同名防追加后缀)。
|
|
75
|
+
* @returns 写入路径。
|
|
76
|
+
*/
|
|
77
|
+
export declare function writePromptFile(dir: string, input: PromptWriteInput): string;
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-files.ts — 提示词模板 md 文件解析与目录扫描(纯 Node,零依赖可测)。
|
|
3
|
+
*
|
|
4
|
+
* 文件是模板的唯一事实源(设计文档 §2):用户可直接放入/编辑/删除 md,
|
|
5
|
+
* 索引层按 mtime 惰性刷新。格式:
|
|
6
|
+
*
|
|
7
|
+
* ```md
|
|
8
|
+
* ---
|
|
9
|
+
* seq: 36
|
|
10
|
+
* name: "样例" # 必填;缺 name 的文件跳过(坏文件不崩不索引)
|
|
11
|
+
* dimension: "前端"
|
|
12
|
+
* difficulty: "L3"
|
|
13
|
+
* tags: ["Three.js"]
|
|
14
|
+
* source: user # user | agent(模型新增打 agent 角标)
|
|
15
|
+
* createdAt: 2026-08-26
|
|
16
|
+
* ---
|
|
17
|
+
* (主提示词全文)
|
|
18
|
+
* ---
|
|
19
|
+
* (备用提示词,可选)
|
|
20
|
+
* ```
|
|
21
|
+
* 解析失败/缺 name 的文件记 warnings,不影响整体扫描。
|
|
22
|
+
*/
|
|
23
|
+
import { readdirSync, readFileSync, statSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
const SUMMARY_CHARS = 200;
|
|
26
|
+
/** 解析简单 frontmatter 值:字符串(含引号)/数字/布尔/数组。 */
|
|
27
|
+
function parseFrontmatterValue(raw) {
|
|
28
|
+
const value = raw.trim();
|
|
29
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
30
|
+
return value
|
|
31
|
+
.slice(1, -1).split(',')
|
|
32
|
+
.map(part => part.trim().replace(/^["']|["']$/g, ''))
|
|
33
|
+
.filter(part => part !== '');
|
|
34
|
+
}
|
|
35
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
36
|
+
return value.slice(1, -1);
|
|
37
|
+
if (value.startsWith("'") && value.endsWith("'"))
|
|
38
|
+
return value.slice(1, -1);
|
|
39
|
+
if (/^\d+$/.test(value))
|
|
40
|
+
return Number(value);
|
|
41
|
+
if (value === 'true')
|
|
42
|
+
return true;
|
|
43
|
+
if (value === 'false')
|
|
44
|
+
return false;
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 解析一份 md(frontmatter + 正文 + 可选备用段)。纯文本解析,不碰文件系统;
|
|
49
|
+
* mtime 由调用方传入(scan 从 stat 取;纯解析测试传 0)。
|
|
50
|
+
* @returns ok:true 带结构化结果;ok:false 带人读错误(跳过不索引)。
|
|
51
|
+
*/
|
|
52
|
+
export function parsePromptFile(path, text, mtime = 0) {
|
|
53
|
+
const lines = text.split(/\r?\n/);
|
|
54
|
+
if (lines[0]?.trim() !== '---') {
|
|
55
|
+
return { ok: false, error: 'missing frontmatter fence (first line must be ---)' };
|
|
56
|
+
}
|
|
57
|
+
let fence = -1;
|
|
58
|
+
for (let i = 1; i < lines.length; i++) {
|
|
59
|
+
if (lines[i].trim() === '---') {
|
|
60
|
+
fence = i;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (fence === -1)
|
|
65
|
+
return { ok: false, error: 'unclosed frontmatter fence' };
|
|
66
|
+
const meta = {};
|
|
67
|
+
for (const line of lines.slice(1, fence)) {
|
|
68
|
+
const m = /^([A-Za-z][A-Za-z0-9]*)\s*:\s*(.*)$/.exec(line);
|
|
69
|
+
if (m === null || m[2] === undefined)
|
|
70
|
+
continue;
|
|
71
|
+
meta[m[1]] = parseFrontmatterValue(m[2]);
|
|
72
|
+
}
|
|
73
|
+
const name = typeof meta.name === 'string' ? meta.name : '';
|
|
74
|
+
if (name === '')
|
|
75
|
+
return { ok: false, error: 'missing required frontmatter field "name"' };
|
|
76
|
+
const source = meta.source === 'agent' ? 'agent' : 'user';
|
|
77
|
+
// 正文:frontmatter 后到第一个独立 `---` 行为止(剩余为备用提示词)
|
|
78
|
+
const bodyLines = [];
|
|
79
|
+
const fallbackLines = [];
|
|
80
|
+
let inFallback = false;
|
|
81
|
+
for (const line of lines.slice(fence + 1)) {
|
|
82
|
+
if (!inFallback && line.trim() === '---') {
|
|
83
|
+
inFallback = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
;
|
|
87
|
+
(inFallback ? fallbackLines : bodyLines).push(line);
|
|
88
|
+
}
|
|
89
|
+
const body = bodyLines.join('\n').trim();
|
|
90
|
+
if (body === '')
|
|
91
|
+
return { ok: false, error: 'empty prompt body' };
|
|
92
|
+
// 备用段首行常为「备用提示词2」之类的标题行,跳过(约定:正文与备用用 --- 分隔)
|
|
93
|
+
const fallbackRaw = fallbackLines.join('\n').trim();
|
|
94
|
+
const fallback = fallbackRaw === '' ? null : fallbackRaw.replace(/^\s*(备用提示词\d*|fallback\d*)\s*\n?/i, '');
|
|
95
|
+
const file = {
|
|
96
|
+
path,
|
|
97
|
+
mtime,
|
|
98
|
+
meta: {
|
|
99
|
+
seq: typeof meta.seq === 'number' ? meta.seq : undefined,
|
|
100
|
+
name,
|
|
101
|
+
dimension: typeof meta.dimension === 'string' ? meta.dimension : undefined,
|
|
102
|
+
difficulty: typeof meta.difficulty === 'string' ? meta.difficulty : undefined,
|
|
103
|
+
tags: Array.isArray(meta.tags) ? meta.tags.filter((tag) => typeof tag === 'string') : [],
|
|
104
|
+
source,
|
|
105
|
+
createdAt: typeof meta.createdAt === 'number' ? meta.createdAt : undefined,
|
|
106
|
+
},
|
|
107
|
+
body,
|
|
108
|
+
fallback,
|
|
109
|
+
summary: body.replace(/\s+/g, ' ').trim().slice(0, SUMMARY_CHARS),
|
|
110
|
+
};
|
|
111
|
+
return { ok: true, file };
|
|
112
|
+
}
|
|
113
|
+
/** 扫描一个目录的全部 *.md(目录不存在 = 空)。坏文件收集 warnings。 */
|
|
114
|
+
export function scanPromptDir(dir) {
|
|
115
|
+
const files = [];
|
|
116
|
+
const warnings = [];
|
|
117
|
+
let entries = [];
|
|
118
|
+
try {
|
|
119
|
+
entries = readdirSync(dir);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return { files, warnings }; // 目录不存在:空库
|
|
123
|
+
}
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (!entry.toLowerCase().endsWith('.md'))
|
|
126
|
+
continue;
|
|
127
|
+
const path = join(dir, entry);
|
|
128
|
+
let text;
|
|
129
|
+
try {
|
|
130
|
+
text = readFileSync(path, 'utf8');
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
warnings.push(`${path}: read failed (${error instanceof Error ? error.message : String(error)})`);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const parsed = parsePromptFile(path, text, statSync(path).mtimeMs);
|
|
137
|
+
if (!parsed.ok) {
|
|
138
|
+
warnings.push(`${path}: ${parsed.error}`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
files.push(parsed.file);
|
|
142
|
+
}
|
|
143
|
+
return { files, warnings };
|
|
144
|
+
}
|
|
145
|
+
/** 文件名安全化(Windows 保留字符替换;保留中文与常用符号)。 */
|
|
146
|
+
export function sanitizeFileName(name) {
|
|
147
|
+
const safe = name.replace(/[<>:"/\\|?*\u0000-\u001F]/g, '_').trim();
|
|
148
|
+
return (safe === '' ? 'untitled' : safe).slice(0, 80);
|
|
149
|
+
}
|
|
150
|
+
/** 下一个可用序号(现有文件 `\d+_` 前缀 + 1;目录空 = 1)。 */
|
|
151
|
+
export function nextSeq(dir) {
|
|
152
|
+
let max = 0;
|
|
153
|
+
try {
|
|
154
|
+
for (const entry of readdirSync(dir)) {
|
|
155
|
+
const m = /^(\d+)_/.exec(entry);
|
|
156
|
+
if (m !== null)
|
|
157
|
+
max = Math.max(max, Number(m[1]));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
catch { /* 目录不存在:从 1 开始 */ }
|
|
161
|
+
return max + 1;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 写一份模板 md(目录不存在自动创建;序号自动分配;同名防追加后缀)。
|
|
165
|
+
* @returns 写入路径。
|
|
166
|
+
*/
|
|
167
|
+
export function writePromptFile(dir, input) {
|
|
168
|
+
mkdirSync(dir, { recursive: true });
|
|
169
|
+
const seq = nextSeq(dir);
|
|
170
|
+
const base = sanitizeFileName(input.name);
|
|
171
|
+
let path = join(dir, `${seq}_${base}.md`);
|
|
172
|
+
let attempt = 0;
|
|
173
|
+
while (existsSync(path)) {
|
|
174
|
+
attempt += 1;
|
|
175
|
+
path = join(dir, `${seq}_${base}_${attempt}.md`);
|
|
176
|
+
}
|
|
177
|
+
const lines = ['---', `seq: ${seq}`, `name: ${JSON.stringify(input.name)}`, `source: ${input.source}`];
|
|
178
|
+
if (input.dimension !== undefined && input.dimension !== '')
|
|
179
|
+
lines.push(`dimension: ${JSON.stringify(input.dimension)}`);
|
|
180
|
+
if (input.difficulty !== undefined && input.difficulty !== '')
|
|
181
|
+
lines.push(`difficulty: ${JSON.stringify(input.difficulty)}`);
|
|
182
|
+
if (input.tags !== undefined && input.tags.length > 0)
|
|
183
|
+
lines.push(`tags: [${input.tags.map(tag => JSON.stringify(tag)).join(', ')}]`);
|
|
184
|
+
lines.push(`createdAt: ${new Date().toISOString().slice(0, 10)}`, '---');
|
|
185
|
+
const text = `${lines.join('\n')}\n\n${input.content.trim()}\n${input.fallback === undefined || input.fallback === '' ? '' : `\n---\n\n${input.fallback.trim()}\n`}`;
|
|
186
|
+
writeFileSync(path, text, 'utf8');
|
|
187
|
+
return path;
|
|
188
|
+
}
|
package/dist/routes.js
CHANGED
|
@@ -98,6 +98,27 @@ function cwdOf(payload) {
|
|
|
98
98
|
const value = payload?.cwd;
|
|
99
99
|
return typeof value === 'string' && value !== '' ? value : undefined;
|
|
100
100
|
}
|
|
101
|
+
/** 取 memory 服务(未挂载 503 且明确失败)。 */
|
|
102
|
+
function requireMemory(ctx) {
|
|
103
|
+
const memory = ctx.get('memory');
|
|
104
|
+
if (memory === undefined)
|
|
105
|
+
throw new MemoryApiError('service-unavailable', 'memory service unavailable', 503);
|
|
106
|
+
return memory;
|
|
107
|
+
}
|
|
108
|
+
/** 模板记录投影(0.6.0):不暴露本地文件路径(meta.path 引擎内部使用)。 */
|
|
109
|
+
function promptProjection(record) {
|
|
110
|
+
const meta = record.meta;
|
|
111
|
+
return {
|
|
112
|
+
id: String(record.id),
|
|
113
|
+
name: meta?.name ?? '',
|
|
114
|
+
...meta?.dimension === undefined ? {} : { dimension: meta.dimension },
|
|
115
|
+
...meta?.difficulty === undefined ? {} : { difficulty: meta.difficulty },
|
|
116
|
+
...meta === undefined ? {} : { tags: meta.tags },
|
|
117
|
+
summary: meta?.summary ?? record.content.slice(0, 200),
|
|
118
|
+
source: meta?.source ?? 'user',
|
|
119
|
+
namespace: record.namespace,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
101
122
|
/** 挂载 /memory/api 路由(apply 调用)。 */
|
|
102
123
|
export function mountMemoryApi(ctx) {
|
|
103
124
|
// webServer/webRuntime 由 index.ts 的 inject 声明(Context 类型未扩展,断言访问)
|
|
@@ -184,6 +205,63 @@ export function mountMemoryApi(ctx) {
|
|
|
184
205
|
const summaryChars = typeof record?.summaryChars === 'number' && record.summaryChars > 0 ? record.summaryChars : DEFAULT_SUMMARY_CHARS;
|
|
185
206
|
return { self: SELF_DESCRIPTION, ...renderInjection(memory.recallRecords(cwd), budget, summaryChars) };
|
|
186
207
|
},
|
|
208
|
+
// ── 提示词模板库(0.6.0)──
|
|
209
|
+
'prompt.list': async (payload) => {
|
|
210
|
+
const memory = requireMemory(ctx);
|
|
211
|
+
const record = (payload ?? {});
|
|
212
|
+
const cwd = cwdOf(record);
|
|
213
|
+
await memory.refreshPromptIndex(cwd);
|
|
214
|
+
const namespace = record.namespace === 'project' || record.namespace === 'global' ? record.namespace : undefined;
|
|
215
|
+
const records = await memory.list({ kind: 'prompt', ...namespace === undefined ? {} : { namespace } }, cwd);
|
|
216
|
+
return records.map(promptProjection);
|
|
217
|
+
},
|
|
218
|
+
'prompt.get': (payload) => {
|
|
219
|
+
const memory = requireMemory(ctx);
|
|
220
|
+
const record = (payload ?? {});
|
|
221
|
+
const lookup = typeof record.name === 'string' ? record.name : typeof record.id === 'string' ? record.id : '';
|
|
222
|
+
if (lookup === '') {
|
|
223
|
+
throw new MemoryApiError('bad-request', 'missing or invalid "id"/"name"');
|
|
224
|
+
}
|
|
225
|
+
return memory.promptGet(lookup, cwdOf(record)).then(file => ({
|
|
226
|
+
name: file.meta.name,
|
|
227
|
+
...file.meta.dimension === undefined ? {} : { dimension: file.meta.dimension },
|
|
228
|
+
...file.meta.difficulty === undefined ? {} : { difficulty: file.meta.difficulty },
|
|
229
|
+
tags: file.meta.tags,
|
|
230
|
+
body: file.body,
|
|
231
|
+
...file.fallback === null ? {} : { fallback: file.fallback },
|
|
232
|
+
}));
|
|
233
|
+
},
|
|
234
|
+
'prompt.add': (payload) => {
|
|
235
|
+
const memory = requireMemory(ctx);
|
|
236
|
+
const record = (payload ?? {});
|
|
237
|
+
if (typeof record.name !== 'string' || record.name === '')
|
|
238
|
+
throw new MemoryApiError('bad-request', 'missing or invalid "name"');
|
|
239
|
+
if (typeof record.content !== 'string' || record.content === '')
|
|
240
|
+
throw new MemoryApiError('bad-request', 'missing or invalid "content"');
|
|
241
|
+
return memory.promptAdd({
|
|
242
|
+
name: record.name,
|
|
243
|
+
content: record.content,
|
|
244
|
+
...typeof record.dimension === 'string' ? { dimension: record.dimension } : {},
|
|
245
|
+
...typeof record.difficulty === 'string' ? { difficulty: record.difficulty } : {},
|
|
246
|
+
...Array.isArray(record.tags) ? { tags: record.tags.filter((tag) => typeof tag === 'string') } : {},
|
|
247
|
+
...typeof record.fallback === 'string' ? { fallback: record.fallback } : {},
|
|
248
|
+
...record.namespace === 'project' ? { namespace: 'project' } : {},
|
|
249
|
+
source: 'user',
|
|
250
|
+
}, cwdOf(record)).then(promptProjection);
|
|
251
|
+
},
|
|
252
|
+
'prompt.remove': (payload) => {
|
|
253
|
+
const memory = requireMemory(ctx);
|
|
254
|
+
const record = (payload ?? {});
|
|
255
|
+
const lookup = typeof record.name === 'string' ? record.name : typeof record.id === 'string' ? record.id : '';
|
|
256
|
+
if (lookup === '') {
|
|
257
|
+
throw new MemoryApiError('bad-request', 'missing or invalid "id"/"name"');
|
|
258
|
+
}
|
|
259
|
+
return memory.promptRemove(lookup, cwdOf(record));
|
|
260
|
+
},
|
|
261
|
+
'prompt.refresh': (payload) => {
|
|
262
|
+
const memory = requireMemory(ctx);
|
|
263
|
+
return memory.refreshPromptIndex(cwdOf((payload ?? {})));
|
|
264
|
+
},
|
|
187
265
|
};
|
|
188
266
|
const fence = (req) => isTrusted(req, webRuntime.trustedHosts);
|
|
189
267
|
ctx.effect(() => webServer.register({
|
package/package.json
CHANGED