dsh-plugin-agentflow 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +88 -2
- package/cordis.patch.yml +195 -0
- package/index.js +448 -0
- package/lib/bridge.js +858 -0
- package/lib/client.js +4 -0
- package/package.json +42 -4
- package/runtime/boards.mjs +486 -0
- package/runtime/bridge.mjs +230 -0
- package/runtime/dsh-source.mjs +289 -0
- package/runtime/engine.mjs +352 -0
- package/runtime/index.html +3299 -0
- package/runtime/llm.mjs +252 -0
- package/runtime/mcp.mjs +823 -0
- package/runtime/preset-gen.mjs +1117 -0
- package/runtime/presets.mjs +399 -0
- package/runtime/run.mjs +76 -0
- package/runtime/server.mjs +1151 -0
- package/runtime/skills.mjs +131 -0
- package/runtime/store.mjs +103 -0
- package/runtime/verify.mjs +219 -0
- package/runtime/web/dist/assets/index-CkM6YPl4.js +348 -0
- package/runtime/web/dist/assets/index-DEZcpxgr.css +1 -0
- package/runtime/web/dist/index.html +13 -0
- package/scripts/build-client.mjs +36 -0
- package/scripts/sync-runtime.mjs +84 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// presets.mjs —— DSH agent preset 的读取与增删(供设置页「AgentFlow」分区用)。
|
|
2
|
+
//
|
|
3
|
+
// 自定义预设散在两处:
|
|
4
|
+
// 1. profile 的 cordis.patch.yml(AgentFlow 导出的标记段 + 用户手写的覆写行)
|
|
5
|
+
// 2. 各 bundle 包自己的 cordis.patch.yml(sharp-agent、qq-vibe 这类插件带的预设)
|
|
6
|
+
// 这里用文本解析(不引 YAML 库),够读 `@deepseek-ai/dsh-agent-preset` 这一种形状。
|
|
7
|
+
// 写入只动 profile:bundle 里的预设属于插件包,改了会被升级覆盖,所以只允许复制。
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
|
|
12
|
+
const PRESET_COMPONENT = '@deepseek-ai/dsh-agent-preset';
|
|
13
|
+
|
|
14
|
+
/** 手动复制/编辑产生的自定义预设写在这一段里,与 AgentFlow 导出的段分开 */
|
|
15
|
+
const MANUAL_BLOCK_START = '# >>> AgentFlow 自定义预设(手动复制/编辑,重新导出不会覆盖)';
|
|
16
|
+
const MANUAL_BLOCK_END = '# <<< AgentFlow 自定义预设结束';
|
|
17
|
+
|
|
18
|
+
function unquote(raw) {
|
|
19
|
+
const v = String(raw ?? '').trim();
|
|
20
|
+
if ((v.startsWith("'") && v.endsWith("'")) || (v.startsWith('"') && v.endsWith('"'))) {
|
|
21
|
+
return v.slice(1, -1).replace(/''/g, "'");
|
|
22
|
+
}
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function yamlString(value) {
|
|
27
|
+
return `'${String(value ?? '').replace(/'/g, "''")}'`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function indentOf(line) {
|
|
31
|
+
return line.length - line.trimStart().length;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 从一份 patch 文本里抽出所有 agent preset 定义。
|
|
36
|
+
* @param {string} text patch 文本
|
|
37
|
+
* @param {{source:'profile'|'bundle', packageName?:string}} meta
|
|
38
|
+
* @returns {Array<{rowId:string,id:string,name:string,description:string,source:string,packageName:string|null,order:number,lineStart:number,lineEnd:number}>}
|
|
39
|
+
*/
|
|
40
|
+
export function extractPresets(text, meta = { source: 'profile', packageName: null }) {
|
|
41
|
+
const lines = String(text || '').split('\n');
|
|
42
|
+
const out = [];
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
// 精确匹配组件名,别把 dsh-agent-preset-registry 也当成预设
|
|
45
|
+
if (!/^\s*name:\s*['"]?@deepseek-ai\/dsh-agent-preset['"]?\s*$/.test(lines[i])) continue;
|
|
46
|
+
// 向上找这一行的 id(- id: preset-xxx)
|
|
47
|
+
let rowId = '';
|
|
48
|
+
let rowIndent = 0;
|
|
49
|
+
let rowStart = -1;
|
|
50
|
+
for (let j = i; j >= 0; j--) {
|
|
51
|
+
const m = /^\s*-\s+id:\s*(\S+)\s*$/.exec(lines[j]);
|
|
52
|
+
if (m) {
|
|
53
|
+
rowId = unquote(m[1]);
|
|
54
|
+
rowIndent = indentOf(lines[j]);
|
|
55
|
+
rowStart = j;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (lines[j].trim() && indentOf(lines[j]) < indentOf(lines[i]) && !lines[j].trim().startsWith('-')) break;
|
|
59
|
+
}
|
|
60
|
+
if (!rowId) continue;
|
|
61
|
+
|
|
62
|
+
// 这一行的结束:下一个缩进 <= rowIndent 且是数组项的行
|
|
63
|
+
let rowEnd = lines.length;
|
|
64
|
+
for (let j = rowStart + 1; j < lines.length; j++) {
|
|
65
|
+
const t = lines[j].trim();
|
|
66
|
+
if (!t || t.startsWith('#')) continue;
|
|
67
|
+
if (indentOf(lines[j]) <= rowIndent && /^-\s/.test(t)) {
|
|
68
|
+
rowEnd = j;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 解析 config 下的 id / name / description / order
|
|
74
|
+
let cfgIndent = -1;
|
|
75
|
+
let id = '';
|
|
76
|
+
let name = '';
|
|
77
|
+
let description = '';
|
|
78
|
+
let order = 0;
|
|
79
|
+
for (let j = i + 1; j < rowEnd; j++) {
|
|
80
|
+
const line = lines[j];
|
|
81
|
+
if (!line.trim() || line.trim().startsWith('#')) continue;
|
|
82
|
+
const indent = indentOf(line);
|
|
83
|
+
if (cfgIndent < 0) {
|
|
84
|
+
if (/^\s*config:\s*$/.test(line)) cfgIndent = indent;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (indent <= cfgIndent) break;
|
|
88
|
+
let m;
|
|
89
|
+
if (!id && (m = /^\s*id:\s*(\S+)\s*$/.exec(line))) id = unquote(m[1]);
|
|
90
|
+
else if (!name && (m = /^\s*name:\s*(.+)$/.exec(line))) name = unquote(m[1]);
|
|
91
|
+
else if (!description && (m = /^\s*description:\s*(.+)$/.exec(line))) description = unquote(m[1]);
|
|
92
|
+
else if (!order && (m = /^\s*order:\s*(\d+)\s*$/.exec(line))) order = Number(m[1]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
out.push({
|
|
96
|
+
rowId,
|
|
97
|
+
id: id || rowId.replace(/^preset-/, ''),
|
|
98
|
+
name: name || id || rowId,
|
|
99
|
+
description,
|
|
100
|
+
source: meta.source,
|
|
101
|
+
packageName: meta.packageName ?? null,
|
|
102
|
+
order,
|
|
103
|
+
lineStart: rowStart,
|
|
104
|
+
lineEnd: rowEnd,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 读一个文件,不存在返回空串 */
|
|
111
|
+
async function readOr(file, fallback = '') {
|
|
112
|
+
return fs.readFile(file, 'utf8').catch(() => fallback);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 列出所有自定义预设(profile + 各 bundle),同 id 时 profile 覆盖 bundle。
|
|
117
|
+
* @param {string} profileDir
|
|
118
|
+
*/
|
|
119
|
+
export async function listPresets(profileDir) {
|
|
120
|
+
const patchFile = path.join(profileDir, 'cordis.patch.yml');
|
|
121
|
+
const pkgFile = path.join(profileDir, 'package.json');
|
|
122
|
+
|
|
123
|
+
const patch = await readOr(patchFile);
|
|
124
|
+
const fromProfile = extractPresets(patch, { source: 'profile', packageName: null });
|
|
125
|
+
|
|
126
|
+
let bundles = [];
|
|
127
|
+
try {
|
|
128
|
+
const pkg = JSON.parse(await fs.readFile(pkgFile, 'utf8'));
|
|
129
|
+
bundles = pkg?.dsh?.profile?.bundles || [];
|
|
130
|
+
} catch {
|
|
131
|
+
bundles = [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const fromBundles = [];
|
|
135
|
+
for (const bundle of bundles) {
|
|
136
|
+
const file = path.join(profileDir, 'node_modules', bundle, 'cordis.patch.yml');
|
|
137
|
+
const text = await readOr(file, '');
|
|
138
|
+
if (!text) continue;
|
|
139
|
+
fromBundles.push(...extractPresets(text, { source: 'bundle', packageName: bundle }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const byId = new Map();
|
|
143
|
+
for (const item of fromBundles) byId.set(item.id, item);
|
|
144
|
+
for (const item of fromProfile) byId.set(item.id, item);
|
|
145
|
+
|
|
146
|
+
return [...byId.values()]
|
|
147
|
+
.map(({ lineStart, lineEnd, ...rest }) => ({
|
|
148
|
+
...rest,
|
|
149
|
+
editable: rest.source === 'profile',
|
|
150
|
+
canCopy: true,
|
|
151
|
+
}))
|
|
152
|
+
.sort((a, b) => (a.order || 99) - (b.order || 99) || a.name.localeCompare(b.name));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 读 profile patch 与它里面某个 preset 的原始文本块(含 plugins) */
|
|
156
|
+
async function readProfilePresetBlock(profileDir, presetId) {
|
|
157
|
+
const file = path.join(profileDir, 'cordis.patch.yml');
|
|
158
|
+
const text = await fs.readFile(file, 'utf8');
|
|
159
|
+
const found = extractPresets(text, { source: 'profile', packageName: null }).find((p) => p.id === presetId);
|
|
160
|
+
if (!found) return null;
|
|
161
|
+
const lines = text.split('\n');
|
|
162
|
+
const block = lines.slice(found.lineStart, found.lineEnd).join('\n');
|
|
163
|
+
return { file, text, found, block, lines };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 从原块里取 plugins 内容,并按相对缩进重排到 8 空格(config.plugins 下面) */
|
|
167
|
+
function extractPluginsBody(block) {
|
|
168
|
+
const lines = String(block || '').split('\n');
|
|
169
|
+
const idx = lines.findIndex((l) => /^\s*plugins:\s*$/.test(l));
|
|
170
|
+
if (idx < 0) return '';
|
|
171
|
+
// 注释行(块尾标记等)不能进 plugins 内容:它们的缩进是 0,
|
|
172
|
+
// 归一化时会被切掉开头几个字符,把 `# <<< ...结束` 变成半截残片。
|
|
173
|
+
const body = lines.slice(idx + 1).filter((l) => !l.trim().startsWith('#'));
|
|
174
|
+
const nonEmpty = body.filter((l) => l.trim());
|
|
175
|
+
if (!nonEmpty.length) return '';
|
|
176
|
+
// 以子项(- id: xxx)的缩进为准:长文本块里的行缩进深浅不一,拿全量最小值会算错
|
|
177
|
+
const itemLines = nonEmpty.filter((l) => /^\s*-\s+id:/.test(l));
|
|
178
|
+
const minIndent = Math.min(...(itemLines.length ? itemLines : nonEmpty).map(indentOf));
|
|
179
|
+
return body
|
|
180
|
+
.map((l) => (l.trim() ? ' '.repeat(8) + l.slice(minIndent) : ''))
|
|
181
|
+
.join('\n')
|
|
182
|
+
.trimEnd();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 生成一个完整的新预设块。
|
|
187
|
+
* 必须用 `- insert:` 包裹:顶层 `- id: xxx` 的语义是「覆写已有行」,
|
|
188
|
+
* 新 id 不存在时整行会被 loader 忽略(踩过)。
|
|
189
|
+
*/
|
|
190
|
+
function buildPresetBlock({ presetId, name, description, order, plugins }) {
|
|
191
|
+
const lines = [
|
|
192
|
+
'- insert:',
|
|
193
|
+
` - id: preset-${presetId}`,
|
|
194
|
+
" name: '@deepseek-ai/dsh-agent-preset'",
|
|
195
|
+
' config:',
|
|
196
|
+
` id: ${presetId}`,
|
|
197
|
+
` name: ${yamlString(name)}`,
|
|
198
|
+
` description: ${yamlString(description || '')}`,
|
|
199
|
+
` order: ${order || 50}`,
|
|
200
|
+
];
|
|
201
|
+
if (plugins) lines.push(' plugins:', plugins);
|
|
202
|
+
return lines.join('\n');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** 生成一个不冲突的预设 id */
|
|
206
|
+
async function uniqueId(profileDir, base) {
|
|
207
|
+
const existing = new Set((await listPresets(profileDir)).map((p) => p.id));
|
|
208
|
+
let candidate = `${base}-copy`;
|
|
209
|
+
let n = 2;
|
|
210
|
+
while (existing.has(candidate)) {
|
|
211
|
+
candidate = `${base}-copy${n}`;
|
|
212
|
+
n++;
|
|
213
|
+
}
|
|
214
|
+
return candidate;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** 把一段文本写进 profile 的自定义预设标记段(已有则替换同一 id 的块) */
|
|
218
|
+
async function upsertManualBlock(profileDir, newId, block) {
|
|
219
|
+
const file = path.join(profileDir, 'cordis.patch.yml');
|
|
220
|
+
const text = await fs.readFile(file, 'utf8');
|
|
221
|
+
const manualStart = text.indexOf(MANUAL_BLOCK_START);
|
|
222
|
+
const head = manualStart >= 0 ? text.slice(0, manualStart).trimEnd() : text.trimEnd();
|
|
223
|
+
const manual = manualStart >= 0
|
|
224
|
+
? text.slice(manualStart)
|
|
225
|
+
.replace(MANUAL_BLOCK_START, '')
|
|
226
|
+
.replace(MANUAL_BLOCK_END, '')
|
|
227
|
+
: '';
|
|
228
|
+
|
|
229
|
+
// 去掉旧的自定义段,重新拼:原有条目 + 这次的新块
|
|
230
|
+
const kept = manual
|
|
231
|
+
.split(/\n(?=\s*-\s+id:\s+)/)
|
|
232
|
+
.filter((chunk) => chunk.trim() && !chunk.includes(`id: preset-${newId}`))
|
|
233
|
+
.join('\n')
|
|
234
|
+
.trim();
|
|
235
|
+
|
|
236
|
+
const body = [kept, block.trim()].filter(Boolean).join('\n\n');
|
|
237
|
+
const next = `${head}\n\n${MANUAL_BLOCK_START}\n${body}\n${MANUAL_BLOCK_END}\n`;
|
|
238
|
+
await fs.writeFile(file, next, 'utf8');
|
|
239
|
+
return file;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 复制一个预设成新的自定义预设(写进 profile 的自定义段)。
|
|
244
|
+
* bundle 来源也能复制 —— 这正是「把插件自带的预设改成自己的」的路径。
|
|
245
|
+
*/
|
|
246
|
+
export async function copyPreset(profileDir, presetId, opts = {}) {
|
|
247
|
+
const newName = typeof opts.name === 'string' && opts.name.trim()
|
|
248
|
+
? opts.name.trim()
|
|
249
|
+
: `${presetId} 副本`;
|
|
250
|
+
const newId = await uniqueId(profileDir, presetId);
|
|
251
|
+
|
|
252
|
+
// 先看 profile 自己的;没有再翻 bundle
|
|
253
|
+
let block = null;
|
|
254
|
+
const fromProfile = await readProfilePresetBlock(profileDir, presetId);
|
|
255
|
+
if (fromProfile) {
|
|
256
|
+
block = fromProfile.block;
|
|
257
|
+
} else {
|
|
258
|
+
const pkg = JSON.parse(await fs.readFile(path.join(profileDir, 'package.json'), 'utf8'));
|
|
259
|
+
for (const bundle of pkg?.dsh?.profile?.bundles || []) {
|
|
260
|
+
const file = path.join(profileDir, 'node_modules', bundle, 'cordis.patch.yml');
|
|
261
|
+
const text = await readOr(file, '');
|
|
262
|
+
if (!text) continue;
|
|
263
|
+
const found = extractPresets(text, { source: 'bundle', packageName: bundle }).find((p) => p.id === presetId);
|
|
264
|
+
if (!found) continue;
|
|
265
|
+
block = text.split('\n').slice(found.lineStart, found.lineEnd).join('\n');
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!block) throw new Error(`找不到预设:${presetId}`);
|
|
270
|
+
|
|
271
|
+
// 原块的显示信息一起带过来,只换 id 与名字
|
|
272
|
+
const meta = extractPresets(block, { source: 'profile', packageName: null })[0] || {};
|
|
273
|
+
const cloned = buildPresetBlock({
|
|
274
|
+
presetId: newId,
|
|
275
|
+
name: newName,
|
|
276
|
+
description: meta.description || '',
|
|
277
|
+
order: meta.order || 50,
|
|
278
|
+
plugins: extractPluginsBody(block),
|
|
279
|
+
});
|
|
280
|
+
const file = await upsertManualBlock(profileDir, newId, cloned);
|
|
281
|
+
return { ok: true, id: newId, name: newName, file };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* 清理自定义预设段里删剩下的空壳:
|
|
286
|
+
* - 孤立的 `- insert:`(后面没有子项了)
|
|
287
|
+
* - 整段都空了就把标记段一起删掉
|
|
288
|
+
*/
|
|
289
|
+
function cleanupManualBlock(text) {
|
|
290
|
+
const start = text.indexOf(MANUAL_BLOCK_START);
|
|
291
|
+
if (start < 0) return text;
|
|
292
|
+
const endIdx = text.indexOf(MANUAL_BLOCK_END);
|
|
293
|
+
const bodyRaw = endIdx > start
|
|
294
|
+
? text.slice(start + MANUAL_BLOCK_START.length, endIdx)
|
|
295
|
+
: text.slice(start + MANUAL_BLOCK_START.length);
|
|
296
|
+
|
|
297
|
+
const lines = bodyRaw.split('\n');
|
|
298
|
+
const kept = [];
|
|
299
|
+
for (let i = 0; i < lines.length; i++) {
|
|
300
|
+
const line = lines[i];
|
|
301
|
+
if (/^-\s+insert:\s*$/.test(line)) {
|
|
302
|
+
// 看后面还有没有属于它的子项(缩进更深的 - id:)
|
|
303
|
+
let hasChild = false;
|
|
304
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
305
|
+
const t = lines[j].trim();
|
|
306
|
+
if (!t || t.startsWith('#')) continue;
|
|
307
|
+
hasChild = /^\s+-\s+/.test(lines[j]);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
if (!hasChild) continue; // 空壳,丢掉
|
|
311
|
+
}
|
|
312
|
+
kept.push(line);
|
|
313
|
+
}
|
|
314
|
+
const body = kept.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
315
|
+
const head = text.slice(0, start).trimEnd();
|
|
316
|
+
const tail = endIdx >= 0 ? text.slice(endIdx + MANUAL_BLOCK_END.length).replace(/^\n+/, '') : '';
|
|
317
|
+
if (!body) {
|
|
318
|
+
return `${head}\n${tail}`.replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
|
|
319
|
+
}
|
|
320
|
+
return `${head}\n\n${MANUAL_BLOCK_START}\n${body}\n${MANUAL_BLOCK_END}\n${tail}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** 从 preset 块里抽 persona 文本(plugins 里 id=persona 的 config.prefix 块标量) */
|
|
324
|
+
function extractPersona(block) {
|
|
325
|
+
const lines = String(block || '').split('\n');
|
|
326
|
+
const idx = lines.findIndex((l) => /^\s*prefix:\s*[|>]/.test(l));
|
|
327
|
+
if (idx < 0) return '';
|
|
328
|
+
const baseIndent = indentOf(lines[idx]);
|
|
329
|
+
const raw = [];
|
|
330
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
331
|
+
const line = lines[i];
|
|
332
|
+
if (line.trim() && indentOf(line) <= baseIndent) break;
|
|
333
|
+
raw.push(line);
|
|
334
|
+
}
|
|
335
|
+
const nonEmpty = raw.filter((l) => l.trim());
|
|
336
|
+
if (!nonEmpty.length) return '';
|
|
337
|
+
const minIndent = Math.min(...nonEmpty.map(indentOf));
|
|
338
|
+
const text = raw.map((l) => (l.trim() ? l.slice(minIndent) : '')).join('\n');
|
|
339
|
+
return text.replace(/\s+$/, '');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 读一个预设的完整规格(名称/描述/persona 文本)。profile 优先,其次 bundle。
|
|
344
|
+
* @returns {Promise<{id:string,name:string,description:string,persona:string,source:string,packageName:string|null}>}
|
|
345
|
+
*/
|
|
346
|
+
export async function exportPresetSpec(profileDir, presetId) {
|
|
347
|
+
const wanted = String(presetId || '').trim();
|
|
348
|
+
if (!wanted) throw new Error('exportPresetSpec 需要 presetId');
|
|
349
|
+
|
|
350
|
+
const fromProfile = await readProfilePresetBlock(profileDir, wanted);
|
|
351
|
+
if (fromProfile) {
|
|
352
|
+
return {
|
|
353
|
+
id: wanted,
|
|
354
|
+
name: fromProfile.found.name || wanted,
|
|
355
|
+
description: fromProfile.found.description || '',
|
|
356
|
+
persona: extractPersona(fromProfile.block),
|
|
357
|
+
source: 'profile',
|
|
358
|
+
packageName: null,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const pkg = JSON.parse(await fs.readFile(path.join(profileDir, 'package.json'), 'utf8'));
|
|
363
|
+
for (const bundle of pkg?.dsh?.profile?.bundles || []) {
|
|
364
|
+
const file = path.join(profileDir, 'node_modules', bundle, 'cordis.patch.yml');
|
|
365
|
+
const text = await readOr(file, '');
|
|
366
|
+
if (!text) continue;
|
|
367
|
+
const found = extractPresets(text, { source: 'bundle', packageName: bundle }).find((p) => p.id === wanted);
|
|
368
|
+
if (!found) continue;
|
|
369
|
+
const block = text.split('\n').slice(found.lineStart, found.lineEnd).join('\n');
|
|
370
|
+
return {
|
|
371
|
+
id: wanted,
|
|
372
|
+
name: found.name || wanted,
|
|
373
|
+
description: found.description || '',
|
|
374
|
+
persona: extractPersona(block),
|
|
375
|
+
source: 'bundle',
|
|
376
|
+
packageName: bundle,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
throw Object.assign(new Error(`找不到预设:${wanted}`), { status: 404 });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** 删除 profile 里的一个自定义预设(bundle 来源不允许删) */
|
|
383
|
+
export async function deletePreset(profileDir, presetId) {
|
|
384
|
+
const file = path.join(profileDir, 'cordis.patch.yml');
|
|
385
|
+
const text = await fs.readFile(file, 'utf8');
|
|
386
|
+
const found = extractPresets(text, { source: 'profile', packageName: null }).find((p) => p.id === presetId);
|
|
387
|
+
if (!found) {
|
|
388
|
+
throw Object.assign(new Error(`profile 里没有这个预设:${presetId}(bundle 自带的只能复制,不能删)`), { status: 400 });
|
|
389
|
+
}
|
|
390
|
+
const lines = text.split('\n');
|
|
391
|
+
const before = lines.slice(0, found.lineStart);
|
|
392
|
+
const after = lines.slice(found.lineEnd);
|
|
393
|
+
// 顺手吃掉块尾多出来的空行,避免越删越多空行
|
|
394
|
+
while (after.length && after[0].trim() === '') after.shift();
|
|
395
|
+
let next = `${before.join('\n').trimEnd()}\n${after.join('\n')}`.replace(/\n{3,}/g, '\n\n');
|
|
396
|
+
next = cleanupManualBlock(next);
|
|
397
|
+
await fs.writeFile(file, next.endsWith('\n') ? next : next + '\n', 'utf8');
|
|
398
|
+
return { ok: true, id: presetId, file };
|
|
399
|
+
}
|
package/runtime/run.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// run.mjs —— 命令行跑画布,不开浏览器也能用。
|
|
3
|
+
//
|
|
4
|
+
// node run.mjs 跑整个 data/board.json
|
|
5
|
+
// node run.mjs n2 n3 只跑指定节点(及其下游)
|
|
6
|
+
// node run.mjs --task "赛博朋克侦探" 覆盖根节点的题材再跑
|
|
7
|
+
|
|
8
|
+
import { runBoard } from './engine.mjs';
|
|
9
|
+
import { loadBoard, saveBoard, loadConfig } from './store.mjs';
|
|
10
|
+
import { normalizeConfig, makeCaller } from './llm.mjs';
|
|
11
|
+
|
|
12
|
+
const args = process.argv.slice(2);
|
|
13
|
+
let subject = '';
|
|
14
|
+
const targets = [];
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
if (args[i] === '--task') subject = args[++i] || '';
|
|
17
|
+
else if (!args[i].startsWith('-')) targets.push(args[i]);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const board = await loadBoard();
|
|
21
|
+
if (!board) {
|
|
22
|
+
console.error('没有 data/board.json,先跑一次 node server.mjs 生成初始画布');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 有题材时塞给所有根节点(没有前驱的节点)
|
|
27
|
+
if (subject) {
|
|
28
|
+
const hasPred = new Set(board.edges.map((e) => e.to));
|
|
29
|
+
for (const n of board.nodes) {
|
|
30
|
+
if (!hasPred.has(n.id)) n.task = `${n.task}\n\n题材:${subject}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const cfg = normalizeConfig(await loadConfig());
|
|
35
|
+
// 模型调用只走 DSH 内核的 bridge;目录可用 AGENTFLOW_BRIDGE_DIR 换,
|
|
36
|
+
// 默认与插件写入的 ~/.dsh/agent-flow/data/dsh-bridge.json 对齐。
|
|
37
|
+
const callLLM = makeCaller({
|
|
38
|
+
bridgeDir: process.env.AGENTFLOW_BRIDGE_DIR || undefined,
|
|
39
|
+
mock: cfg.mock,
|
|
40
|
+
skillDirs: cfg.skillDirs,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
console.log(`模式:${cfg.mock ? 'mock' : '真实调用'}\n`);
|
|
44
|
+
|
|
45
|
+
const t0 = Date.now();
|
|
46
|
+
const result = await runBoard(board, {
|
|
47
|
+
targets: targets.length ? targets : undefined,
|
|
48
|
+
callLLM,
|
|
49
|
+
emit: (ev) => {
|
|
50
|
+
const t = ((Date.now() - t0) / 1000).toFixed(1);
|
|
51
|
+
if (ev.type === 'node:start') console.log(`[${t}s] ▶ ${ev.id}`);
|
|
52
|
+
else if (ev.type === 'node:done') {
|
|
53
|
+
const u = ev.usage || {};
|
|
54
|
+
console.log(`[${t}s] ✓ ${ev.id} ${ev.model} ${u.promptTokens || 0}/${u.completionTokens || 0} tok ${u.ms || 0}ms`);
|
|
55
|
+
const preview = (ev.content || '').split('\n').slice(0, 3).join('\n ');
|
|
56
|
+
console.log(` ${preview}\n`);
|
|
57
|
+
} else if (ev.type === 'node:retry') console.log(`[${t}s] ↻ ${ev.id} ${ev.model} 失败:${ev.error}`);
|
|
58
|
+
else if (ev.type === 'node:failed') console.log(`[${t}s] ✗ ${ev.id} 全部模型失败`);
|
|
59
|
+
else if (ev.type === 'node:blocked') console.log(`[${t}s] ⊘ ${ev.id} 阻塞(${ev.reason})`);
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// 把物料写回去,下次打开网页就能看到
|
|
64
|
+
for (const n of board.nodes) {
|
|
65
|
+
const c = result.content.get(n.id);
|
|
66
|
+
if (c) n.content = c;
|
|
67
|
+
const st = result.state.get(n.id);
|
|
68
|
+
if (st) n.status = st;
|
|
69
|
+
const m = result.usedModel.get(n.id);
|
|
70
|
+
if (m) n.usage = { model: m, ...(result.usage.get(n.id) || {}) };
|
|
71
|
+
}
|
|
72
|
+
await saveBoard(board);
|
|
73
|
+
|
|
74
|
+
const { done, failed, blocked } = result.summary;
|
|
75
|
+
console.log(`\n完成 ${done.length} · 失败 ${failed.length} · 阻塞 ${blocked.length}`);
|
|
76
|
+
process.exit(failed.length || blocked.length ? 1 : 0);
|