dsh-plugin-bridge 0.2.10 → 0.3.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 +18 -11
- package/README.zh.md +18 -11
- package/docs/design.md +5 -3
- package/docs/guide.zh.md +13 -3
- package/docs/native-webui-feasibility.md +54 -28
- package/lib/api-rpc.d.ts +5 -2
- package/lib/api-rpc.js +10 -2
- package/lib/cli.js +15 -12
- package/lib/client-contract.d.ts +87 -0
- package/lib/client-contract.js +487 -0
- package/lib/client.d.ts +15 -0
- package/lib/client.js +1247 -0
- package/lib/client.js.map +1 -0
- package/lib/command.d.ts +10 -20
- package/lib/command.js +142 -18
- package/lib/host.d.ts +177 -0
- package/lib/host.js +102 -0
- package/lib/index.js +2 -2
- package/lib/migrate.d.ts +26 -43
- package/lib/migrate.js +119 -75
- package/package.json +48 -3
- package/reports/native-workbench-2026-08-25.md +30 -0
- package/reports/native-workbench-2026-08-25.raw.json +34 -0
package/lib/migrate.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 迁移编排:取材 → 压缩工人 → 目标会话。
|
|
3
3
|
*
|
|
4
|
-
* 只依赖注入进来的 `
|
|
5
|
-
*
|
|
4
|
+
* 只依赖注入进来的 `BridgeHost`,不碰 process / argv / 文件系统,所以可以对着
|
|
5
|
+
* 任意宿主 adapter(见 test/migrate.test.mjs)跑完整条链路而不烧任何 token。
|
|
6
6
|
* CLI(`cli.ts`)与客户端 GUI 都消费这里的函数,保证「被验证的」和
|
|
7
7
|
* 「被交付的」是同一条代码路径。
|
|
8
8
|
*/
|
|
9
9
|
import { buildBridgeInstruction, buildBridgeKickoff, buildBridgeSource, appendVisualEvidence, collectVisualEvidence, detectLang, } from './compression.js';
|
|
10
10
|
import { foldSessionEvents } from './fold.js';
|
|
11
|
+
import { asBridgeHost, missingHostCapability, } from './host.js';
|
|
11
12
|
import { RpcError, sleep } from './rpc.js';
|
|
12
13
|
/** 工人会话优先使用的 preset(工具越少越省,也不会误动工作区)。 */
|
|
13
14
|
const WORKER_PRESET_PREFERENCE = ['minimal', 'standard'];
|
|
@@ -21,11 +22,12 @@ const HISTORY_MAX_PAGES = 4;
|
|
|
21
22
|
* `session.list` 的 v1 一次返回全部,`cursor` 是预留位;这里仍按 cursor 取完,
|
|
22
23
|
* 免得上游哪天真的分页之后这里悄悄只看第一页。
|
|
23
24
|
*/
|
|
24
|
-
export async function listSessions(
|
|
25
|
+
export async function listSessions(input) {
|
|
26
|
+
const host = asBridgeHost(input);
|
|
25
27
|
const rows = [];
|
|
26
28
|
let cursor;
|
|
27
29
|
for (let page = 0; page < 20; page += 1) {
|
|
28
|
-
const res = await
|
|
30
|
+
const res = await host.sessions.list(cursor === undefined ? {} : { cursor });
|
|
29
31
|
rows.push(...(res.items ?? []));
|
|
30
32
|
if (!res.nextCursor)
|
|
31
33
|
break;
|
|
@@ -33,22 +35,24 @@ export async function listSessions(rpc) {
|
|
|
33
35
|
}
|
|
34
36
|
return rows;
|
|
35
37
|
}
|
|
36
|
-
export async function findSession(
|
|
37
|
-
return (await listSessions(
|
|
38
|
+
export async function findSession(host, sessionId) {
|
|
39
|
+
return (await listSessions(host)).find((row) => row.sessionId === sessionId);
|
|
38
40
|
}
|
|
39
41
|
/** 找出会话所属工作区;找不到就返回 undefined(调用方退回用 cwd 建会话)。 */
|
|
40
|
-
export async function findWorkspaceId(
|
|
41
|
-
const
|
|
42
|
+
export async function findWorkspaceId(input, sessionId) {
|
|
43
|
+
const host = asBridgeHost(input);
|
|
44
|
+
const res = await host.workspaces.list();
|
|
42
45
|
return res.items?.find((ws) => ws.sessionIds?.includes(sessionId))?.workspaceId;
|
|
43
46
|
}
|
|
44
47
|
/** 可作为迁移目标的 preset(去掉 broken 的)。 */
|
|
45
|
-
export async function listPresets(
|
|
46
|
-
const
|
|
48
|
+
export async function listPresets(input) {
|
|
49
|
+
const host = asBridgeHost(input);
|
|
50
|
+
const res = await host.presets.list();
|
|
47
51
|
return (res.presets ?? []).filter((preset) => preset.broken === undefined);
|
|
48
52
|
}
|
|
49
53
|
/** 新会话的落点:优先同工作区,否则同 cwd,再否则交给 host 默认。 */
|
|
50
|
-
async function placement(
|
|
51
|
-
const workspaceId = await findWorkspaceId(
|
|
54
|
+
async function placement(host, source, sessionId) {
|
|
55
|
+
const workspaceId = await findWorkspaceId(host, sessionId).catch(() => undefined);
|
|
52
56
|
if (workspaceId)
|
|
53
57
|
return { workspaceId };
|
|
54
58
|
if (source?.cwd)
|
|
@@ -62,11 +66,12 @@ async function placement(rpc, source, sessionId) {
|
|
|
62
66
|
* 里挑。挑不到就退回源会话当前模型——档位是省钱偏好,不该成为换 provider 的人
|
|
63
67
|
* 装不上的理由。
|
|
64
68
|
*/
|
|
65
|
-
export async function resolveWorkerModel(
|
|
69
|
+
export async function resolveWorkerModel(input, sessionId, tier, override = {}) {
|
|
70
|
+
const host = asBridgeHost(input);
|
|
66
71
|
if (override.provider && override.model) {
|
|
67
72
|
return { provider: override.provider, model: override.model, reason: 'configured' };
|
|
68
73
|
}
|
|
69
|
-
const models = await
|
|
74
|
+
const models = await host.sessions.models({ sessionId });
|
|
70
75
|
const current = models.current;
|
|
71
76
|
const fallback = {
|
|
72
77
|
provider: override.provider ?? current?.provider ?? '',
|
|
@@ -86,8 +91,8 @@ export async function resolveWorkerModel(rpc, sessionId, tier, override = {}) {
|
|
|
86
91
|
return fallback;
|
|
87
92
|
}
|
|
88
93
|
/** 压缩工人用哪个 preset:优先 minimal,其次 standard,再否则 host 默认。 */
|
|
89
|
-
export async function resolveWorkerPreset(
|
|
90
|
-
const presets = await listPresets(
|
|
94
|
+
export async function resolveWorkerPreset(input) {
|
|
95
|
+
const presets = await listPresets(input).catch(() => []);
|
|
91
96
|
for (const wanted of WORKER_PRESET_PREFERENCE) {
|
|
92
97
|
if (presets.some((preset) => preset.id === wanted))
|
|
93
98
|
return wanted;
|
|
@@ -95,39 +100,51 @@ export async function resolveWorkerPreset(rpc) {
|
|
|
95
100
|
return presets.find((preset) => preset.isDefault)?.id;
|
|
96
101
|
}
|
|
97
102
|
/**
|
|
98
|
-
*
|
|
103
|
+
* 等一个会话的新一轮写入 `turn/end`。
|
|
99
104
|
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
105
|
+
* `session.list` 是全局列表,拿它每两秒轮询一个 worker 会把会话总量放大成
|
|
106
|
+
* O(会话数 × 轮询次数)。`session.history` 则只读目标会话;用 prompt 前的事件
|
|
107
|
+
* 水位隔开旧轮次后,`turn/start` / `turn/end` 也比易过期的 running 快照更可靠。
|
|
103
108
|
*/
|
|
104
|
-
export async function waitIdle(
|
|
109
|
+
export async function waitIdle(input, sessionId, options = {}) {
|
|
110
|
+
const host = asBridgeHost(input);
|
|
105
111
|
const pollMs = options.pollMs ?? 2000;
|
|
106
112
|
const deadline = Date.now() + (options.timeoutMs ?? 360_000);
|
|
107
113
|
const startBy = Date.now() + (options.startGraceMs ?? 25_000);
|
|
114
|
+
const afterSeq = options.afterSeq ?? 0;
|
|
108
115
|
let started = false;
|
|
109
116
|
while (Date.now() < deadline) {
|
|
110
117
|
await sleep(pollMs);
|
|
111
|
-
const
|
|
112
|
-
|
|
118
|
+
const events = await tailSessionEvents(host, sessionId).catch(() => []);
|
|
119
|
+
const fresh = events.filter((event) => typeof event.seq === 'number' && event.seq > afterSeq);
|
|
120
|
+
if (fresh.some((event) => event.type === 'turn/start'))
|
|
113
121
|
started = true;
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
if (started)
|
|
122
|
+
if (fresh.some((event) => event.type === 'turn/end'))
|
|
117
123
|
return { idle: true, started: true };
|
|
118
|
-
if (Date.now() > startBy)
|
|
124
|
+
if (!started && Date.now() > startBy)
|
|
119
125
|
return { idle: true, started: false };
|
|
120
126
|
}
|
|
121
127
|
return { idle: false, started };
|
|
122
128
|
}
|
|
129
|
+
/** 读取单个会话的尾页;不触发全局 session.list 扫描。 */
|
|
130
|
+
async function tailSessionEvents(input, sessionId) {
|
|
131
|
+
const host = asBridgeHost(input);
|
|
132
|
+
const res = await host.sessions.history({ sessionId, maxMessages: 2 }, { timeoutMs: 60_000 });
|
|
133
|
+
return (res.events ?? []).map((entry) => entry.event);
|
|
134
|
+
}
|
|
135
|
+
async function latestSessionSeq(host, sessionId) {
|
|
136
|
+
const events = await tailSessionEvents(host, sessionId);
|
|
137
|
+
return events.reduce((max, event) => (typeof event.seq === 'number' && event.seq > max ? event.seq : max), 0);
|
|
138
|
+
}
|
|
123
139
|
/** 拉取并折叠会话历史(按需翻页)。 */
|
|
124
|
-
export async function foldedHistory(
|
|
140
|
+
export async function foldedHistory(input, sessionId, options = {}) {
|
|
141
|
+
const host = asBridgeHost(input);
|
|
125
142
|
const pageMessages = options.pageMessages ?? HISTORY_PAGE_MESSAGES;
|
|
126
143
|
const maxPages = options.maxPages ?? HISTORY_MAX_PAGES;
|
|
127
144
|
let events = [];
|
|
128
145
|
let beforeSeq;
|
|
129
146
|
for (let page = 0; page < maxPages; page += 1) {
|
|
130
|
-
const res = await
|
|
147
|
+
const res = await host.sessions.history({ sessionId, maxMessages: pageMessages, ...(beforeSeq === undefined ? {} : { beforeSeq }) }, { timeoutMs: 60_000 });
|
|
131
148
|
const chunk = (res.events ?? []).map((entry) => entry.event);
|
|
132
149
|
if (!chunk.length)
|
|
133
150
|
break;
|
|
@@ -142,59 +159,62 @@ export async function foldedHistory(rpc, sessionId, options = {}) {
|
|
|
142
159
|
return foldSessionEvents(events);
|
|
143
160
|
}
|
|
144
161
|
/** 会话里最后一条非空 assistant 文本。 */
|
|
145
|
-
export async function lastAssistantText(
|
|
146
|
-
const messages = await foldedHistory(
|
|
162
|
+
export async function lastAssistantText(host, sessionId) {
|
|
163
|
+
const messages = await foldedHistory(host, sessionId, { pageMessages: 20, maxPages: 1 });
|
|
147
164
|
return [...messages].reverse().find((m) => m.role === 'assistant' && m.content.trim())?.content.trim() ?? '';
|
|
148
165
|
}
|
|
149
166
|
/** 生成交接摘要:取材 → 起临时工人 → 收摘要 → 归档工人。 */
|
|
150
|
-
export async function previewMigration(
|
|
167
|
+
export async function previewMigration(input, options) {
|
|
168
|
+
const host = asBridgeHost(input);
|
|
151
169
|
const progress = options.onProgress ?? (() => { });
|
|
152
|
-
const sourceSession = await findSession(
|
|
170
|
+
const sourceSession = options.sourceSession ?? await findSession(host, options.sessionId);
|
|
153
171
|
progress('拉取并折叠会话历史…');
|
|
154
|
-
const messages = await foldedHistory(
|
|
172
|
+
const messages = await foldedHistory(host, options.sessionId);
|
|
155
173
|
const source = buildBridgeSource(messages, { sourceCharBudget: options.sourceCharBudget });
|
|
156
174
|
if (!source.text.trim()) {
|
|
157
175
|
throw new RpcError('bridge.preview', 'empty-source', '这个会话还没有可迁移的内容(取材为空)。直接开一个新会话更省事。');
|
|
158
176
|
}
|
|
159
177
|
const lang = options.lang && options.lang !== 'auto' ? options.lang : detectLang(source.text);
|
|
160
178
|
const tier = options.tier ?? 'pro';
|
|
161
|
-
const route = await resolveWorkerModel(
|
|
179
|
+
const route = await resolveWorkerModel(host, options.sessionId, tier, options);
|
|
162
180
|
if (options.dryRun) {
|
|
163
181
|
return { summary: '', source, lang, worker: { ...route }, capped: false, sourceSession };
|
|
164
182
|
}
|
|
165
|
-
const preset = await resolveWorkerPreset(
|
|
166
|
-
const where = await placement(
|
|
183
|
+
const preset = await resolveWorkerPreset(host);
|
|
184
|
+
const where = await placement(host, sourceSession, options.sessionId);
|
|
167
185
|
progress(`起压缩工人(${preset ?? '默认 preset'} / ${route.model || '会话默认模型'})…`);
|
|
168
|
-
const worker = await
|
|
186
|
+
const worker = await host.sessions.create({
|
|
169
187
|
...where,
|
|
170
188
|
...(preset === undefined ? {} : { agentPreset: preset }),
|
|
171
189
|
});
|
|
172
190
|
let capped = false;
|
|
173
191
|
try {
|
|
174
192
|
if (route.provider && route.model) {
|
|
175
|
-
await
|
|
193
|
+
await host.sessions.selectModel({ sessionId: worker.sessionId, provider: route.provider, model: route.model })
|
|
176
194
|
.catch((error) => {
|
|
177
195
|
// 选模型失败不该让整次迁移失败:工人用会话默认模型照样能写摘要。
|
|
178
196
|
progress(`选模型失败,改用默认模型(${error instanceof Error ? error.message : String(error)})`);
|
|
179
197
|
});
|
|
180
198
|
}
|
|
181
199
|
const instruction = buildBridgeInstruction(lang, { summaryCharBudget: options.summaryCharBudget });
|
|
182
|
-
await
|
|
200
|
+
const workerBaselineSeq = await latestSessionSeq(host, worker.sessionId).catch(() => 0);
|
|
201
|
+
await host.sessions.prompt({
|
|
183
202
|
sessionId: worker.sessionId,
|
|
184
203
|
mode: 'queue',
|
|
185
204
|
content: [{ type: 'text', text: `${instruction}${source.text}` }],
|
|
186
205
|
});
|
|
187
206
|
progress('等待摘要…');
|
|
188
|
-
const settled = await waitIdle(
|
|
207
|
+
const settled = await waitIdle(host, worker.sessionId, {
|
|
189
208
|
timeoutMs: options.workerTimeoutMs ?? 360_000,
|
|
209
|
+
afterSeq: workerBaselineSeq,
|
|
190
210
|
...(options.pollMs === undefined ? {} : { pollMs: options.pollMs, startGraceMs: options.pollMs * 6 }),
|
|
191
211
|
});
|
|
192
212
|
if (!settled.idle) {
|
|
193
213
|
capped = true;
|
|
194
|
-
await
|
|
214
|
+
await host.sessions.cancel({ sessionId: worker.sessionId }).catch(() => undefined);
|
|
195
215
|
await sleep(2500);
|
|
196
216
|
}
|
|
197
|
-
const workerSummary = await lastAssistantText(
|
|
217
|
+
const workerSummary = await lastAssistantText(host, worker.sessionId);
|
|
198
218
|
if (!workerSummary) {
|
|
199
219
|
throw new RpcError('bridge.preview', 'worker-empty', '压缩工人没有产出摘要(可能是模型不可用或被取消)。');
|
|
200
220
|
}
|
|
@@ -210,7 +230,7 @@ export async function previewMigration(rpc, options) {
|
|
|
210
230
|
}
|
|
211
231
|
finally {
|
|
212
232
|
// 工人是一次性的:无论成败都归档,不在侧栏留垃圾。
|
|
213
|
-
await
|
|
233
|
+
await host.workspaces.archiveSession({ sessionId: worker.sessionId }).catch(() => undefined);
|
|
214
234
|
}
|
|
215
235
|
}
|
|
216
236
|
/** 找出没有关联助手正文的图片引用;已有逐字视觉证据时默认不重复烧视觉 token。 */
|
|
@@ -232,14 +252,17 @@ function unresolvedImageRefs(messages) {
|
|
|
232
252
|
}
|
|
233
253
|
return { refs, missing };
|
|
234
254
|
}
|
|
235
|
-
async function readPromptImages(
|
|
255
|
+
async function readPromptImages(host, sourceSessionId, refs) {
|
|
256
|
+
const readAttachment = host.sessions.attachment;
|
|
257
|
+
if (readAttachment === undefined)
|
|
258
|
+
throw missingHostCapability('session attachment reading');
|
|
236
259
|
return Promise.all(refs.map(async (ref) => {
|
|
237
|
-
const stored = await
|
|
260
|
+
const stored = await readAttachment({
|
|
238
261
|
sessionId: sourceSessionId,
|
|
239
262
|
attachmentId: ref.attachmentId,
|
|
240
263
|
});
|
|
241
264
|
if (!stored.data || typeof stored.data !== 'string') {
|
|
242
|
-
throw new RpcError('
|
|
265
|
+
throw new RpcError('bridge.host', 'empty-image', `附件 ${ref.attachmentId} 没有返回图片字节。`);
|
|
243
266
|
}
|
|
244
267
|
const attachment = stored.attachment ?? ref;
|
|
245
268
|
return {
|
|
@@ -253,9 +276,9 @@ async function readPromptImages(rpc, sourceSessionId, refs) {
|
|
|
253
276
|
function imageFallbackAllowed(error) {
|
|
254
277
|
if (!(error instanceof RpcError))
|
|
255
278
|
return false;
|
|
256
|
-
if (error.
|
|
279
|
+
if (error.code === 'unavailable' || error.code === 'empty-image')
|
|
257
280
|
return true;
|
|
258
|
-
if (error.
|
|
281
|
+
if (error.code !== 'attachment-error')
|
|
259
282
|
return false;
|
|
260
283
|
const reason = error.details && typeof error.details === 'object' && 'reason' in error.details
|
|
261
284
|
? String(error.details.reason)
|
|
@@ -272,19 +295,20 @@ function imageFallbackAllowed(error) {
|
|
|
272
295
|
* 摘要能被模型看见依赖 goal-round-driver 把它渲染成 `<goal_round>` 提示,
|
|
273
296
|
* 或模型主动调 `get_goal`。所以默认把摘要同时放进首轮提示:任何组装下都成立。
|
|
274
297
|
*/
|
|
275
|
-
export async function executeMigration(
|
|
298
|
+
export async function executeMigration(input, options) {
|
|
299
|
+
const host = asBridgeHost(input);
|
|
276
300
|
const progress = options.onProgress ?? (() => { });
|
|
277
301
|
const warnings = [];
|
|
278
302
|
const inject = options.inject ?? 'both';
|
|
279
303
|
const lang = options.lang ?? detectLang(options.summary);
|
|
280
|
-
const summary = options.summary
|
|
281
|
-
if (!summary)
|
|
304
|
+
const summary = options.summary;
|
|
305
|
+
if (!summary.trim())
|
|
282
306
|
throw new RpcError('bridge.migrate', 'empty-summary', '摘要为空,拒绝迁移。');
|
|
283
|
-
const sourceSession = await findSession(
|
|
284
|
-
const where = await placement(
|
|
307
|
+
const sourceSession = options.sourceSession ?? await findSession(host, options.sessionId);
|
|
308
|
+
const where = await placement(host, sourceSession, options.sessionId);
|
|
285
309
|
let sourceModel;
|
|
286
310
|
try {
|
|
287
|
-
const models = await
|
|
311
|
+
const models = await host.sessions.models({ sessionId: options.sessionId });
|
|
288
312
|
const current = models.current;
|
|
289
313
|
if (typeof current?.provider === 'string' && current.provider
|
|
290
314
|
&& typeof current.model === 'string' && current.model) {
|
|
@@ -301,7 +325,7 @@ export async function executeMigration(rpc, options) {
|
|
|
301
325
|
warnings.push(`读取源会话模型失败,目标将使用 host 默认模型:${error instanceof Error ? error.message : String(error)}`);
|
|
302
326
|
}
|
|
303
327
|
progress(`在 ${options.to} 模式下新建会话…`);
|
|
304
|
-
const created = await
|
|
328
|
+
const created = await host.sessions.create({
|
|
305
329
|
...where,
|
|
306
330
|
agentPreset: options.to,
|
|
307
331
|
});
|
|
@@ -312,7 +336,7 @@ export async function executeMigration(rpc, options) {
|
|
|
312
336
|
let modelTransferred = false;
|
|
313
337
|
if (sourceModel) {
|
|
314
338
|
try {
|
|
315
|
-
await
|
|
339
|
+
await host.sessions.selectModel({ sessionId: created.sessionId, ...sourceModel });
|
|
316
340
|
modelTransferred = true;
|
|
317
341
|
}
|
|
318
342
|
catch (error) {
|
|
@@ -322,7 +346,7 @@ export async function executeMigration(rpc, options) {
|
|
|
322
346
|
let titled = false;
|
|
323
347
|
if (options.title) {
|
|
324
348
|
try {
|
|
325
|
-
await
|
|
349
|
+
await host.sessions.rename({ sessionId: created.sessionId, title: options.title });
|
|
326
350
|
titled = true;
|
|
327
351
|
}
|
|
328
352
|
catch (error) {
|
|
@@ -336,14 +360,14 @@ export async function executeMigration(rpc, options) {
|
|
|
336
360
|
let imagesSent = 0;
|
|
337
361
|
if (inject === 'goal' || inject === 'both') {
|
|
338
362
|
try {
|
|
339
|
-
const createdGoal = await
|
|
363
|
+
const createdGoal = await host.goals.create({
|
|
340
364
|
sessionId: created.sessionId,
|
|
341
365
|
objective: summary,
|
|
342
366
|
maxGoalRounds: options.goalRounds ?? 1,
|
|
343
367
|
});
|
|
344
368
|
goalCreated = true;
|
|
345
369
|
try {
|
|
346
|
-
await
|
|
370
|
+
await host.goals.pause({ sessionId: created.sessionId, ref: createdGoal.ref });
|
|
347
371
|
goalPaused = true;
|
|
348
372
|
}
|
|
349
373
|
catch (error) {
|
|
@@ -352,13 +376,16 @@ export async function executeMigration(rpc, options) {
|
|
|
352
376
|
safeToKickoff = false;
|
|
353
377
|
let goalCleared = false;
|
|
354
378
|
try {
|
|
355
|
-
|
|
379
|
+
const clearGoal = host.goals.clear;
|
|
380
|
+
if (clearGoal === undefined)
|
|
381
|
+
throw missingHostCapability('goal clearing');
|
|
382
|
+
await clearGoal({ sessionId: created.sessionId, ref: createdGoal.ref });
|
|
356
383
|
goalCleared = true;
|
|
357
384
|
}
|
|
358
385
|
catch (clearError) {
|
|
359
386
|
warnings.push(`清除未暂停的交接目标失败,已继续取消目标会话;请保持目标会话关闭并手动检查:${clearError instanceof Error ? clearError.message : String(clearError)}`);
|
|
360
387
|
}
|
|
361
|
-
await
|
|
388
|
+
await host.sessions.cancel({ sessionId: created.sessionId }).catch(() => undefined);
|
|
362
389
|
warnings.push(goalCleared
|
|
363
390
|
? `暂停交接目标失败,已清除目标并取消自动启动;请打开目标会话检查后手动继续:${error instanceof Error ? error.message : String(error)}`
|
|
364
391
|
: `暂停交接目标失败,已取消自动启动但无法确认目标已清除;请打开目标会话检查后手动继续:${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -376,7 +403,7 @@ export async function executeMigration(rpc, options) {
|
|
|
376
403
|
progress('发送首轮交接指令…');
|
|
377
404
|
let unresolved = { refs: [], missing: 0 };
|
|
378
405
|
try {
|
|
379
|
-
unresolved = unresolvedImageRefs(await foldedHistory(
|
|
406
|
+
unresolved = unresolvedImageRefs(await foldedHistory(host, options.sessionId));
|
|
380
407
|
}
|
|
381
408
|
catch (error) {
|
|
382
409
|
warnings.push(`读取图片状态失败,已按纯文本交接:${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -384,33 +411,50 @@ export async function executeMigration(rpc, options) {
|
|
|
384
411
|
if (unresolved.missing > 0) {
|
|
385
412
|
warnings.push(`有 ${unresolved.missing} 张未解析图片缺少可复用的持久附件引用;目标会话必须回源会话核验。`);
|
|
386
413
|
}
|
|
414
|
+
let uncertainImagePromptFailure = false;
|
|
387
415
|
if (unresolved.refs.length) {
|
|
388
416
|
try {
|
|
389
|
-
const images = await readPromptImages(
|
|
417
|
+
const images = await readPromptImages(host, options.sessionId, unresolved.refs);
|
|
390
418
|
const transferNote = lang === 'en'
|
|
391
419
|
? 'Bridge transfer note: the unresolved source images listed above are attached to this kickoff. Inspect them directly; do not infer details that are not visible.'
|
|
392
420
|
: 'Bridge 搬运说明:上文列出的未解析源图片已附在本次 kickoff 中。请直接检查原图,不得推断看不清的细节。';
|
|
393
|
-
await
|
|
421
|
+
await host.sessions.prompt({
|
|
394
422
|
sessionId: created.sessionId,
|
|
395
423
|
mode: 'queue',
|
|
396
424
|
content: [...images, { type: 'text', text: `${baseText}\n\n${transferNote}` }],
|
|
397
425
|
});
|
|
398
426
|
imagesSent = images.length;
|
|
427
|
+
kickoffSent = true;
|
|
399
428
|
}
|
|
400
429
|
catch (error) {
|
|
401
|
-
if (
|
|
402
|
-
|
|
403
|
-
|
|
430
|
+
if (imageFallbackAllowed(error)) {
|
|
431
|
+
warnings.push(`目标模型或 host 不能接收原图,已使用逐字视觉证据/未解析提示降级:${error instanceof Error ? error.message : String(error)}`);
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
// 目标已经存在且 goal 已暂停。对不确定是否送达的 prompt 不能盲目重试,
|
|
435
|
+
// 否则可能让模型收到两份 kickoff;把原目标交还给用户手动检查更安全。
|
|
436
|
+
uncertainImagePromptFailure = true;
|
|
437
|
+
warnings.push(lang === 'en'
|
|
438
|
+
? `The target session was created, but its image kickoff could not be confirmed. Open that target and continue manually; Bridge did not retry an ambiguously delivered prompt: ${error instanceof Error ? error.message : String(error)}`
|
|
439
|
+
: `目标会话已创建,但带图 kickoff 是否送达无法确认。请打开这个目标手动检查并继续;Bridge 没有盲目重试可能已送达的提示:${error instanceof Error ? error.message : String(error)}`);
|
|
440
|
+
}
|
|
404
441
|
}
|
|
405
442
|
}
|
|
406
|
-
if (imagesSent === 0) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
443
|
+
if (imagesSent === 0 && !uncertainImagePromptFailure) {
|
|
444
|
+
try {
|
|
445
|
+
await host.sessions.prompt({
|
|
446
|
+
sessionId: created.sessionId,
|
|
447
|
+
mode: 'queue',
|
|
448
|
+
content: [{ type: 'text', text: baseText }],
|
|
449
|
+
});
|
|
450
|
+
kickoffSent = true;
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
warnings.push(lang === 'en'
|
|
454
|
+
? `The target session was created, but its kickoff could not be confirmed. Open that target and continue manually; Bridge did not create another session: ${error instanceof Error ? error.message : String(error)}`
|
|
455
|
+
: `目标会话已创建,但 kickoff 是否送达无法确认。请打开这个目标手动检查并继续;Bridge 没有再创建一个会话:${error instanceof Error ? error.message : String(error)}`);
|
|
456
|
+
}
|
|
412
457
|
}
|
|
413
|
-
kickoffSent = true;
|
|
414
458
|
}
|
|
415
459
|
return {
|
|
416
460
|
sessionId: created.sessionId,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Previewable cross-preset session migration for DeepSeek Harness with bounded, fixed-schema handoffs",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -11,12 +11,15 @@
|
|
|
11
11
|
"reports/v0.2.3-e2e-2026-08-20T13-19-13-924Z.raw.json",
|
|
12
12
|
"reports/v0.2.3-e2e-report.md",
|
|
13
13
|
"reports/v0.2.6-rc11-vision-report.md",
|
|
14
|
+
"reports/native-workbench-2026-08-25.md",
|
|
15
|
+
"reports/native-workbench-2026-08-25.raw.json",
|
|
14
16
|
"cordis.patch.yml",
|
|
15
17
|
"README.md",
|
|
16
18
|
"README.zh.md"
|
|
17
19
|
],
|
|
18
20
|
"scripts": {
|
|
19
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
+
"build": "tsc -p tsconfig.json && npm run build:client",
|
|
22
|
+
"build:client": "tsdown --config tsdown.config.ts",
|
|
20
23
|
"build:check": "npm run build && git diff --exit-code -- lib",
|
|
21
24
|
"typecheck": "tsc -p tsconfig.check.json",
|
|
22
25
|
"prepack": "npm run build",
|
|
@@ -41,20 +44,50 @@
|
|
|
41
44
|
"dsh": {
|
|
42
45
|
"bundle": {
|
|
43
46
|
"patch": "./cordis.patch.yml"
|
|
47
|
+
},
|
|
48
|
+
"client": {
|
|
49
|
+
"inject": [
|
|
50
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
51
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
52
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
53
|
+
],
|
|
54
|
+
"platform": "web"
|
|
44
55
|
}
|
|
45
56
|
},
|
|
46
57
|
"engines": {
|
|
47
58
|
"node": ">=22"
|
|
48
59
|
},
|
|
49
60
|
"peerDependencies": {
|
|
50
|
-
"@deepseek-ai/cordis": "^4.0.1"
|
|
61
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
62
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.7 || ^0.1.1-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.7 || ^0.1.1-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.0-rc.7 || ^0.1.1-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.7 || ^0.1.1-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.7 || ^0.1.1-rc.1",
|
|
67
|
+
"react": "^18.2.0"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@deepseek-ai/dsh-api-remotes": { "optional": true },
|
|
71
|
+
"@deepseek-ai/dsh-client-runtime": { "optional": true },
|
|
72
|
+
"@deepseek-ai/dsh-client-ui-conversation": { "optional": true },
|
|
73
|
+
"@deepseek-ai/dsh-client-ui-primitives": { "optional": true },
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-slots": { "optional": true },
|
|
75
|
+
"react": { "optional": true }
|
|
51
76
|
},
|
|
52
77
|
"dependencies": {
|
|
53
78
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
54
79
|
},
|
|
55
80
|
"devDependencies": {
|
|
56
81
|
"@deepseek-ai/cordis": "4.0.1",
|
|
82
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.1-rc.2",
|
|
83
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
|
|
84
|
+
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
|
|
85
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
|
|
57
87
|
"@types/node": "^26.2.0",
|
|
88
|
+
"@types/react": "~18.3.1",
|
|
89
|
+
"react": "^18.3.1",
|
|
90
|
+
"tsdown": "^0.22.14",
|
|
58
91
|
"typescript": "^7.0.2"
|
|
59
92
|
},
|
|
60
93
|
"publishConfig": {
|
|
@@ -88,6 +121,10 @@
|
|
|
88
121
|
"types": "./lib/migrate.d.ts",
|
|
89
122
|
"default": "./lib/migrate.js"
|
|
90
123
|
},
|
|
124
|
+
"./host": {
|
|
125
|
+
"types": "./lib/host.d.ts",
|
|
126
|
+
"default": "./lib/host.js"
|
|
127
|
+
},
|
|
91
128
|
"./rpc": {
|
|
92
129
|
"types": "./lib/rpc.d.ts",
|
|
93
130
|
"default": "./lib/rpc.js"
|
|
@@ -100,6 +137,14 @@
|
|
|
100
137
|
"types": "./lib/command.d.ts",
|
|
101
138
|
"default": "./lib/command.js"
|
|
102
139
|
},
|
|
140
|
+
"./client": {
|
|
141
|
+
"types": "./lib/client.d.ts",
|
|
142
|
+
"default": "./lib/client.js"
|
|
143
|
+
},
|
|
144
|
+
"./client-contract": {
|
|
145
|
+
"types": "./lib/client-contract.d.ts",
|
|
146
|
+
"default": "./lib/client-contract.js"
|
|
147
|
+
},
|
|
103
148
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
104
149
|
"./package.json": "./package.json"
|
|
105
150
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Native WebUI workbench acceptance · 2026-08-25
|
|
2
|
+
|
|
3
|
+
Environment: isolated official `@deepseek-ai/dsh@0.1.1-rc.2` Web profile, locally packed Bridge `0.2.11` plus the current PR changes. The source, worker, and target sessions were synthetic and isolated from the user's normal DSH home.
|
|
4
|
+
|
|
5
|
+
## What was exercised
|
|
6
|
+
|
|
7
|
+
- the client half loaded through the official `dsh.client` module graph and occupied `conversation.chat.commandview`;
|
|
8
|
+
- `/bridge --doctor` rendered through the native card and reported 13/13 host capabilities;
|
|
9
|
+
- the running row showed immediate elapsed-time feedback without mutating the source;
|
|
10
|
+
- completed Markdown rendered through the official primitive, a complete JSON edit rendered as a keyboard-accessible tree, and the textarea round-tripped user changes;
|
|
11
|
+
- the reviewed payload was accepted by the host command without being recorded in command input;
|
|
12
|
+
- the created goal was paused, the first target turn restated the handoff, and the client automatically opened the target session.
|
|
13
|
+
|
|
14
|
+
## Fixed repeat gate
|
|
15
|
+
|
|
16
|
+
Each run carried the same five low-collision facts: port `8118`, PostgreSQL, MongoDB forbidden, `src/orders/router.ts`, and “idempotency tests only.”
|
|
17
|
+
|
|
18
|
+
| Run | Route | Preview facts | Target facts | Worker time | Target LLM time | Paused | Auto-open |
|
|
19
|
+
|---|---|---:|---:|---:|---:|---:|---:|
|
|
20
|
+
| R1 | standard → code | 5/5 | 5/5 | 12.846 s | 7.807 s | yes | yes |
|
|
21
|
+
| R2 | minimal → code | 5/5 | 5/5 | 9.713 s | 6.175 s | yes | yes |
|
|
22
|
+
| R3 | minimal → code | 5/5 | 5/5 | 7.421 s | 5.772 s | yes | yes |
|
|
23
|
+
|
|
24
|
+
R1 additionally changed `8118` to `8118(用户在 WebUI 校对)` inside the native editor; the exact marker appeared in both the stored target goal and the first target response.
|
|
25
|
+
|
|
26
|
+
The token counters and per-run records are in [`native-workbench-2026-08-25.raw.json`](native-workbench-2026-08-25.raw.json).
|
|
27
|
+
|
|
28
|
+
## Boundary
|
|
29
|
+
|
|
30
|
+
This is a three-run fixed release gate, not a latency distribution or population-level reliability claim. Worker and target latency, token use, and phrasing remain model-, preset-, cache-, and provider-dependent. Automated fake-host tests cover failure and payload bounds without spending tokens; this report covers the installed official-WebUI path that those tests cannot prove.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"date": "2026-08-25",
|
|
3
|
+
"dshVersion": "0.1.1-rc.2",
|
|
4
|
+
"bridgePackageVersion": "0.2.11",
|
|
5
|
+
"environment": "isolated official dsh web profile",
|
|
6
|
+
"workerModel": "deepseek-v4-flash",
|
|
7
|
+
"facts": ["8118", "PostgreSQL", "MongoDB forbidden", "src/orders/router.ts", "idempotency tests only"],
|
|
8
|
+
"runs": [
|
|
9
|
+
{
|
|
10
|
+
"id": "R1", "from": "standard", "to": "code",
|
|
11
|
+
"previewFacts": 5, "targetFacts": 5,
|
|
12
|
+
"workerMs": 12846, "targetMs": 7807,
|
|
13
|
+
"editedMarkerPreserved": true, "targetPaused": true, "autoOpened": true,
|
|
14
|
+
"workerTokens": { "uncachedInput": 931, "output": 1112, "cacheRead": 1024 },
|
|
15
|
+
"targetTokens": { "uncachedInput": 10415, "output": 436, "cacheRead": 0 }
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"id": "R2", "from": "minimal", "to": "code",
|
|
19
|
+
"previewFacts": 5, "targetFacts": 5,
|
|
20
|
+
"workerMs": 9713, "targetMs": 6175,
|
|
21
|
+
"targetPaused": true, "autoOpened": true,
|
|
22
|
+
"workerTokens": { "uncachedInput": 518, "output": 771, "cacheRead": 1024 },
|
|
23
|
+
"targetTokens": { "uncachedInput": 9670, "output": 278, "cacheRead": 640 }
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "R3", "from": "minimal", "to": "code",
|
|
27
|
+
"previewFacts": 5, "targetFacts": 5,
|
|
28
|
+
"workerMs": 7421, "targetMs": 5772,
|
|
29
|
+
"targetPaused": true, "autoOpened": true,
|
|
30
|
+
"workerTokens": { "uncachedInput": 131, "output": 615, "cacheRead": 1408 },
|
|
31
|
+
"targetTokens": { "uncachedInput": 1622, "output": 290, "cacheRead": 8704 }
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|