dsh-plugin-bridge 0.2.10
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/LICENSE +21 -0
- package/README.md +146 -0
- package/README.zh.md +146 -0
- package/cordis.patch.yml +18 -0
- package/docs/articles/agent-session-handoff.md +162 -0
- package/docs/benchmark.md +165 -0
- package/docs/design.md +82 -0
- package/docs/guide.zh.md +152 -0
- package/docs/native-webui-feasibility.md +44 -0
- package/docs/plan.md +65 -0
- package/lib/api-rpc.d.ts +64 -0
- package/lib/api-rpc.js +76 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +368 -0
- package/lib/command.d.ts +92 -0
- package/lib/command.js +377 -0
- package/lib/compression.d.ts +90 -0
- package/lib/compression.js +369 -0
- package/lib/fold.d.ts +39 -0
- package/lib/fold.js +406 -0
- package/lib/index.d.ts +55 -0
- package/lib/index.js +76 -0
- package/lib/migrate.d.ts +176 -0
- package/lib/migrate.js +449 -0
- package/lib/rpc.d.ts +34 -0
- package/lib/rpc.js +76 -0
- package/lib/types.d.ts +64 -0
- package/lib/types.js +9 -0
- package/package.json +106 -0
- package/reports/v0.2.3-e2e-2026-08-20T13-19-13-924Z.raw.json +2727 -0
- package/reports/v0.2.3-e2e-report.md +116 -0
- package/reports/v0.2.6-rc11-vision-report.md +65 -0
package/lib/cli.js
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dsh-bridge:跨 preset 会话迁移的命令行入口。
|
|
4
|
+
*
|
|
5
|
+
* **日常使用请在会话里打 `/bridge <preset>`**(见 src/command.ts)——那条路走
|
|
6
|
+
* 进程内的 `ctx.apiProxy`,不需要端口也不需要环境变量。
|
|
7
|
+
*
|
|
8
|
+
* 这个 CLI 是手动 / 脚本路径:从 minimal 之类拿不到命令面的地方迁出来、
|
|
9
|
+
* 在终端里批量操作、或者给评测 harness 复用同一套编排。会话身份取自
|
|
10
|
+
* `DSH_SESSION_ID`(模型 shell 环境里有),网关地址取自 `DSH_WEB_URL`。
|
|
11
|
+
*
|
|
12
|
+
* node <pkg>/lib/cli.js presets
|
|
13
|
+
* node <pkg>/lib/cli.js preview --to code
|
|
14
|
+
* node <pkg>/lib/cli.js migrate --to code --summary-file <预览给出的路径>
|
|
15
|
+
* node <pkg>/lib/cli.js doctor
|
|
16
|
+
*/
|
|
17
|
+
import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { SOURCE_CHAR_BUDGET, SUMMARY_CHAR_BUDGET, estimateSummaryTokens } from './compression.js';
|
|
21
|
+
import { executeMigration, findSession, listPresets, migratedTitle, previewMigration, resolveWorkerModel, titleOf, } from './migrate.js';
|
|
22
|
+
import { RpcError, createRpc, resolveApiBase } from './rpc.js';
|
|
23
|
+
const HELP = `dsh-bridge · 跨 preset 会话迁移
|
|
24
|
+
|
|
25
|
+
用法:
|
|
26
|
+
dsh-bridge presets 列出可迁入的模式
|
|
27
|
+
dsh-bridge preview --to <preset> 生成交接摘要并打印(不改动任何会话)
|
|
28
|
+
dsh-bridge migrate --to <preset> --summary-file <path>
|
|
29
|
+
用(可能已编辑过的)摘要建新会话并交接
|
|
30
|
+
dsh-bridge run --to <preset> 预览 + 迁移一步到位(无人值守时用)
|
|
31
|
+
dsh-bridge doctor 自检:网关、会话身份、可用模式
|
|
32
|
+
|
|
33
|
+
通用参数:
|
|
34
|
+
--session <id> 源会话 id(默认取环境变量 DSH_SESSION_ID)
|
|
35
|
+
--api <url> 网关地址(默认 DSH_API,其次 DSH_WEB_URL/api,其次 127.0.0.1:3080/api)
|
|
36
|
+
--json 输出 JSON,便于程序消费
|
|
37
|
+
--quiet 不打印进度
|
|
38
|
+
|
|
39
|
+
preview / run:
|
|
40
|
+
--tier <flash|current|pro> 压缩档位(默认 pro)
|
|
41
|
+
--provider <id> --model <id> 直接指定压缩模型,跳过档位推断
|
|
42
|
+
--lang <zh|en|auto> 摘要语言(默认 auto,跟着会话内容走)
|
|
43
|
+
--source-budget <n> 取材字符预算(默认 ${SOURCE_CHAR_BUDGET})
|
|
44
|
+
--summary-budget <n> 摘要字符预算(默认 ${SUMMARY_CHAR_BUDGET})
|
|
45
|
+
--poll-ms <n> 轮询工人是否跑完的间隔(默认 2000,调试用)
|
|
46
|
+
--worker-timeout <ms> 工人单轮上限(默认 360000)
|
|
47
|
+
|
|
48
|
+
migrate / run:
|
|
49
|
+
--summary-file <path> 摘要正文(migrate 必填;run 忽略)
|
|
50
|
+
--goal-rounds <n> 目标自主轮次上限(默认 1;上游部署默认是 256)
|
|
51
|
+
--inject <goal|prompt|both> 摘要注入方式(默认 both)
|
|
52
|
+
--continue 同一轮复述并继续;默认复述后等待确认
|
|
53
|
+
--no-kickoff 不发首轮交接指令
|
|
54
|
+
--title <text> 新会话标题(默认「<原标题> → <preset>」)
|
|
55
|
+
`;
|
|
56
|
+
function parseArgs(argv) {
|
|
57
|
+
const flags = new Map();
|
|
58
|
+
const positional = [];
|
|
59
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
60
|
+
const token = argv[i];
|
|
61
|
+
if (!token.startsWith('--')) {
|
|
62
|
+
positional.push(token);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
const eq = token.indexOf('=');
|
|
66
|
+
if (eq > 0) {
|
|
67
|
+
flags.set(token.slice(2, eq), token.slice(eq + 1));
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const key = token.slice(2);
|
|
71
|
+
const next = argv[i + 1];
|
|
72
|
+
if (next !== undefined && !next.startsWith('--')) {
|
|
73
|
+
flags.set(key, next);
|
|
74
|
+
i += 1;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
flags.set(key, true);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { command: positional[0] ?? 'help', flags };
|
|
81
|
+
}
|
|
82
|
+
function str(args, key) {
|
|
83
|
+
const value = args.flags.get(key);
|
|
84
|
+
return typeof value === 'string' ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
function num(args, key) {
|
|
87
|
+
const value = str(args, key);
|
|
88
|
+
if (value === undefined)
|
|
89
|
+
return undefined;
|
|
90
|
+
const parsed = Number(value);
|
|
91
|
+
if (!Number.isFinite(parsed))
|
|
92
|
+
throw new UsageError(`--${key} 需要一个数字,收到 "${value}"`);
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
function bool(args, key) {
|
|
96
|
+
return args.flags.has(key);
|
|
97
|
+
}
|
|
98
|
+
class UsageError extends Error {
|
|
99
|
+
}
|
|
100
|
+
function resolveSessionId(args) {
|
|
101
|
+
const explicit = str(args, 'session');
|
|
102
|
+
if (explicit)
|
|
103
|
+
return explicit;
|
|
104
|
+
const fromEnv = process.env.DSH_SESSION_ID;
|
|
105
|
+
if (fromEnv)
|
|
106
|
+
return fromEnv;
|
|
107
|
+
throw new UsageError('取不到当前会话 id。dsh 的模型 shell 环境会自动注入 DSH_SESSION_ID;'
|
|
108
|
+
+ '如果你是在普通终端里手动跑,请用 --session <id> 指定(`dsh-bridge doctor` 会列出候选)。');
|
|
109
|
+
}
|
|
110
|
+
function tierOf(args) {
|
|
111
|
+
const value = str(args, 'tier') ?? 'pro';
|
|
112
|
+
if (value !== 'flash' && value !== 'current' && value !== 'pro') {
|
|
113
|
+
throw new UsageError(`--tier 只能是 flash / current / pro,收到 "${value}"`);
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
function langOf(args) {
|
|
118
|
+
const value = str(args, 'lang') ?? 'auto';
|
|
119
|
+
if (value !== 'zh' && value !== 'en' && value !== 'auto') {
|
|
120
|
+
throw new UsageError(`--lang 只能是 zh / en / auto,收到 "${value}"`);
|
|
121
|
+
}
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
function injectOf(args) {
|
|
125
|
+
const value = str(args, 'inject') ?? 'both';
|
|
126
|
+
if (value !== 'goal' && value !== 'prompt' && value !== 'both') {
|
|
127
|
+
throw new UsageError(`--inject 只能是 goal / prompt / both,收到 "${value}"`);
|
|
128
|
+
}
|
|
129
|
+
return value;
|
|
130
|
+
}
|
|
131
|
+
function requireTarget(args) {
|
|
132
|
+
const to = str(args, 'to');
|
|
133
|
+
if (!to)
|
|
134
|
+
throw new UsageError('缺少 --to <preset>。先跑 `dsh-bridge presets` 看有哪些模式。');
|
|
135
|
+
return to;
|
|
136
|
+
}
|
|
137
|
+
function writeSummaryFile(sessionId, summary) {
|
|
138
|
+
const dir = mkdtempSync(join(tmpdir(), 'dsh-bridge-'));
|
|
139
|
+
const file = join(dir, `summary-${sessionId.slice(0, 12)}.md`);
|
|
140
|
+
writeFileSync(file, summary, 'utf8');
|
|
141
|
+
return file;
|
|
142
|
+
}
|
|
143
|
+
/* ------------------------------------------------------------------ 命令 */
|
|
144
|
+
async function main(argv) {
|
|
145
|
+
const args = parseArgs(argv);
|
|
146
|
+
if (args.command === 'help' || bool(args, 'help')) {
|
|
147
|
+
process.stdout.write(HELP);
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
const json = bool(args, 'json');
|
|
151
|
+
const quiet = bool(args, 'quiet') || json;
|
|
152
|
+
const api = resolveApiBase(str(args, 'api'));
|
|
153
|
+
const rpc = createRpc({ api, prefix: 'bridge-cli' });
|
|
154
|
+
const progress = (message) => {
|
|
155
|
+
if (!quiet)
|
|
156
|
+
process.stderr.write(`… ${message}\n`);
|
|
157
|
+
};
|
|
158
|
+
const out = (value, human) => {
|
|
159
|
+
process.stdout.write(json ? `${JSON.stringify(value, null, 2)}\n` : human());
|
|
160
|
+
};
|
|
161
|
+
switch (args.command) {
|
|
162
|
+
case 'doctor': {
|
|
163
|
+
const report = { api, sessionIdFromEnv: process.env.DSH_SESSION_ID ?? null };
|
|
164
|
+
try {
|
|
165
|
+
const presets = await listPresets(rpc);
|
|
166
|
+
report.gateway = 'ok';
|
|
167
|
+
report.presets = presets.map((p) => p.id);
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
report.gateway = error instanceof Error ? error.message : String(error);
|
|
171
|
+
}
|
|
172
|
+
const sessionId = str(args, 'session') ?? process.env.DSH_SESSION_ID;
|
|
173
|
+
if (sessionId) {
|
|
174
|
+
const row = await findSession(rpc, sessionId).catch(() => undefined);
|
|
175
|
+
report.session = row ? { sessionId: row.sessionId, agentPreset: row.agentPreset, cwd: row.cwd } : 'not-found';
|
|
176
|
+
}
|
|
177
|
+
out(report, () => {
|
|
178
|
+
const lines = [`网关:${api} — ${String(report.gateway)}`];
|
|
179
|
+
lines.push(`会话 id:${String(report.sessionIdFromEnv ?? '(环境变量 DSH_SESSION_ID 未设置)')}`);
|
|
180
|
+
if (report.session)
|
|
181
|
+
lines.push(`会话:${JSON.stringify(report.session)}`);
|
|
182
|
+
if (report.presets)
|
|
183
|
+
lines.push(`可用模式:${report.presets.join(' / ')}`);
|
|
184
|
+
return `${lines.join('\n')}\n`;
|
|
185
|
+
});
|
|
186
|
+
return report.gateway === 'ok' ? 0 : 1;
|
|
187
|
+
}
|
|
188
|
+
case 'presets': {
|
|
189
|
+
const sessionId = str(args, 'session') ?? process.env.DSH_SESSION_ID;
|
|
190
|
+
const current = sessionId ? (await findSession(rpc, sessionId).catch(() => undefined))?.agentPreset : undefined;
|
|
191
|
+
const presets = await listPresets(rpc);
|
|
192
|
+
const rows = presets.map((p) => ({ ...p, current: p.id === current }));
|
|
193
|
+
out({ current, presets: rows }, () => {
|
|
194
|
+
const lines = rows.map((p) => {
|
|
195
|
+
const mark = p.current ? '(当前)' : '';
|
|
196
|
+
const name = p.name ? ` ${p.name}` : '';
|
|
197
|
+
const desc = p.description ? ` — ${p.description}` : '';
|
|
198
|
+
return ` ${p.id}${name}${mark}${desc}`;
|
|
199
|
+
});
|
|
200
|
+
return `可迁入的模式:\n${lines.join('\n')}\n`;
|
|
201
|
+
});
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
case 'preview': {
|
|
205
|
+
const sessionId = resolveSessionId(args);
|
|
206
|
+
const to = str(args, 'to');
|
|
207
|
+
const result = await previewMigration(rpc, {
|
|
208
|
+
sessionId,
|
|
209
|
+
tier: tierOf(args),
|
|
210
|
+
provider: str(args, 'provider'),
|
|
211
|
+
model: str(args, 'model'),
|
|
212
|
+
sourceCharBudget: num(args, 'source-budget'),
|
|
213
|
+
summaryCharBudget: num(args, 'summary-budget'),
|
|
214
|
+
lang: langOf(args),
|
|
215
|
+
...(num(args, 'poll-ms') === undefined ? {} : { pollMs: num(args, 'poll-ms') }),
|
|
216
|
+
...(num(args, 'worker-timeout') === undefined ? {} : { workerTimeoutMs: num(args, 'worker-timeout') }),
|
|
217
|
+
onProgress: progress,
|
|
218
|
+
});
|
|
219
|
+
const file = writeSummaryFile(sessionId, result.summary);
|
|
220
|
+
const cost = estimateSummaryTokens(result.source.text.length, {
|
|
221
|
+
summaryCharBudget: num(args, 'summary-budget'),
|
|
222
|
+
});
|
|
223
|
+
out({
|
|
224
|
+
summary: result.summary,
|
|
225
|
+
summaryFile: file,
|
|
226
|
+
lang: result.lang,
|
|
227
|
+
worker: result.worker,
|
|
228
|
+
capped: result.capped,
|
|
229
|
+
source: {
|
|
230
|
+
chars: result.source.text.length,
|
|
231
|
+
userMessagesUsed: result.source.userMessagesUsed,
|
|
232
|
+
userMessagesTotal: result.source.userMessagesTotal,
|
|
233
|
+
reusedCompaction: result.source.reusedCompaction,
|
|
234
|
+
truncated: result.source.truncated,
|
|
235
|
+
dropped: result.source.dropped,
|
|
236
|
+
visualEvidence: result.source.visualEvidence,
|
|
237
|
+
},
|
|
238
|
+
estimatedTokens: cost,
|
|
239
|
+
nextCommand: `dsh-bridge migrate --to ${to ?? '<preset>'} --summary-file ${file}`,
|
|
240
|
+
}, () => {
|
|
241
|
+
const s = result.source;
|
|
242
|
+
const lines = [
|
|
243
|
+
'',
|
|
244
|
+
'─── 交接摘要(请人工过目,尤其是数字与路径)───',
|
|
245
|
+
result.summary,
|
|
246
|
+
'─────────────────────────────────────────────',
|
|
247
|
+
`取材:${s.text.length} 字符 · 用户消息 ${s.userMessagesUsed}/${s.userMessagesTotal} 条`
|
|
248
|
+
+ `${s.reusedCompaction ? ' · 复用了 compaction 底稿' : ''}`,
|
|
249
|
+
];
|
|
250
|
+
if (s.truncated)
|
|
251
|
+
lines.push(`⚠ 取材因预算被裁剪,受影响分区:${s.dropped.join(' / ')}`);
|
|
252
|
+
if (s.visualEvidence.images) {
|
|
253
|
+
lines.push(`图片:${s.visualEvidence.images} 张 / ${s.visualEvidence.imageMessages} 条消息`
|
|
254
|
+
+ ` · 有关联原文 ${s.visualEvidence.represented} 条 · 未解析 ${s.visualEvidence.unresolved} 条`);
|
|
255
|
+
}
|
|
256
|
+
if (result.capped)
|
|
257
|
+
lines.push('⚠ 压缩工人超时被取消,摘要按已产出文本计');
|
|
258
|
+
lines.push(`压缩模型:${result.worker.model || '(会话默认)'}(${result.worker.reason})`);
|
|
259
|
+
lines.push(`摘要已写入:${file}`);
|
|
260
|
+
lines.push(`确认无误后执行:dsh-bridge migrate --to ${to ?? '<preset>'} --summary-file ${file}`);
|
|
261
|
+
lines.push('');
|
|
262
|
+
return `${lines.join('\n')}\n`;
|
|
263
|
+
});
|
|
264
|
+
return 0;
|
|
265
|
+
}
|
|
266
|
+
case 'migrate': {
|
|
267
|
+
const sessionId = resolveSessionId(args);
|
|
268
|
+
const to = requireTarget(args);
|
|
269
|
+
const file = str(args, 'summary-file');
|
|
270
|
+
if (!file)
|
|
271
|
+
throw new UsageError('缺少 --summary-file <path>(先跑 `dsh-bridge preview --to ' + to + '`)。');
|
|
272
|
+
let summary;
|
|
273
|
+
try {
|
|
274
|
+
summary = readFileSync(file, 'utf8');
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
throw new UsageError(`读不到摘要文件 ${file}:${error instanceof Error ? error.message : String(error)}`);
|
|
278
|
+
}
|
|
279
|
+
const source = await findSession(rpc, sessionId).catch(() => undefined);
|
|
280
|
+
const result = await executeMigration(rpc, {
|
|
281
|
+
sessionId,
|
|
282
|
+
to,
|
|
283
|
+
summary,
|
|
284
|
+
goalRounds: num(args, 'goal-rounds'),
|
|
285
|
+
inject: injectOf(args),
|
|
286
|
+
kickoff: !bool(args, 'no-kickoff'),
|
|
287
|
+
autoContinue: bool(args, 'continue'),
|
|
288
|
+
title: str(args, 'title') ?? migratedTitle(titleOf(source), to),
|
|
289
|
+
onProgress: progress,
|
|
290
|
+
});
|
|
291
|
+
out(result, () => {
|
|
292
|
+
const lines = [`已在 ${result.agentPreset} 模式下建好新会话:${result.sessionId}`];
|
|
293
|
+
if (result.kickoffSent) {
|
|
294
|
+
lines.push(bool(args, 'continue')
|
|
295
|
+
? '交接目标已暂停;新会话会在同一轮复述并继续,不触发额外 goal 轮次。'
|
|
296
|
+
: '交接目标已暂停;新会话复述后等待你确认。');
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
lines.push('新会话没有自动启动;请检查警告后手动继续。');
|
|
300
|
+
}
|
|
301
|
+
if (result.imagesSent)
|
|
302
|
+
lines.push(`已把 ${result.imagesSent} 张尚未解析的原图附到目标 kickoff。`);
|
|
303
|
+
lines.push('原会话原封不动,随时可以点回去。');
|
|
304
|
+
for (const warning of result.warnings)
|
|
305
|
+
lines.push(`⚠ ${warning}`);
|
|
306
|
+
return `${lines.join('\n')}\n`;
|
|
307
|
+
});
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
case 'run': {
|
|
311
|
+
const sessionId = resolveSessionId(args);
|
|
312
|
+
const to = requireTarget(args);
|
|
313
|
+
const preview = await previewMigration(rpc, {
|
|
314
|
+
sessionId,
|
|
315
|
+
tier: tierOf(args),
|
|
316
|
+
provider: str(args, 'provider'),
|
|
317
|
+
model: str(args, 'model'),
|
|
318
|
+
sourceCharBudget: num(args, 'source-budget'),
|
|
319
|
+
summaryCharBudget: num(args, 'summary-budget'),
|
|
320
|
+
lang: langOf(args),
|
|
321
|
+
...(num(args, 'poll-ms') === undefined ? {} : { pollMs: num(args, 'poll-ms') }),
|
|
322
|
+
...(num(args, 'worker-timeout') === undefined ? {} : { workerTimeoutMs: num(args, 'worker-timeout') }),
|
|
323
|
+
onProgress: progress,
|
|
324
|
+
});
|
|
325
|
+
const source = await findSession(rpc, sessionId).catch(() => undefined);
|
|
326
|
+
const result = await executeMigration(rpc, {
|
|
327
|
+
sessionId,
|
|
328
|
+
to,
|
|
329
|
+
summary: preview.summary,
|
|
330
|
+
lang: preview.lang,
|
|
331
|
+
goalRounds: num(args, 'goal-rounds'),
|
|
332
|
+
inject: injectOf(args),
|
|
333
|
+
kickoff: !bool(args, 'no-kickoff'),
|
|
334
|
+
autoContinue: bool(args, 'continue'),
|
|
335
|
+
title: str(args, 'title') ?? migratedTitle(titleOf(source), to),
|
|
336
|
+
onProgress: progress,
|
|
337
|
+
});
|
|
338
|
+
out({ ...result, summary: preview.summary, source: preview.source }, () => `已迁移到 ${result.agentPreset}:${result.sessionId}\n\n${preview.summary}\n`);
|
|
339
|
+
return 0;
|
|
340
|
+
}
|
|
341
|
+
case 'worker-model': {
|
|
342
|
+
// 诊断用:只回答「档位会挑到哪个模型」,不建任何会话。
|
|
343
|
+
const sessionId = resolveSessionId(args);
|
|
344
|
+
const route = await resolveWorkerModel(rpc, sessionId, tierOf(args), {
|
|
345
|
+
provider: str(args, 'provider'),
|
|
346
|
+
model: str(args, 'model'),
|
|
347
|
+
});
|
|
348
|
+
out(route, () => `${route.provider} / ${route.model}(${route.reason})\n`);
|
|
349
|
+
return 0;
|
|
350
|
+
}
|
|
351
|
+
default:
|
|
352
|
+
process.stderr.write(`未知命令 "${args.command}"\n\n${HELP}`);
|
|
353
|
+
return 2;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const exitCode = await main(process.argv.slice(2)).catch((error) => {
|
|
357
|
+
if (error instanceof UsageError) {
|
|
358
|
+
process.stderr.write(`${error.message}\n`);
|
|
359
|
+
return 2;
|
|
360
|
+
}
|
|
361
|
+
if (error instanceof RpcError) {
|
|
362
|
+
process.stderr.write(`${error.message}\n`);
|
|
363
|
+
return 1;
|
|
364
|
+
}
|
|
365
|
+
process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
366
|
+
return 1;
|
|
367
|
+
});
|
|
368
|
+
process.exit(exitCode);
|
package/lib/command.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/bridge` —— 人发起的跨 preset 迁移命令。
|
|
3
|
+
*
|
|
4
|
+
* 这是本插件的主入口,形状对齐上游 `dsh-plan-mode` 的 `/plan`:命令由 UI 直接
|
|
5
|
+
* 派发给注册表,**不经过模型**,命令结果也不进模型历史。于是
|
|
6
|
+
*
|
|
7
|
+
* - 不需要模型「愿意」加载什么东西,也不需要 bash 或环境变量;
|
|
8
|
+
* - `minimal` 这种没有 skill 工具的 preset 照样能发起迁移;
|
|
9
|
+
* - 结果文本只给人看,原会话的上下文一个字都不动。
|
|
10
|
+
*
|
|
11
|
+
* 上游 `session.prompt` 的契约保证了这一点:「A prompt whose content is exactly
|
|
12
|
+
* one text block starting with '/' is a slash command: the host executes it
|
|
13
|
+
* through the command registry (mode-agnostic) and it is never sent to the
|
|
14
|
+
* model.」——所以在官方 WebUI 的输入框里打 `/bridge code` 就能用。
|
|
15
|
+
*/
|
|
16
|
+
import { type InjectMode, type Lang, type ModelTier } from './migrate.ts';
|
|
17
|
+
import type { MethodProbe } from './api-rpc.ts';
|
|
18
|
+
import { type Rpc } from './rpc.ts';
|
|
19
|
+
/** 命令处理器从注册表拿到的东西(结构化声明,不 import 上游类型)。 */
|
|
20
|
+
export interface BridgeInvocation {
|
|
21
|
+
agent?: {
|
|
22
|
+
session?: {
|
|
23
|
+
id?: string;
|
|
24
|
+
header?: {
|
|
25
|
+
id?: string;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
rawInput?: string;
|
|
30
|
+
/**
|
|
31
|
+
* rc.8 起注册表会传这个字段(随命令提交的图片块)。`/bridge` 没有声明
|
|
32
|
+
* `input.images`,所以带图片的调用会在进入这里之前就被注册表挡下来;
|
|
33
|
+
* 声明出来只是为了让类型如实反映上游传了什么。
|
|
34
|
+
*/
|
|
35
|
+
attachments?: readonly unknown[];
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}
|
|
38
|
+
export type BridgeResult = {
|
|
39
|
+
kind: 'success';
|
|
40
|
+
text?: string;
|
|
41
|
+
} | {
|
|
42
|
+
kind: 'error';
|
|
43
|
+
text: string;
|
|
44
|
+
};
|
|
45
|
+
export interface BridgeCommandConfig {
|
|
46
|
+
modelTier: ModelTier;
|
|
47
|
+
sourceCharBudget: number;
|
|
48
|
+
summaryCharBudget: number;
|
|
49
|
+
goalRounds: number;
|
|
50
|
+
inject: InjectMode;
|
|
51
|
+
lang: Lang;
|
|
52
|
+
workerProvider?: string;
|
|
53
|
+
workerModel?: string;
|
|
54
|
+
/** 命令路径下等压缩工人的上限。命令是同步返回的,不能等太久。 */
|
|
55
|
+
previewTimeoutMs: number;
|
|
56
|
+
}
|
|
57
|
+
export interface BridgeCommandDeps {
|
|
58
|
+
/** 按本次调用的取消信号建一个 Rpc。 */
|
|
59
|
+
rpcFor: (signal?: AbortSignal) => Rpc;
|
|
60
|
+
/** 自检:这套 host 的网关面还是不是插件预期的形状。 */
|
|
61
|
+
probe?: () => MethodProbe[];
|
|
62
|
+
config: BridgeCommandConfig;
|
|
63
|
+
/** 摘要落盘,返回路径;给「改完再执行」这条路用。失败返回 undefined。 */
|
|
64
|
+
writeSummary?: (sessionId: string, summary: string) => string | undefined;
|
|
65
|
+
readSummary?: (path: string) => string;
|
|
66
|
+
now?: () => number;
|
|
67
|
+
}
|
|
68
|
+
interface ParsedInput {
|
|
69
|
+
preset?: string;
|
|
70
|
+
go: boolean;
|
|
71
|
+
doctor: boolean;
|
|
72
|
+
autoContinue: boolean;
|
|
73
|
+
tier?: ModelTier;
|
|
74
|
+
lang?: Lang;
|
|
75
|
+
inject?: InjectMode;
|
|
76
|
+
goalRounds?: number;
|
|
77
|
+
file?: string;
|
|
78
|
+
help: boolean;
|
|
79
|
+
error?: string;
|
|
80
|
+
}
|
|
81
|
+
/** `<preset> [--go] [--continue] [--tier x] [--lang l] [--inject m] [--goal-rounds n] [--file p]` */
|
|
82
|
+
export declare function parseBridgeInput(rawInput: string): ParsedInput;
|
|
83
|
+
/** 建一个 `/bridge` 命令定义。返回值形状对齐上游 `CommandDefinition`。 */
|
|
84
|
+
export declare function createBridgeCommand(deps: BridgeCommandDeps): {
|
|
85
|
+
name: string;
|
|
86
|
+
description: string;
|
|
87
|
+
input: {
|
|
88
|
+
hint: string;
|
|
89
|
+
};
|
|
90
|
+
handler: (invocation: BridgeInvocation) => Promise<BridgeResult>;
|
|
91
|
+
};
|
|
92
|
+
export {};
|