dsh-plugin-tool-management 0.17.0 → 0.18.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/CHANGELOG.md +39 -0
- package/README.md +18 -3
- package/README_EN.md +18 -3
- package/docs/images/1-EN.png +0 -0
- package/docs/images/1.png +0 -0
- package/docs/images/2-EN.png +0 -0
- package/docs/images/2.png +0 -0
- package/docs/images/3-EN.png +0 -0
- package/docs/images/3.png +0 -0
- package/docs/images/4-EN.png +0 -0
- package/docs/images/4.png +0 -0
- package/docs/images/5-EN.png +0 -0
- package/docs/images/5.png +0 -0
- package/docs/images/6-EN.png +0 -0
- package/docs/images/6.png +0 -0
- package/docs/images/7-EN.png +0 -0
- package/docs/images/7.png +0 -0
- package/docs/images/8-EN.png +0 -0
- package/docs/images/8.png +0 -0
- package/docs/update.md +16 -0
- package/lib/client.js +1573 -133
- package/lib/compat/capability-specs.js +321 -0
- package/lib/compat/host-probe.js +261 -0
- package/lib/compat/host-roots.js +168 -0
- package/lib/compat/probe-types.js +6 -0
- package/lib/compat/probe.js +9 -720
- package/lib/context-inject-contract.js +114 -0
- package/lib/context-inject-frame.js +147 -0
- package/lib/context-inject-select.js +127 -0
- package/lib/context-inject-surface.js +97 -0
- package/lib/context-inject.js +10 -478
- package/lib/http-api.js +229 -0
- package/lib/hub.js +2 -0
- package/lib/index.js +134 -836
- package/lib/mcp/inspect-probe.js +199 -0
- package/lib/mcp/manager.js +30 -548
- package/lib/mcp/tool-visibility.js +388 -0
- package/lib/memories/archive-engine.js +142 -5
- package/lib/memories/archive.js +10 -1
- package/lib/memories/discovery.js +261 -0
- package/lib/memories/export-plan.js +42 -0
- package/lib/memories/memories-types.js +1 -0
- package/lib/memories/service.js +4 -38
- package/lib/memories/snapshot.js +10 -252
- package/lib/op-registry.js +21 -0
- package/lib/ops/compat.js +11 -0
- package/lib/ops/quick-prompts.js +26 -0
- package/lib/ops/scene-records.js +40 -0
- package/lib/ops/scene-sync.js +71 -1
- package/lib/ops/sessions.js +13 -9
- package/lib/ops/snapshot.js +18 -12
- package/lib/prompts/quick-service.js +211 -0
- package/lib/scene-linkage.js +184 -0
- package/lib/sessions/archive-schemas.js +121 -0
- package/lib/sessions/path-key.js +46 -0
- package/lib/sessions/workspace.js +5 -163
- package/lib/settings-sidecars.js +143 -0
- package/lib/skills/core-types.js +8 -0
- package/lib/skills/core-util.js +81 -0
- package/lib/skills/core.js +56 -2326
- package/lib/skills/dir-watch.js +71 -0
- package/lib/skills/frontmatter.js +96 -0
- package/lib/skills/homes.js +14 -0
- package/lib/skills/import-pipeline.js +231 -0
- package/lib/skills/manager-state.js +800 -0
- package/lib/skills/official.js +269 -0
- package/lib/skills/provider-registration.js +89 -0
- package/lib/skills/service.js +5 -240
- package/lib/skills/skill-roots.js +287 -0
- package/lib/skills/skill-trash.js +265 -0
- package/lib/skills/system-trash.js +97 -0
- package/lib/skills/upload-pipeline.js +374 -0
- package/lib/slash-settings.js +25 -0
- package/lib/subagents/persona-parse.js +312 -0
- package/lib/subagents/persona-types.js +5 -0
- package/lib/subagents/persona-write.js +93 -0
- package/lib/subagents/run-provider.js +159 -0
- package/lib/subagents/service.js +14 -638
- package/lib/subagents/tool-filter.js +98 -0
- package/lib/token-config.js +240 -0
- package/lib/tool-table-switch.js +163 -0
- package/lib/tools/scene.js +40 -9
- package/package.json +3 -2
package/lib/memories/archive.js
CHANGED
|
@@ -64,6 +64,9 @@ export function normalizeArchive(raw) {
|
|
|
64
64
|
out.skills = normalizeStringList(obj.skills);
|
|
65
65
|
if (obj.subagents != null)
|
|
66
66
|
out.subagents = normalizeStringList(obj.subagents);
|
|
67
|
+
// 快捷提示词:`!= null` 才算定义(空表 = 定义了但全不勾 = 进入后全部停用,必须留住)。
|
|
68
|
+
if (obj.quickPrompts != null)
|
|
69
|
+
out.quickPrompts = normalizeStringList(obj.quickPrompts);
|
|
67
70
|
if (obj.memories != null)
|
|
68
71
|
out.memories = normalizeStringList(obj.memories);
|
|
69
72
|
// 工具表方案是**标量**不是集合:空串 / 非字符串一律视为未绑定(段不存在)。
|
|
@@ -208,7 +211,12 @@ mcpNotes = [],
|
|
|
208
211
|
*/
|
|
209
212
|
subagentStates = null,
|
|
210
213
|
/** 进场景前的模型工具表关停名单(null = 这次不进快照,退出也就不动工具表)。 */
|
|
211
|
-
toolTableHidden = null
|
|
214
|
+
toolTableHidden = null,
|
|
215
|
+
/**
|
|
216
|
+
* 进场景前**每一条**快捷提示词的开关(null = 档案没定义 quickPrompts 段 → 这一域不碰,
|
|
217
|
+
* 也就不进快照,退出同样一个字不动)。
|
|
218
|
+
*/
|
|
219
|
+
quickPromptStates = null) {
|
|
212
220
|
return {
|
|
213
221
|
mcp: Object.fromEntries(Object.entries(mcpRaw).map(([k, v]) => [k, v.slice()])),
|
|
214
222
|
skills: { ...skills },
|
|
@@ -219,6 +227,7 @@ toolTableHidden = null) {
|
|
|
219
227
|
...(subagentStates ? { subagentsAll: { ...subagentStates } } : {}),
|
|
220
228
|
...(mcpNotes.length ? { mcpNotes: mcpNotes.map((x) => ({ id: x.id, note: x.note })) } : {}),
|
|
221
229
|
...(toolTableHidden ? { toolTableHidden: toolTableHidden.slice() } : {}),
|
|
230
|
+
...(quickPromptStates ? { quickPromptsAll: { ...quickPromptStates } } : {}),
|
|
222
231
|
};
|
|
223
232
|
}
|
|
224
233
|
// ── 记忆勾选(v3)──────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
// 规则/记忆的**发现**:扫一个来源目录读成条目、把 frontmatter 投成规则、按老布局搬家。
|
|
2
|
+
//
|
|
3
|
+
// 声明从 memories/snapshot.ts 按行号区间搬来,正文一字未改(唯一的容差是
|
|
4
|
+
// `--export-top-level` 加的 `export ` 前缀,moved-verify 逐字对拍)。
|
|
5
|
+
//
|
|
6
|
+
// 为什么与快照分两层:发现是「读外面进来的东西」(官方技能目录、AGENTS.md、旧布局),
|
|
7
|
+
// 快照是「记下当前场景长什么样并写回索引」。两者各自演化的方向不同 —— 前者跟着**来源形状**
|
|
8
|
+
// 变,后者跟着**索引字段**变。
|
|
9
|
+
import { statSync } from 'node:fs';
|
|
10
|
+
import { cp, lstat, mkdir, readdir, realpath, rename, rm } from 'node:fs/promises';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { resolveDshHome, unquote } from '../skills/core.js';
|
|
13
|
+
import { MAX_SOURCE_DEPTH, MAX_DIRECTORIES, MAX_ENTRIES, MAX_DESCRIPTION_LENGTH, DEFAULT_ORDER, GLOBAL_SCENE, LEGACY_BUNDLE_DOC, bundleDocName, SEGMENT_RULE_HINT, isValidGroupSegment } from './constants.js';
|
|
14
|
+
import { pathExists } from './index-io.js';
|
|
15
|
+
export const identity = (p) => (process.platform === 'win32' ? p.toLowerCase() : p);
|
|
16
|
+
export async function discover(memoriesRoot) {
|
|
17
|
+
const entries = new Map();
|
|
18
|
+
const shadowed = [];
|
|
19
|
+
const groups = new Set();
|
|
20
|
+
const scenes = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
const rootPath = resolve(memoriesRoot);
|
|
23
|
+
try {
|
|
24
|
+
const st = await lstat(rootPath);
|
|
25
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
26
|
+
return { entries, shadowed, groups, scenes, warnings, truncated: false };
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return { entries, shadowed, groups, scenes, warnings, truncated: false };
|
|
30
|
+
}
|
|
31
|
+
const queue = [
|
|
32
|
+
{ path: rootPath, group: '', depth: 0, ancestors: new Set() },
|
|
33
|
+
];
|
|
34
|
+
let directories = 0;
|
|
35
|
+
let itemCount = 0;
|
|
36
|
+
let truncated = false;
|
|
37
|
+
const addEntry = (entry) => {
|
|
38
|
+
const existing = entries.get(entry.id);
|
|
39
|
+
if (!existing) {
|
|
40
|
+
entries.set(entry.id, entry);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (entry.kind === 'bundle' && existing.kind === 'flat') {
|
|
44
|
+
shadowed.push({ ...existing, shadowed: true });
|
|
45
|
+
entries.set(entry.id, entry);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (entry.kind === 'flat' && existing.kind === 'bundle') {
|
|
49
|
+
shadowed.push({ ...entry, shadowed: true });
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
for (let cursor = 0; cursor < queue.length; cursor++) {
|
|
53
|
+
if (directories >= MAX_DIRECTORIES || itemCount >= MAX_ENTRIES) {
|
|
54
|
+
truncated = true;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
const current = queue[cursor];
|
|
58
|
+
let realDirectory;
|
|
59
|
+
let dirents = [];
|
|
60
|
+
try {
|
|
61
|
+
realDirectory = await realpath(current.path);
|
|
62
|
+
if (current.ancestors.has(identity(realDirectory)))
|
|
63
|
+
continue;
|
|
64
|
+
const dir = await readdir(current.path, { withFileTypes: true });
|
|
65
|
+
directories++;
|
|
66
|
+
for (const item of dir) {
|
|
67
|
+
if (itemCount >= MAX_ENTRIES) {
|
|
68
|
+
truncated = true;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
itemCount++;
|
|
72
|
+
dirents.push(item);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue; // 失效链接或不可读目录不阻断其他来源
|
|
77
|
+
}
|
|
78
|
+
// bundle 检查:当前目录含 `<目录名>.md`(或旧版 `SKILL.md`)→ 它是规则叶子,父路径为场景。
|
|
79
|
+
//
|
|
80
|
+
// **根下的一级目录恒为场景**(`parentGroup === ''` 时不做 bundle 判断)——与
|
|
81
|
+
// `probeSceneFilesSync`(注入段)同口径。场景 `1` 里一条叫 `1` 的记忆(`memories/1/1.md`)
|
|
82
|
+
// 与「根层的 bundle `1`」在磁盘上同形,认成后者会把整个场景目录吃成叶子:同场景其余
|
|
83
|
+
// 记忆全从列表消失、多出一条「未归属场景」的同名记忆,新建记忆还会被误报「同名 bundle
|
|
84
|
+
// 遮蔽」(2026-09-16 用户实测)。根层因此没有 bundle,只有 legacy 裸 .md(见 rulesCheck 的 noScene)。
|
|
85
|
+
if (current.group !== '') {
|
|
86
|
+
const sepIdx = current.group.lastIndexOf('/');
|
|
87
|
+
const parentGroup = sepIdx >= 0 ? current.group.slice(0, sepIdx) : '';
|
|
88
|
+
const name = sepIdx >= 0 ? current.group.slice(sepIdx + 1) : current.group;
|
|
89
|
+
let docPath = '';
|
|
90
|
+
if (parentGroup !== '') {
|
|
91
|
+
for (const candidate of [bundleDocName(name), LEGACY_BUNDLE_DOC]) {
|
|
92
|
+
const p = join(current.path, candidate);
|
|
93
|
+
try {
|
|
94
|
+
const st = await lstat(p);
|
|
95
|
+
if (st.isFile() && !st.isSymbolicLink()) {
|
|
96
|
+
docPath = p;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch { /* 该候选名不存在 → 试下一个 */ }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (docPath !== '') {
|
|
104
|
+
const id = parentGroup ? `${parentGroup}/${name}` : name;
|
|
105
|
+
addEntry({ id, group: parentGroup, name, kind: 'bundle', docPath, entryPath: current.path });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (current.group !== '') {
|
|
110
|
+
groups.add(current.group);
|
|
111
|
+
// 一级目录 = 场景名(多段场景名用 '/' 连接,见 isValidGroupPath)。
|
|
112
|
+
if (current.depth === 1)
|
|
113
|
+
scenes.push(current.group);
|
|
114
|
+
}
|
|
115
|
+
dirents.sort((a, b) => a.name.localeCompare(b.name));
|
|
116
|
+
const ancestors = new Set(current.ancestors).add(identity(realDirectory));
|
|
117
|
+
for (const item of dirents) {
|
|
118
|
+
if (item.name.startsWith('.'))
|
|
119
|
+
continue; // 隐藏目录/文件跳过
|
|
120
|
+
const path = join(current.path, item.name);
|
|
121
|
+
try {
|
|
122
|
+
const st = await lstat(path);
|
|
123
|
+
// 符号链接一律不跟随:注入路径(`probeSceneFilesSync`)本来就不跟随,两端必须同口径 ——
|
|
124
|
+
// 否则列表里会出现**永远注入不进去**的记忆(用户打开开关却什么也没发生)。顺带也关掉了
|
|
125
|
+
// 「链接指向根外、写操作顺着链接穿透出去」这条路(写入侧另有 realpath 断言兜底)。
|
|
126
|
+
if (st.isSymbolicLink())
|
|
127
|
+
continue;
|
|
128
|
+
if (st.isDirectory()) {
|
|
129
|
+
if (!isValidGroupSegment(item.name)) {
|
|
130
|
+
warnings.push(`跳过非法目录名(${SEGMENT_RULE_HINT}):${current.group ? current.group + '/' : ''}${item.name}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (current.depth >= MAX_SOURCE_DEPTH || queue.length >= MAX_DIRECTORIES) {
|
|
134
|
+
truncated = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
queue.push({
|
|
138
|
+
path,
|
|
139
|
+
group: current.group ? `${current.group}/${item.name}` : item.name,
|
|
140
|
+
depth: current.depth + 1,
|
|
141
|
+
ancestors,
|
|
142
|
+
});
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!st.isFile())
|
|
146
|
+
continue;
|
|
147
|
+
if (!item.name.toLowerCase().endsWith('.md'))
|
|
148
|
+
continue; // 只认 .md
|
|
149
|
+
const name = item.name.slice(0, -3);
|
|
150
|
+
const id = current.group ? `${current.group}/${name}` : name;
|
|
151
|
+
addEntry({ id, group: current.group, name, kind: 'flat', docPath: path, entryPath: path });
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
/* 来源在扫描期间失效时跳过 */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { entries, shadowed, groups, scenes, warnings, truncated };
|
|
159
|
+
}
|
|
160
|
+
export function deriveDescription(body) {
|
|
161
|
+
const text = String(body ?? '').trim();
|
|
162
|
+
if (!text)
|
|
163
|
+
return '';
|
|
164
|
+
const heading = /^#\s+(.+)$/m.exec(text);
|
|
165
|
+
const raw = heading && heading[1].trim() ? heading[1].trim() : text.split(/\r?\n/, 1)[0].trim();
|
|
166
|
+
return raw.slice(0, MAX_DESCRIPTION_LENGTH);
|
|
167
|
+
}
|
|
168
|
+
export function deriveFromDoc(entry, doc) {
|
|
169
|
+
const declaredName = doc.map.name != null && String(doc.map.name).trim() !== '' ? unquote(String(doc.map.name)).trim() : '';
|
|
170
|
+
const name = declaredName || entry.name;
|
|
171
|
+
let description = doc.map.description != null ? unquote(String(doc.map.description)).trim() : '';
|
|
172
|
+
let descriptionDerived = false;
|
|
173
|
+
if (description === '') {
|
|
174
|
+
description = deriveDescription(doc.body);
|
|
175
|
+
descriptionDerived = true;
|
|
176
|
+
}
|
|
177
|
+
const globs = doc.map.globs != null && String(doc.map.globs).trim() !== '' ? String(doc.map.globs).trim() : undefined;
|
|
178
|
+
let whenToUse;
|
|
179
|
+
if (doc.map.whenToUse != null && String(doc.map.whenToUse).trim() !== '') {
|
|
180
|
+
whenToUse = String(doc.map.whenToUse).trim();
|
|
181
|
+
}
|
|
182
|
+
else if (globs) {
|
|
183
|
+
whenToUse = `适用路径:${globs}`;
|
|
184
|
+
}
|
|
185
|
+
const metadata = doc.map.metadata !== undefined ? doc.map.metadata : undefined;
|
|
186
|
+
return { name, description, descriptionDerived, whenToUse, globs, metadata };
|
|
187
|
+
}
|
|
188
|
+
export function projectRule(entry, derived, idxEntry) {
|
|
189
|
+
// 体积按**正文文件**算(bundle 的附件不计:附件不进注入正文)。一次 stat 一条记忆,
|
|
190
|
+
// 结果随快照的 1s TTL 缓存一起复用;读不到就不带这个字段,别把"不知道"报成 0 B。
|
|
191
|
+
let size;
|
|
192
|
+
try {
|
|
193
|
+
size = statSync(entry.docPath).size;
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
size = undefined;
|
|
197
|
+
}
|
|
198
|
+
const rule = {
|
|
199
|
+
id: entry.id,
|
|
200
|
+
group: entry.group,
|
|
201
|
+
name: derived.name,
|
|
202
|
+
form: entry.kind,
|
|
203
|
+
path: entry.docPath,
|
|
204
|
+
...(size !== undefined ? { bytes: size } : {}),
|
|
205
|
+
description: derived.description,
|
|
206
|
+
...(derived.descriptionDerived ? { descriptionDerived: true } : {}),
|
|
207
|
+
...(derived.whenToUse !== undefined ? { whenToUse: derived.whenToUse } : {}),
|
|
208
|
+
...(derived.globs !== undefined ? { globs: derived.globs } : {}),
|
|
209
|
+
...(derived.metadata !== undefined ? { metadata: derived.metadata } : {}),
|
|
210
|
+
enabled: idxEntry?.enabled ?? true,
|
|
211
|
+
order: idxEntry?.order ?? DEFAULT_ORDER,
|
|
212
|
+
tags: Array.isArray(idxEntry?.tags) ? idxEntry.tags : [],
|
|
213
|
+
pinned: idxEntry?.pinned === true,
|
|
214
|
+
note: typeof idxEntry?.note === 'string' ? idxEntry.note : '',
|
|
215
|
+
...(typeof idxEntry?.updatedAt === 'string' ? { updatedAt: idxEntry.updatedAt } : {}),
|
|
216
|
+
};
|
|
217
|
+
return rule;
|
|
218
|
+
}
|
|
219
|
+
export async function relocateLegacyLayout(memoriesRoot) {
|
|
220
|
+
const home = resolveDshHome();
|
|
221
|
+
const legacyRoots = [join(home, 'scene-memory'), join(home, 'rules')];
|
|
222
|
+
for (const legacyRoot of legacyRoots) {
|
|
223
|
+
if (!(await pathExists(legacyRoot)))
|
|
224
|
+
continue;
|
|
225
|
+
if (identity(resolve(legacyRoot)) === identity(resolve(memoriesRoot)))
|
|
226
|
+
continue; // 自定义 roots 落在旧路径 → 不自我搬移
|
|
227
|
+
let items;
|
|
228
|
+
try {
|
|
229
|
+
items = await readdir(legacyRoot, { withFileTypes: true });
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
for (const item of items) {
|
|
235
|
+
if (item.name.startsWith('.'))
|
|
236
|
+
continue;
|
|
237
|
+
const src = join(legacyRoot, item.name);
|
|
238
|
+
// 根层 .md = 旧的「全局」桶 → 搬进 `global/`,文件名保留(否则多套一层目录)。
|
|
239
|
+
// 其余条目(场景目录)整体搬进 memories/,目录名保留。
|
|
240
|
+
const isRootMd = item.isFile() && item.name.toLowerCase().endsWith('.md');
|
|
241
|
+
const destDir = isRootMd ? join(memoriesRoot, GLOBAL_SCENE) : memoriesRoot;
|
|
242
|
+
const dest = join(destDir, item.name);
|
|
243
|
+
try {
|
|
244
|
+
if (await pathExists(dest))
|
|
245
|
+
continue;
|
|
246
|
+
await mkdir(destDir, { recursive: true });
|
|
247
|
+
try {
|
|
248
|
+
await rename(src, dest);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
// 跨设备/被占用 → 复制后删源;复制失败则保留源文件(宁可重复,不可丢失)。
|
|
252
|
+
await cp(src, dest, { recursive: true });
|
|
253
|
+
await rm(src, { recursive: true, force: true });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
/* 单项失败不阻断其余项(如文件被外部程序锁住,下次启动再试) */
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// 记忆导出的落盘映射:把「导出哪几条记忆」算成 zip 里的条目与缺失清单。
|
|
2
|
+
//
|
|
3
|
+
// 从 memories/service.ts 整段搬来,一行未改。它是纯函数(不碰磁盘)——flat / bundle 两种
|
|
4
|
+
// 形态与「场景名本身可含 /」的口径都写在它自己的注释里,那条口径两边必须一致。
|
|
5
|
+
import { dirname } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* 记忆导出的落盘映射(`bundle-export` 的 `kind: 'memories'` 用,见 design-plan D14)。
|
|
8
|
+
*
|
|
9
|
+
* 记忆有两种形态(见文件头):flat = `<场景>/<name>.md`,bundle = `<场景>/<name>/<name>.md`;
|
|
10
|
+
* 而且**场景名本身可含 `/`**(多段场景名,见 `isValidGroupPath`)。所以「按 id 的最后一个
|
|
11
|
+
* `/` 切出场景与名字、再拼 `.md`」是错的:bundle 会被读成 `<场景>/<name>.md` → 读不到 →
|
|
12
|
+
* 该项目静默丢失(只在 `missing` 里留个名,界面按「导出成功」显示)。
|
|
13
|
+
*
|
|
14
|
+
* 这里一律按索引里那条规则自己的 `path` 与 `form` 决定;bundle 只交出目录,由调用方
|
|
15
|
+
* 把目录内的文件全部打包(与技能分支同口径)。索引里没有、或已被同名 bundle 遮蔽的 id
|
|
16
|
+
* 进 `missing`——遮蔽的 flat 不会被加载,导出去只会让人以为它能用。
|
|
17
|
+
*
|
|
18
|
+
* 纯函数(不碰磁盘),可直接断言。
|
|
19
|
+
*/
|
|
20
|
+
export function planMemoryExport(names, rules) {
|
|
21
|
+
const list = Array.isArray(names) ? names.map((n) => String(n).trim()).filter(Boolean) : [];
|
|
22
|
+
const byId = new Map();
|
|
23
|
+
for (const row of (Array.isArray(rules) ? rules : [])) {
|
|
24
|
+
const id = row && typeof row.id === 'string' ? row.id : '';
|
|
25
|
+
if (id !== '')
|
|
26
|
+
byId.set(id, row);
|
|
27
|
+
}
|
|
28
|
+
const entries = [];
|
|
29
|
+
const missing = [];
|
|
30
|
+
for (const id of list) {
|
|
31
|
+
const row = byId.get(id);
|
|
32
|
+
const abs = row && typeof row.path === 'string' ? row.path : '';
|
|
33
|
+
if (!row || abs === '' || row.shadowed === true) {
|
|
34
|
+
missing.push(id);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
entries.push(row.form === 'bundle'
|
|
38
|
+
? { id, zip: id, abs: dirname(abs), kind: 'dir' }
|
|
39
|
+
: { id, zip: `${id}.md`, abs, kind: 'file' });
|
|
40
|
+
}
|
|
41
|
+
return { entries, missing };
|
|
42
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/memories/service.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
import { buildMemoryOps } from '../ops/memory.js';
|
|
3
3
|
import { buildTrashOps } from '../ops/trash.js';
|
|
4
4
|
import { buildSceneRecordOps } from '../ops/scene-records.js';
|
|
5
|
+
// 类型面与导出计划拆出去后,memories/service.js 的公开面照旧(snapshot.ts、projection.ts、
|
|
6
|
+
// index-io.ts、ops/* 与 index.ts 都按这个路径引)。
|
|
7
|
+
export { planMemoryExport } from './export-plan.js';
|
|
5
8
|
// dsh-plugin-tool-management —— 规则/记忆(Rules v0.3,见 CHANGE-REQUEST-01)服务层。
|
|
6
9
|
//
|
|
7
10
|
// 记忆 = $DSH_HOME/tool-management/memories/<场景>/<name>.md(flat)或 <场景>/<name>/<name>.md(bundle)。
|
|
@@ -38,7 +41,7 @@ import { createHash } from 'node:crypto';
|
|
|
38
41
|
import { readFileSync, statSync } from 'node:fs';
|
|
39
42
|
import { copyFile, lstat, mkdir, readFile, readdir, stat } from 'node:fs/promises';
|
|
40
43
|
import { homedir } from 'node:os';
|
|
41
|
-
import {
|
|
44
|
+
import { join, resolve } from 'node:path';
|
|
42
45
|
import { isInsideRootResolved } from '../paths.js';
|
|
43
46
|
import { parseSkillDoc, resolveDshHome } from '../skills/core.js';
|
|
44
47
|
import { newTrashId } from '../hub.js';
|
|
@@ -74,43 +77,6 @@ export const SCENES_DIR = 'scenes';
|
|
|
74
77
|
export const MEMORIES_DIR = 'memories';
|
|
75
78
|
/** 提示词预设库目录名(hub 根下;域 = 提示词,工具 `prompt_manager_*`)。 */
|
|
76
79
|
export const PRESETS_DIR = 'prompts';
|
|
77
|
-
/**
|
|
78
|
-
* 记忆导出的落盘映射(`bundle-export` 的 `kind: 'memories'` 用,见 design-plan D14)。
|
|
79
|
-
*
|
|
80
|
-
* 记忆有两种形态(见文件头):flat = `<场景>/<name>.md`,bundle = `<场景>/<name>/<name>.md`;
|
|
81
|
-
* 而且**场景名本身可含 `/`**(多段场景名,见 `isValidGroupPath`)。所以「按 id 的最后一个
|
|
82
|
-
* `/` 切出场景与名字、再拼 `.md`」是错的:bundle 会被读成 `<场景>/<name>.md` → 读不到 →
|
|
83
|
-
* 该项目静默丢失(只在 `missing` 里留个名,界面按「导出成功」显示)。
|
|
84
|
-
*
|
|
85
|
-
* 这里一律按索引里那条规则自己的 `path` 与 `form` 决定;bundle 只交出目录,由调用方
|
|
86
|
-
* 把目录内的文件全部打包(与技能分支同口径)。索引里没有、或已被同名 bundle 遮蔽的 id
|
|
87
|
-
* 进 `missing`——遮蔽的 flat 不会被加载,导出去只会让人以为它能用。
|
|
88
|
-
*
|
|
89
|
-
* 纯函数(不碰磁盘),可直接断言。
|
|
90
|
-
*/
|
|
91
|
-
export function planMemoryExport(names, rules) {
|
|
92
|
-
const list = Array.isArray(names) ? names.map((n) => String(n).trim()).filter(Boolean) : [];
|
|
93
|
-
const byId = new Map();
|
|
94
|
-
for (const row of (Array.isArray(rules) ? rules : [])) {
|
|
95
|
-
const id = row && typeof row.id === 'string' ? row.id : '';
|
|
96
|
-
if (id !== '')
|
|
97
|
-
byId.set(id, row);
|
|
98
|
-
}
|
|
99
|
-
const entries = [];
|
|
100
|
-
const missing = [];
|
|
101
|
-
for (const id of list) {
|
|
102
|
-
const row = byId.get(id);
|
|
103
|
-
const abs = row && typeof row.path === 'string' ? row.path : '';
|
|
104
|
-
if (!row || abs === '' || row.shadowed === true) {
|
|
105
|
-
missing.push(id);
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
entries.push(row.form === 'bundle'
|
|
109
|
-
? { id, zip: id, abs: dirname(abs), kind: 'dir' }
|
|
110
|
-
: { id, zip: `${id}.md`, abs, kind: 'file' });
|
|
111
|
-
}
|
|
112
|
-
return { entries, missing };
|
|
113
|
-
}
|
|
114
80
|
export function createMemoriesService(ctx, deps) {
|
|
115
81
|
// 仅测试注入绝对路径;生产按 $DSH_HOME 解析(与核心技能目录同源)。
|
|
116
82
|
// v0.4:全部落到 $DSH_HOME/tool-management/ 一个目录内(memories/ + scenes/ + 侧车),
|