dsh-plugin-tool-management 0.2.0 → 0.5.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 +236 -226
- package/README_EN.md +290 -231
- package/cordis.patch.yml +11 -0
- package/docs/Changelog.md +471 -0
- package/docs/images/MCP.png +0 -0
- package/docs/images//344/274/232/350/257/235.png +0 -0
- package/docs/images//345/234/272/346/231/257.png +0 -0
- package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images//346/212/200/350/203/275.png +0 -0
- package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images//350/256/260/345/277/206.png +0 -0
- package/lib/approval-policy.js +46 -0
- package/lib/client.js +3489 -2173
- package/lib/hub.js +94 -0
- package/lib/imports/upload.js +286 -0
- package/lib/index.js +512 -155
- package/lib/rules/archive-engine.js +191 -0
- package/lib/rules/archive.js +140 -0
- package/lib/rules/service.js +609 -100
- package/lib/skills/core.js +356 -22
- package/lib/skills/service.js +31 -5
- package/lib/subagents/service.js +333 -0
- package/lib/subagents/tools.js +62 -0
- package/package.json +12 -3
- package/screenshots.json +7 -5
- package/docs/images/agents-md.png +0 -0
- package/docs/images/history.png +0 -0
- package/docs/images/mcp.png +0 -0
- package/docs/images/skills.png +0 -0
- package/docs/images//345/234/272/346/231/257/350/256/260/345/277/206.png +0 -0
package/lib/hub.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// dsh-plugin-tool-management —— 统一数据目录(v0.4「一个 hub」)。
|
|
2
|
+
//
|
|
3
|
+
// 用户在 2026-09-13 提出:插件产生的文件夹/文件全部收在 `$DSH_HOME/tool-management/`
|
|
4
|
+
// 一个目录里,方便统一备份与查看。此前它们散在四处:
|
|
5
|
+
//
|
|
6
|
+
// $DSH_HOME/scene-memory/ 记忆真源 → tool-management/memories/
|
|
7
|
+
// $DSH_HOME/tool-management/ 侧车(索引/回收站) → 就地(hub 根)
|
|
8
|
+
// $DSH_HOME/subagents/ 人设 → tool-management/agents/
|
|
9
|
+
// <插件目录>/data/agents-md-presets/ AGENTS.md 预设库 → tool-management/agents-md/
|
|
10
|
+
//
|
|
11
|
+
// 最终布局:
|
|
12
|
+
//
|
|
13
|
+
// ~/.dsh/tool-management/
|
|
14
|
+
// ├─ memories/<场景>/<名>.md | <场景>/<名>/SKILL.md 记忆正文(真源)
|
|
15
|
+
// ├─ agents/<人设>.md 子智能体人设
|
|
16
|
+
// ├─ agents-md/<预设 id>/AGENTS.md 全局指令基线预设库
|
|
17
|
+
// ├─ skills/ 插件新建的技能(可写根)
|
|
18
|
+
// ├─ trash/rules-trash/<id>/ 记忆回收站
|
|
19
|
+
// ├─ rules-index.json 启停/顺序/场景记录/档案/模式
|
|
20
|
+
// └─ config.json 插件设置
|
|
21
|
+
//
|
|
22
|
+
// 搬运纪律(三条,全部为了「不丢数据」):
|
|
23
|
+
// 1. **只搬不删**——源目录里的文件被 `rename` 走后,空壳目录保留;
|
|
24
|
+
// 搬不动的(被占用、跨卷失败)留在原地,下次启动再试。
|
|
25
|
+
// 2. **绝不覆盖**——目标已存在同名项时跳过该项,保留目标。
|
|
26
|
+
// 3. **每进程一次**——迁移是启动期的一次性动作,失败也不阻断服务(旧目录仍可读)。
|
|
27
|
+
import { cp, lstat, mkdir, readdir, rename, rm } from 'node:fs/promises';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import { resolveDshHome } from './skills/core.js';
|
|
30
|
+
/** hub 根目录名(`$DSH_HOME` 下)。 */
|
|
31
|
+
export const HUB_DIR = 'tool-management';
|
|
32
|
+
/** hub 根绝对路径。 */
|
|
33
|
+
export function hubRoot() {
|
|
34
|
+
return join(resolveDshHome(), HUB_DIR);
|
|
35
|
+
}
|
|
36
|
+
/** hub 下的子目录绝对路径。 */
|
|
37
|
+
export function hubPath(...segments) {
|
|
38
|
+
return join(hubRoot(), ...segments);
|
|
39
|
+
}
|
|
40
|
+
/** 路径是否存在(不跟随符号链接)。 */
|
|
41
|
+
export async function pathExists(path) {
|
|
42
|
+
try {
|
|
43
|
+
await lstat(path);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 把一个目录里的**顶层条目**搬到另一个目录(幂等、只搬不删、绝不覆盖)。
|
|
52
|
+
* @param from 源目录(不存在则直接返回 0)
|
|
53
|
+
* @param to 目标目录(自动创建)
|
|
54
|
+
* @param filter 可选的条目过滤(默认全部非隐藏项)
|
|
55
|
+
* @returns 实际搬移的条目数
|
|
56
|
+
*/
|
|
57
|
+
export async function relocateEntries(from, to, filter) {
|
|
58
|
+
if (from === to)
|
|
59
|
+
return 0;
|
|
60
|
+
let names;
|
|
61
|
+
try {
|
|
62
|
+
names = await readdir(from);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
await mkdir(to, { recursive: true }).catch(() => undefined);
|
|
68
|
+
let moved = 0;
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
if (name.startsWith('.'))
|
|
71
|
+
continue;
|
|
72
|
+
if (filter && !filter(name))
|
|
73
|
+
continue;
|
|
74
|
+
const src = join(from, name);
|
|
75
|
+
const dest = join(to, name);
|
|
76
|
+
try {
|
|
77
|
+
if (await pathExists(dest))
|
|
78
|
+
continue; // 绝不覆盖
|
|
79
|
+
try {
|
|
80
|
+
await rename(src, dest);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// 跨卷 / 目标被占用 → 复制后删源;复制失败则原样留在旧目录(下次再试)。
|
|
84
|
+
await cp(src, dest, { recursive: true });
|
|
85
|
+
await rm(src, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
moved++;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* 单项失败不阻断其余项 */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return moved;
|
|
94
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// src/imports/upload.ts —— 导入通道共享纯逻辑(无 I/O):base64 解码、ZIP 解包、路径校验、落点规划。
|
|
2
|
+
// 两个使用方:子智能体(人设 = ~/.dsh/tool-management/agents/<名>.md)与
|
|
3
|
+
// 场景记忆(~/.dsh/tool-management/memories/<场景>/<名>.md;场景留空 = 保留场景 global)。
|
|
4
|
+
// 约定:只认 .md;zip 内任意层级;隐藏项 / 绝对路径 / `..` 穿越 / 超限条目一律跳过并回报;
|
|
5
|
+
// 重名策略(跳过 or 覆盖)不在这里实现——由调用方按文件系统现状裁决(本项目取「跳过并报告」)。
|
|
6
|
+
import { unzipSync } from 'fflate';
|
|
7
|
+
export const MAX_IMPORT_FILES = 200;
|
|
8
|
+
export const MAX_IMPORT_ENTRY_BYTES = 8 * 1024 * 1024;
|
|
9
|
+
export const MAX_IMPORT_TOTAL_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
export const MAX_IMPORT_ENTRIES = 2000;
|
|
11
|
+
export const MAX_IMPORT_NAME_LENGTH = 64;
|
|
12
|
+
const message = (e) => String((e && e.message) || e);
|
|
13
|
+
/** base64 → 字节;容忍 `data:...;base64,` 前缀。 */
|
|
14
|
+
export function decodeBase64(data) {
|
|
15
|
+
const text = typeof data === 'string' ? data : '';
|
|
16
|
+
const clean = text.startsWith('data:') && text.indexOf(',') >= 0 ? text.slice(text.indexOf(',') + 1) : text;
|
|
17
|
+
return new Uint8Array(Buffer.from(clean, 'base64'));
|
|
18
|
+
}
|
|
19
|
+
/** ZIP 魔数(PK\x03\x04);不靠扩展名判断,改名的压缩包也能认出来。 */
|
|
20
|
+
export function isZipBytes(bytes) {
|
|
21
|
+
return bytes.length > 3 && bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 归档条目路径归一化:反斜杠按 `/` 处理、丢弃 `.`/空段;
|
|
25
|
+
* 绝对路径(`/foo`、`C:\foo`)、`..` 穿越、隐藏项(`.x`)→ null(跳过)。
|
|
26
|
+
*/
|
|
27
|
+
export function normalizeEntryPath(raw) {
|
|
28
|
+
const p = String(raw || '').replace(/\\/g, '/').trim();
|
|
29
|
+
if (!p)
|
|
30
|
+
return null;
|
|
31
|
+
if (p.startsWith('/') || /^[A-Za-z]:/.test(p))
|
|
32
|
+
return null;
|
|
33
|
+
const parts = [];
|
|
34
|
+
for (const seg of p.split('/')) {
|
|
35
|
+
if (seg === '' || seg === '.')
|
|
36
|
+
continue;
|
|
37
|
+
if (seg === '..')
|
|
38
|
+
return null;
|
|
39
|
+
if (seg.startsWith('.'))
|
|
40
|
+
return null;
|
|
41
|
+
if (seg.length > 255)
|
|
42
|
+
return null;
|
|
43
|
+
parts.push(seg);
|
|
44
|
+
}
|
|
45
|
+
return parts.length ? parts.join('/') : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* 展开上传文件列表 → 扁平条目(zip 解包 + 路径校验 + 限额)。
|
|
49
|
+
* 单个文件损坏/超限只记 problem,不阻断同批其余文件(部分成功语义)。
|
|
50
|
+
*/
|
|
51
|
+
export function expandUploads(files) {
|
|
52
|
+
const all = Array.isArray(files) ? files : [];
|
|
53
|
+
const list = all.slice(0, MAX_IMPORT_FILES);
|
|
54
|
+
const problems = [];
|
|
55
|
+
const entries = [];
|
|
56
|
+
if (all.length > MAX_IMPORT_FILES) {
|
|
57
|
+
problems.push({ name: `(其余 ${all.length - MAX_IMPORT_FILES} 个文件)`, reason: `一次最多导入 ${MAX_IMPORT_FILES} 个文件` });
|
|
58
|
+
}
|
|
59
|
+
let total = 0;
|
|
60
|
+
for (const raw of list) {
|
|
61
|
+
const file = (raw || {});
|
|
62
|
+
const name = String(file.name || '').trim() || '(未命名)';
|
|
63
|
+
const bytes = decodeBase64(file.data);
|
|
64
|
+
if (!bytes.length) {
|
|
65
|
+
problems.push({ name, reason: '内容为空' });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (bytes.length > MAX_IMPORT_ENTRY_BYTES) {
|
|
69
|
+
problems.push({ name, reason: `单个文件超过 ${MAX_IMPORT_ENTRY_BYTES >> 20} MiB` });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
total += bytes.length;
|
|
73
|
+
if (total > MAX_IMPORT_TOTAL_BYTES) {
|
|
74
|
+
problems.push({ name, reason: `合计超过 ${MAX_IMPORT_TOTAL_BYTES >> 20} MiB,其余文件已忽略` });
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
if (isZipBytes(bytes)) {
|
|
78
|
+
let unzipped;
|
|
79
|
+
try {
|
|
80
|
+
let count = 0;
|
|
81
|
+
unzipped = unzipSync(bytes, {
|
|
82
|
+
filter(info) {
|
|
83
|
+
count += 1;
|
|
84
|
+
// 被 filter 丢掉的条目必须回报:静默丢弃会让用户以为「全都导入成功了」。
|
|
85
|
+
// 2026-09-13 实测(限额验证批):9 MiB 附件与第 2001 个条目都曾无声消失,
|
|
86
|
+
// 响应里 imported=1 / skipped=[] —— 用户完全看不出少了东西。
|
|
87
|
+
if (count > MAX_IMPORT_ENTRIES) {
|
|
88
|
+
// 超条目数时只报一次,避免 2000+ 条问题刷屏。
|
|
89
|
+
if (count === MAX_IMPORT_ENTRIES + 1) {
|
|
90
|
+
problems.push({ name, reason: `zip 内条目超过 ${MAX_IMPORT_ENTRIES} 个,其余条目已忽略` });
|
|
91
|
+
}
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (!info.name.endsWith('/') && info.originalSize > MAX_IMPORT_ENTRY_BYTES) {
|
|
95
|
+
problems.push({ name: info.name, reason: `zip 内单条目超过 ${MAX_IMPORT_ENTRY_BYTES >> 20} MiB,已跳过` });
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
problems.push({ name, reason: 'ZIP 解压失败:' + message(e) });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
for (const [entryName, content] of Object.entries(unzipped)) {
|
|
107
|
+
if (entryName.endsWith('/'))
|
|
108
|
+
continue;
|
|
109
|
+
const normalized = normalizeEntryPath(entryName);
|
|
110
|
+
if (!normalized) {
|
|
111
|
+
problems.push({ name: entryName, reason: '路径非法或隐藏项,已跳过' });
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
// zip 内保留全部扩展名:bundle 导入需要 SKILL.md 的附件(图片等);各 planner 自行取舍。
|
|
115
|
+
entries.push({ path: normalized, bytes: content });
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!name.toLowerCase().endsWith('.md')) {
|
|
120
|
+
problems.push({ name, reason: '只支持 .md 或 .zip' });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
entries.push({ path: name, bytes });
|
|
124
|
+
}
|
|
125
|
+
return { entries, problems };
|
|
126
|
+
}
|
|
127
|
+
/** 单个名字段(人设名 / 记忆名 / 场景路径的一段)合法性:与宿主侧校验同口径。 */
|
|
128
|
+
export function isValidImportName(name) {
|
|
129
|
+
const s = String(name || '');
|
|
130
|
+
return s.length > 0 && s.length <= MAX_IMPORT_NAME_LENGTH && s === s.trim() && !s.startsWith('.') && !/[\\/<>:"|?*]/.test(s);
|
|
131
|
+
}
|
|
132
|
+
/** 场景/分组路径合法性:逐段校验(空串 = 根/全局,合法)。 */
|
|
133
|
+
export function isValidImportGroup(group) {
|
|
134
|
+
const s = String(group || '');
|
|
135
|
+
if (s === '')
|
|
136
|
+
return true;
|
|
137
|
+
const segs = s.split('/');
|
|
138
|
+
return segs.length > 0 && segs.every((seg) => isValidImportName(seg));
|
|
139
|
+
}
|
|
140
|
+
/** 人设落点:**只看文件名**(zip 内的目录层级忽略),一个 .md 一个人设;zip 里顺带的非 .md(如附件)忽略。 */
|
|
141
|
+
export function planPersonaImport(entries) {
|
|
142
|
+
const targets = [];
|
|
143
|
+
const problems = [];
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
for (const entry of entries) {
|
|
146
|
+
const base = entry.path.slice(entry.path.lastIndexOf('/') + 1);
|
|
147
|
+
if (!base.toLowerCase().endsWith('.md'))
|
|
148
|
+
continue;
|
|
149
|
+
const name = base.slice(0, -3); // 去掉 .md
|
|
150
|
+
if (!isValidImportName(name)) {
|
|
151
|
+
problems.push({ name: entry.path, reason: '人设名不合法(非空、≤64 字符、不含路径分隔符与 < > : " | ? *、不以 . 开头)' });
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (seen.has(name)) {
|
|
155
|
+
problems.push({ name: entry.path, reason: '同批次重名,已跳过' });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
seen.add(name);
|
|
159
|
+
targets.push({ name, bytes: entry.bytes });
|
|
160
|
+
}
|
|
161
|
+
return { targets, problems };
|
|
162
|
+
}
|
|
163
|
+
/** bundle 附件限额:与 rules-attach 同口径(名字平铺、单个 8 MiB 由 expandUploads 兜底、单包 32 个/16 MiB)。 */
|
|
164
|
+
const MAX_BUNDLE_ATTACHMENTS = 32;
|
|
165
|
+
const MAX_BUNDLE_ATTACH_TOTAL = 16 * 1024 * 1024;
|
|
166
|
+
/**
|
|
167
|
+
* 记忆落点:
|
|
168
|
+
* - 裸 `.md` → 落到 defaultScene(空串 = 全局:任何对话都注入);zip 内带目录 → 目录路径即场景/分组。
|
|
169
|
+
* - zip 内 `<场景路径>/<名>/SKILL.md` → bundle 记忆(目录末段是记忆名,其余前缀是场景);同层非 `.md`
|
|
170
|
+
* 文件作为附件一并带入(`.md` 不带——bundle 是叶子,塞进去不会被发现,静默降级反而误导)。
|
|
171
|
+
* - zip 根层的裸 `SKILL.md` 没有目录名可当记忆名 → 跳过并回报;`SKILL.md` 的大小写变体也跳过
|
|
172
|
+
* (发现层只认精确 `SKILL.md`,NTFS 大小写不敏感下两者不能共存)。
|
|
173
|
+
*/
|
|
174
|
+
export function planMemoryImport(entries, defaultScene) {
|
|
175
|
+
const fallback = String(defaultScene || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '').trim();
|
|
176
|
+
const targets = [];
|
|
177
|
+
const problems = [];
|
|
178
|
+
const seen = new Set();
|
|
179
|
+
const split = (path) => {
|
|
180
|
+
const idx = path.lastIndexOf('/');
|
|
181
|
+
return { dir: idx >= 0 ? path.slice(0, idx) : '', base: path.slice(idx + 1) };
|
|
182
|
+
};
|
|
183
|
+
// pass 1:认 bundle 目录(精确 SKILL.md;大小写变体只报跳过,不当 bundle 也不当 flat)。
|
|
184
|
+
const bundleDocs = new Map();
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
const { dir, base } = split(entry.path);
|
|
187
|
+
if (base !== 'SKILL.md' && base.toLowerCase() !== 'skill.md')
|
|
188
|
+
continue;
|
|
189
|
+
if (base !== 'SKILL.md') {
|
|
190
|
+
problems.push({ name: entry.path, reason: 'SKILL.md 大小写变体不导入(发现层只认精确 SKILL.md,且与 NTFS 大小写不敏感冲突),已跳过' });
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (dir === '') {
|
|
194
|
+
problems.push({ name: entry.path, reason: 'SKILL.md 在 zip 根层,没有目录名可作记忆名,已跳过' });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
bundleDocs.set(dir, entry);
|
|
198
|
+
}
|
|
199
|
+
// pass 2:bundle 附件(同层非 .md);深层与同层 .md 明确回报,bundle 目录外的非 .md 维持旧口径静默忽略。
|
|
200
|
+
const attachments = new Map();
|
|
201
|
+
for (const entry of entries) {
|
|
202
|
+
const { dir, base } = split(entry.path);
|
|
203
|
+
if (base.toLowerCase().endsWith('.md'))
|
|
204
|
+
continue;
|
|
205
|
+
const bundleDir = bundleDocs.has(dir) ? dir : [...bundleDocs.keys()].find((d) => dir.startsWith(d + '/'));
|
|
206
|
+
if (!bundleDir)
|
|
207
|
+
continue;
|
|
208
|
+
if (dir !== bundleDir) {
|
|
209
|
+
problems.push({ name: entry.path, reason: 'bundle 只支持一层附件(SKILL.md 同层),子目录条目已跳过' });
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (!entry.bytes.length) {
|
|
213
|
+
problems.push({ name: entry.path, reason: '附件内容为空,已跳过' });
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const name = base;
|
|
217
|
+
if (!isValidImportName(name)) {
|
|
218
|
+
problems.push({ name: entry.path, reason: '附件名不合法,已跳过' });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const list = attachments.get(bundleDir) || [];
|
|
222
|
+
if (list.length >= MAX_BUNDLE_ATTACHMENTS) {
|
|
223
|
+
problems.push({ name: entry.path, reason: `附件超过 ${MAX_BUNDLE_ATTACHMENTS} 个,已跳过` });
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const total = list.reduce((sum, a) => sum + a.bytes.length, 0) + entry.bytes.length;
|
|
227
|
+
if (total > MAX_BUNDLE_ATTACH_TOTAL) {
|
|
228
|
+
problems.push({ name: entry.path, reason: `附件合计超过 ${MAX_BUNDLE_ATTACH_TOTAL >> 20} MiB,已跳过` });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
list.push({ name, bytes: entry.bytes });
|
|
232
|
+
attachments.set(bundleDir, list);
|
|
233
|
+
}
|
|
234
|
+
// pass 3:先规划 bundle 目标(含 SKILL.md 同层 .md 的明确跳过),再走 flat。
|
|
235
|
+
for (const [dir, doc] of bundleDocs) {
|
|
236
|
+
const segs = dir.split('/');
|
|
237
|
+
const name = segs[segs.length - 1];
|
|
238
|
+
const group = segs.slice(0, -1).join('/');
|
|
239
|
+
if (!isValidImportName(name)) {
|
|
240
|
+
problems.push({ name: doc.path, reason: 'bundle 名不合法(非空、≤64 字符、不含路径分隔符与 < > : " | ? *、不以 . 开头),已跳过' });
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
if (!isValidImportGroup(group)) {
|
|
244
|
+
problems.push({ name: doc.path, reason: '场景名不合法,已跳过' });
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const id = group ? `${group}/${name}` : name;
|
|
248
|
+
if (seen.has(id)) {
|
|
249
|
+
problems.push({ name: doc.path, reason: '同批次重名,已跳过' });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
seen.add(id);
|
|
253
|
+
targets.push({ group, name, bytes: doc.bytes, kind: 'bundle', attachments: attachments.get(dir) || [] });
|
|
254
|
+
}
|
|
255
|
+
for (const entry of entries) {
|
|
256
|
+
const { dir, base } = split(entry.path);
|
|
257
|
+
if (!base.toLowerCase().endsWith('.md'))
|
|
258
|
+
continue;
|
|
259
|
+
if (base === 'SKILL.md')
|
|
260
|
+
continue;
|
|
261
|
+
if (base.toLowerCase() === 'skill.md')
|
|
262
|
+
continue; // pass 1 已回报
|
|
263
|
+
if ([...bundleDocs.keys()].some((d) => dir === d || dir.startsWith(d + '/'))) {
|
|
264
|
+
problems.push({ name: entry.path, reason: 'bundle 目录内的非 SKILL.md 的 .md 不导入(bundle 是叶子,放进去不会被发现),已跳过' });
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const name = base.slice(0, -3); // 去掉 .md
|
|
268
|
+
if (!isValidImportName(name)) {
|
|
269
|
+
problems.push({ name: entry.path, reason: '记忆名不合法(非空、≤64 字符、不含路径分隔符与 < > : " | ? *、不以 . 开头)' });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
272
|
+
const group = dir || fallback;
|
|
273
|
+
if (!isValidImportGroup(group)) {
|
|
274
|
+
problems.push({ name: entry.path, reason: '场景名不合法,已跳过' });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const id = group ? `${group}/${name}` : name;
|
|
278
|
+
if (seen.has(id)) {
|
|
279
|
+
problems.push({ name: entry.path, reason: '同批次重名,已跳过' });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
seen.add(id);
|
|
283
|
+
targets.push({ group, name, bytes: entry.bytes, kind: 'flat' });
|
|
284
|
+
}
|
|
285
|
+
return { targets, problems };
|
|
286
|
+
}
|