pi-web-ui 0.86.2 → 0.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -21
- package/README.md +1 -1
- package/bin/pi-web-ui.mjs +312 -40
- package/dist/server/agent-service.js +297 -6
- package/dist/server/client-state.js +8 -0
- package/dist/server/composer-drafts.js +138 -0
- package/dist/server/dsh/dsh-agent-service.js +531 -54
- package/dist/server/dsh/dsh-client.js +28 -0
- package/dist/server/dsh/dsh-sessions.js +28 -0
- package/dist/server/dsh/dsh-usage.js +82 -0
- package/dist/server/dsh/preset-clones.js +260 -0
- package/dist/server/dsh/runtime/custom-prompt.mjs +33 -0
- package/dist/server/dsh/runtime/goal-rpc.mjs +406 -22
- package/dist/server/dsh/runtime/launcher.mjs +17 -0
- package/dist/server/dsh/runtime/override.patch.yml +19 -1
- package/dist/server/files-service.js +259 -1
- package/dist/server/index.js +219 -3
- package/dist/server/mcp-bridge.js +126 -22
- package/dist/server/mcp-hot-reload.js +117 -0
- package/dist/server/model-admin.js +93 -3
- package/dist/server/plugin-dom.js +83 -0
- package/dist/server/plugin-facilities.js +15 -1
- package/dist/server/plugin-installer.js +6 -0
- package/dist/server/plugins.js +781 -74
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/provider-oauth-flow.js +157 -0
- package/package.json +1 -1
- package/plugins/catalog.json +9 -0
- package/themes/dark-teal.css +63 -22
- package/web/dist/assets/index-DePnXpq-.js +374 -0
- package/web/dist/assets/index-F86qWlJy.css +41 -0
- package/web/dist/index.html +3 -2
- package/web/dist/assets/TerminalPanel-BytY8dx7.js +0 -6
- package/web/dist/assets/TerminalPanel-DOrYoP_4.css +0 -32
- package/web/dist/assets/index-CKoVyDkP.css +0 -10
- package/web/dist/assets/index-Dmwji4Cr.js +0 -361
|
@@ -132,6 +132,35 @@ function agentOf(server, sessionId) {
|
|
|
132
132
|
* 官方 SDK server 的 goal 扩展:goal/* 方法直连 ctx.goals 服务动词。
|
|
133
133
|
*/
|
|
134
134
|
class DshGoalJsonRpcServer extends HarnessSdkJsonRpcServer {
|
|
135
|
+
/**
|
|
136
|
+
* 官方构造器订阅 session.event / session.status / subagent.*;这里再加**直播流**:
|
|
137
|
+
* 新版 DSH 运行时(0.1.1-rc.2 之后)把逐 chunk 的模型输出发成 agent scope 的
|
|
138
|
+
* `agent/assistant-stream` 帧(持久的 `assistant/chunk` 事件已取消),host 侧的
|
|
139
|
+
* jsonrpc 面收不到 → pi-web-ui 既没有 streamingMessage / 实时速率,也拿不到
|
|
140
|
+
* 逐次调用 usage(底栏上下文占用 / 缓存命中)。
|
|
141
|
+
*
|
|
142
|
+
* 转成 `assistant.stream` 通知:`{ sessionId, frame }`,frame 是运行时的
|
|
143
|
+
* start / chunk / end 三态;chunk 形状与旧 `assistant/chunk.data.chunk` 同构。
|
|
144
|
+
*
|
|
145
|
+
* `{ global: true }` 是必需的:该事件在 agent scope 派发,host 上注册的观察者
|
|
146
|
+
* 不加 global 永远收不到(同 dsh-api-session-controller 的写法)。
|
|
147
|
+
*/
|
|
148
|
+
constructor(ctx, transport, options) {
|
|
149
|
+
super(ctx, transport, options);
|
|
150
|
+
this.disposers.push(
|
|
151
|
+
ctx.on(
|
|
152
|
+
"agent/assistant-stream",
|
|
153
|
+
({ agent, frame }) => {
|
|
154
|
+
this.transport.notify("assistant.stream", {
|
|
155
|
+
sessionId: String(agent.session.id),
|
|
156
|
+
frame,
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
{ global: true },
|
|
160
|
+
),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
135
164
|
/**
|
|
136
165
|
* 创建(或替换已完成的)目标并 arm —— round-driver 会在 agent idle 时
|
|
137
166
|
* 自动续第一轮,不需要额外 prompt。
|
|
@@ -194,6 +223,321 @@ class DshGoalJsonRpcServer extends HarnessSdkJsonRpcServer {
|
|
|
194
223
|
return viewPayload(this.ctx.goals.edit(agent, { id: view.id, revision: view.revision }, request));
|
|
195
224
|
}
|
|
196
225
|
|
|
226
|
+
// -----------------------------------------------------------------------
|
|
227
|
+
// Agent 预设(dsh-web 四模式:standard/ptc/minimal/cordis + 用户自建)。
|
|
228
|
+
//
|
|
229
|
+
// roster 挂了 preset-plane patch 才存在(见 server/dsh/preset-clones.ts 生成的
|
|
230
|
+
// preset-plane.patch.yml);legacy 单组合(无 patch)下全部回退:list 报 unavailable,
|
|
231
|
+
// assign/select 透过,创建走官方原逻辑,per-agent 钩子照装(问卷 waterfall 修法
|
|
232
|
+
// 本就要求 agent scope 注册,与预设有无无关)。
|
|
233
|
+
//
|
|
234
|
+
// 约定:Node 侧(dsh-agent-service)是 preset 真源 —— 新会话首个 prompt 前调
|
|
235
|
+
// preset/assign 登记,会话创建时消费;空白会话切换走 preset/select(首轮后锁,
|
|
236
|
+
// 与官方 agent-preset/locked 语义一致)。RPC 错误不抛 domain 码(transport 统一
|
|
237
|
+
// 坍缩成 -32603),一律走 {ok:false,code,message} 信封。
|
|
238
|
+
// -----------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/** 待消费的创建期预设:sessionId → preset id(创建时消费并删除)。 */
|
|
241
|
+
pendingPresets = new Map();
|
|
242
|
+
/** minimal 下被门禁的 agent id(bridge 工具 + 问卷调用期拒绝,中英双语报错)。 */
|
|
243
|
+
minimalAgents = new Set();
|
|
244
|
+
/** 技能目录取数 agent(standard 预设,常驻但永不 prompt,零 token)。 */
|
|
245
|
+
inventorySessionId = "pi-webui-inventory";
|
|
246
|
+
|
|
247
|
+
/** roster 服务(无 preset-plane patch 时 undefined → 全员 legacy)。 */
|
|
248
|
+
presetRoster() {
|
|
249
|
+
try {
|
|
250
|
+
return this.ctx.get("agentPresets") ?? undefined;
|
|
251
|
+
} catch {
|
|
252
|
+
return undefined;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** 解析一个 preset id(非法/unknown → 默认;roster 缺失 → undefined)。 */
|
|
257
|
+
async resolvePresetId(requested) {
|
|
258
|
+
const roster = this.presetRoster();
|
|
259
|
+
if (!roster) return undefined;
|
|
260
|
+
try {
|
|
261
|
+
return (await roster.resolve(requested)).id;
|
|
262
|
+
} catch {
|
|
263
|
+
try {
|
|
264
|
+
return (await roster.resolve()).id;
|
|
265
|
+
} catch {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** preset/list RPC:roster 名录(display 字段以运行时树为准)。 */
|
|
272
|
+
async listPresets() {
|
|
273
|
+
const roster = this.presetRoster();
|
|
274
|
+
if (!roster) return { presets: [], defaultPreset: "standard", authorable: false, unavailable: true };
|
|
275
|
+
try {
|
|
276
|
+
// service.list() = discovery 数组;isDefault/authorable 走同名的 getter。
|
|
277
|
+
const items = await roster.list();
|
|
278
|
+
let defaultId = "standard";
|
|
279
|
+
try {
|
|
280
|
+
defaultId = roster.defaultId ?? defaultId;
|
|
281
|
+
} catch {
|
|
282
|
+
/* keep */
|
|
283
|
+
}
|
|
284
|
+
let authorable = false;
|
|
285
|
+
try {
|
|
286
|
+
authorable = !!roster.authorable;
|
|
287
|
+
} catch {
|
|
288
|
+
/* keep */
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
presets: (Array.isArray(items) ? items : []).map((p) => ({
|
|
292
|
+
id: p.id,
|
|
293
|
+
trust: p.trust,
|
|
294
|
+
isDefault: p.id === defaultId,
|
|
295
|
+
...(p.name ? { name: p.name } : {}),
|
|
296
|
+
...(p.description ? { description: p.description } : {}),
|
|
297
|
+
...(Number.isSafeInteger(p.order) ? { order: p.order } : {}),
|
|
298
|
+
...(p.broken ? { broken: p.broken } : {}),
|
|
299
|
+
})),
|
|
300
|
+
defaultPreset: defaultId,
|
|
301
|
+
authorable,
|
|
302
|
+
};
|
|
303
|
+
} catch (err) {
|
|
304
|
+
return {
|
|
305
|
+
presets: [],
|
|
306
|
+
defaultPreset: "standard",
|
|
307
|
+
authorable: false,
|
|
308
|
+
unavailable: true,
|
|
309
|
+
error: err?.message ?? String(err),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** preset/assign RPC:登记创建期预设(首个 prompt/goal 前调用,仅创建时消费)。 */
|
|
315
|
+
async assignPreset(params) {
|
|
316
|
+
if (typeof params?.sessionId !== "string") throw new TypeError("preset/assign requires sessionId");
|
|
317
|
+
if (typeof params?.preset !== "string" || !params.preset) throw new TypeError("preset/assign requires preset");
|
|
318
|
+
this.pendingPresets.set(params.sessionId, params.preset);
|
|
319
|
+
return { ok: true };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** agent 存活检查(项目依赖的 0.1.1-rc.2 无基类 assertLiveAgent,内联同语义)。 */
|
|
323
|
+
liveAgent(rec, sessionId) {
|
|
324
|
+
if (!rec || this.ctx.agents.get(rec.handle.agent.id) !== rec.handle.agent) {
|
|
325
|
+
throw new Error(`session agent was disposed outside the server: ${sessionId}`);
|
|
326
|
+
}
|
|
327
|
+
return rec.handle.agent;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** preset/select RPC:空白会话切换预设(首轮后官方抛 agent-preset/locked)。 */
|
|
331
|
+
async selectPreset(params) {
|
|
332
|
+
if (typeof params?.sessionId !== "string") throw new TypeError("preset/select requires sessionId");
|
|
333
|
+
if (typeof params?.preset !== "string" || !params.preset) throw new TypeError("preset/select requires preset");
|
|
334
|
+
const roster = this.presetRoster();
|
|
335
|
+
if (!roster)
|
|
336
|
+
return { ok: false, code: "agent-preset/unavailable", message: "agent presets unavailable (legacy runtime)" };
|
|
337
|
+
try {
|
|
338
|
+
const rec = await this.getOrCreateSession(params.sessionId);
|
|
339
|
+
const agent = this.liveAgent(rec, params.sessionId);
|
|
340
|
+
const id = await roster.select(agent, params.preset);
|
|
341
|
+
this.pendingPresets.delete(params.sessionId);
|
|
342
|
+
if (id === "minimal") this.minimalAgents.add(agent.id);
|
|
343
|
+
else this.minimalAgents.delete(agent.id);
|
|
344
|
+
return { ok: true, preset: id };
|
|
345
|
+
} catch (err) {
|
|
346
|
+
return { ok: false, code: err?.code ?? "agent-preset/error", message: err?.message ?? String(err) };
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** permission/get RPC:读一个会话的权限预设 projection(官方 /permission 弹窗同源)。 */
|
|
351
|
+
async getPermission(params) {
|
|
352
|
+
if (typeof params?.sessionId !== "string") throw new TypeError("permission/get requires sessionId");
|
|
353
|
+
const rec = await this.getOrCreateSession(params.sessionId);
|
|
354
|
+
const agent = this.liveAgent(rec, params.sessionId);
|
|
355
|
+
const permission = this.ctx.permissionPresets;
|
|
356
|
+
if (!permission) throw new Error("permission/get: permission service unavailable");
|
|
357
|
+
return permission.selectFor(permission.permissionState(agent.session));
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** permission/set RPC:按会话热切换权限预设(写会话日志,无需重启;
|
|
361
|
+
* 与官方 /permission 命令同语义:approval 走 live writer,模型会收到一条
|
|
362
|
+
* 策略变更的用户消息)。未知预设走 {ok:false} 信封,不抛 domain 错。 */
|
|
363
|
+
async setPermission(params) {
|
|
364
|
+
if (typeof params?.sessionId !== "string") throw new TypeError("permission/set requires sessionId");
|
|
365
|
+
if (typeof params?.preset !== "string" || !params.preset) throw new TypeError("permission/set requires preset");
|
|
366
|
+
const permission = this.ctx.permissionPresets;
|
|
367
|
+
if (!permission) throw new Error("permission/set: permission service unavailable");
|
|
368
|
+
if (!permission.names.includes(params.preset)) {
|
|
369
|
+
return {
|
|
370
|
+
ok: false,
|
|
371
|
+
code: "permission/unknown-preset",
|
|
372
|
+
message: `unknown preset "${params.preset}" (known: ${permission.names.join(", ")})`,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const rec = await this.getOrCreateSession(params.sessionId);
|
|
377
|
+
const agent = this.liveAgent(rec, params.sessionId);
|
|
378
|
+
permission.apply(agent.session, params.preset, (policy) => this.ctx.approval.setPolicy(agent, policy));
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
preset: params.preset,
|
|
382
|
+
...permission.selectFor(permission.permissionState(agent.session)),
|
|
383
|
+
};
|
|
384
|
+
} catch (err) {
|
|
385
|
+
return { ok: false, code: "permission/error", message: err?.message ?? String(err) };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** preset/tools RPC(仅 DEBUG):读一个会话 agent scope 的实际工具名(校验挂载用)。 */
|
|
390
|
+
async presetTools(params) {
|
|
391
|
+
if (process.env.PI_WEB_DSH_DEBUG !== "1") {
|
|
392
|
+
throw new Error("preset/tools 仅调试可用(服务端需设 PI_WEB_DSH_DEBUG=1)");
|
|
393
|
+
}
|
|
394
|
+
if (typeof params?.sessionId !== "string") throw new TypeError("preset/tools requires sessionId");
|
|
395
|
+
const rec = await this.getOrCreateSession(params.sessionId);
|
|
396
|
+
const agent = this.liveAgent(rec, params.sessionId);
|
|
397
|
+
// agent 视角(scope):preset 层 + host 层合并目录;缺省全局视角只看到 host 层。
|
|
398
|
+
const schemas = this.ctx.tools.schemas(agent);
|
|
399
|
+
return { tools: schemas.map((s) => s.name).sort() };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** debug/assemble RPC(仅 DEBUG):读一个会话 agent 视角的系统提示词 section 名录
|
|
403
|
+
* (零 token 验证 persona 遮蔽/complete 压制/自定义 section 用;不返回正文)。 */
|
|
404
|
+
async debugAssemble(params) {
|
|
405
|
+
if (process.env.PI_WEB_DSH_DEBUG !== "1") {
|
|
406
|
+
throw new Error("debug/assemble 仅调试可用(服务端需设 PI_WEB_DSH_DEBUG=1)");
|
|
407
|
+
}
|
|
408
|
+
if (typeof params?.sessionId !== "string") throw new TypeError("debug/assemble requires sessionId");
|
|
409
|
+
const rec = await this.getOrCreateSession(params.sessionId);
|
|
410
|
+
const agent = this.liveAgent(rec, params.sessionId);
|
|
411
|
+
// 组装上下文同 agent-loop(assembleContextFor):{ agent, scope: agent }。
|
|
412
|
+
const contexts = [{ agent, scope: agent }, { scope: agent }, { scope: agent.ctx }];
|
|
413
|
+
let lastErr = null;
|
|
414
|
+
for (const context of contexts) {
|
|
415
|
+
try {
|
|
416
|
+
const asm = await this.ctx.systemPrompt.assemble(context);
|
|
417
|
+
if (asm && Array.isArray(asm.sections)) {
|
|
418
|
+
return { sections: asm.sections.map((s) => ({ name: s.name, chars: String(s.text ?? "").length })) };
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
lastErr = err;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
throw new Error(`debug/assemble: assembly failed (${lastErr?.message ?? lastErr})`);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** 会话创建(官方同形 + setup):mount 预设 + 装 per-agent 钩子。
|
|
428
|
+
* 磁盘上已有同 id(运行时重启/优雅关闭后重建)→ 转 agents.resume
|
|
429
|
+
* (官方恢复路径,自动缝合被中断的 turn;setup 照跑,预设照挂)。 */
|
|
430
|
+
async createSession(sessionId, preset) {
|
|
431
|
+
const requested = preset ?? this.pendingPresets.get(sessionId);
|
|
432
|
+
this.pendingPresets.delete(sessionId);
|
|
433
|
+
const resolved = await this.resolvePresetId(requested);
|
|
434
|
+
const agentOptions = {
|
|
435
|
+
provider: this.provider,
|
|
436
|
+
model: this.model,
|
|
437
|
+
...(this.reasoningEffort === void 0 ? {} : { reasoningEffort: this.reasoningEffort }),
|
|
438
|
+
...(this.maxTokens === void 0 ? {} : { maxTokens: this.maxTokens }),
|
|
439
|
+
};
|
|
440
|
+
const setup = async (agentCtx, agent) => this.agentSetup(agentCtx, agent, resolved);
|
|
441
|
+
let handle;
|
|
442
|
+
try {
|
|
443
|
+
handle = await this.ctx.agents.create({
|
|
444
|
+
sessionId,
|
|
445
|
+
meta: { cwd: this.cwd, ...(resolved ? { agentPreset: resolved } : {}) },
|
|
446
|
+
agentOptions,
|
|
447
|
+
setup,
|
|
448
|
+
});
|
|
449
|
+
} catch (err) {
|
|
450
|
+
if (typeof err?.message === "string" && err.message.includes(`session "${sessionId}" already exists`)) {
|
|
451
|
+
process.stderr.write(`[pi-web-ui-dsh] 会话 ${sessionId} 已在磁盘,resume 恢复\n`);
|
|
452
|
+
handle = await this.ctx.agents.resume({ resumeSessionId: sessionId, agentOptions, setup });
|
|
453
|
+
} else {
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const rec = { handle };
|
|
458
|
+
this.sessions.set(sessionId, rec);
|
|
459
|
+
return rec;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** 官方 getOrCreateSession 同形(多出来的 preset 参数供创建时使用)。 */
|
|
463
|
+
async getOrCreateSession(sessionId, preset) {
|
|
464
|
+
if (this.shuttingDown) throw new Error("SDK server is shutting down");
|
|
465
|
+
const existing = this.sessions.get(sessionId);
|
|
466
|
+
if (existing) return existing;
|
|
467
|
+
const pending = this.sessionCreations.get(sessionId);
|
|
468
|
+
if (pending) return pending;
|
|
469
|
+
const creation = this.createSession(sessionId, preset);
|
|
470
|
+
this.sessionCreations.set(sessionId, creation);
|
|
471
|
+
creation.then(
|
|
472
|
+
() => {
|
|
473
|
+
this.sessionCreations.delete(sessionId);
|
|
474
|
+
},
|
|
475
|
+
() => {
|
|
476
|
+
this.sessionCreations.delete(sessionId);
|
|
477
|
+
},
|
|
478
|
+
);
|
|
479
|
+
return creation;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** 每个 agent 出生时跑一次:mount 预设 + 问卷 waterfall + 技能目录过滤。
|
|
483
|
+
* 预设优先级:会话记录(header/selected 事件,重启恢复时是唯一真相)>
|
|
484
|
+
* 创建参数 > roster 默认。 */
|
|
485
|
+
async agentSetup(agentCtx, agent, presetId) {
|
|
486
|
+
const roster = this.presetRoster();
|
|
487
|
+
let effective = presetId;
|
|
488
|
+
if (roster) {
|
|
489
|
+
try {
|
|
490
|
+
const prior = this.ctx.sessionProjections.stateOf(agent.session, "agentPreset");
|
|
491
|
+
if (typeof prior === "string" && prior) effective = prior;
|
|
492
|
+
} catch {
|
|
493
|
+
/* 无 projection 能力 → 用创建参数 */
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (roster && effective) {
|
|
497
|
+
try {
|
|
498
|
+
await roster.mount(agentCtx, effective);
|
|
499
|
+
if (effective === "minimal") this.minimalAgents.add(agent.id);
|
|
500
|
+
} catch (err) {
|
|
501
|
+
process.stderr.write(
|
|
502
|
+
`[pi-web-ui-dsh] preset "${effective}" mount 失败,转 host 直连: ${err?.message ?? err}\n`,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// 问卷 waterfall(P0 修法):answerer 必须挂 agent scope —— host 层收不到
|
|
507
|
+
// agent-scoped 事件,且新版 UserQuestionService 已删 registerProvider。
|
|
508
|
+
// 与官方 ui-user-questions 同构:返回 {answers} 即 short-circuit。
|
|
509
|
+
agentCtx.on("user-questions/request", (request, _next) => this.answerScopedQuestion(agent, request));
|
|
510
|
+
// 技能目录过滤:与上面同理,host 版已死(收不到事件),改挂 agent scope。
|
|
511
|
+
// eslint-disable-next-line no-empty-pattern -- only next is needed
|
|
512
|
+
agentCtx.on("agent/pre-step", async ({}, next) => {
|
|
513
|
+
const decision = await next();
|
|
514
|
+
if (this.disabledSkills.size && decision?.kind === "enter" && Array.isArray(decision.messages)) {
|
|
515
|
+
if (decision.messages.some((m) => m?.source?.kind === "skill-catalog")) {
|
|
516
|
+
decision.messages = decision.messages.map((m) => filterSkillCatalogMessage(m, this.disabledSkills));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return decision;
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** agent scope 的问卷 answerer:minimal 门禁 → 浏览器桥(复用排队逻辑)。 */
|
|
524
|
+
async answerScopedQuestion(agent, request) {
|
|
525
|
+
if (this.minimalAgents.has(agent?.id)) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
"ask_user_question is unavailable under the minimal preset; use the shell instead " +
|
|
528
|
+
"(极简模式下不可提问,请直接用 shell 命令推进)",
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
if (request?.signal?.aborted) throw new Error("ask_user_question aborted(提问已中止)");
|
|
532
|
+
return this.askUser(request ?? {});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/** 技能目录取数 agent(standard 常驻,永不 prompt;roster 缺失时不用)。 */
|
|
536
|
+
async ensureInventoryAgent() {
|
|
537
|
+
const rec = await this.getOrCreateSession(this.inventorySessionId);
|
|
538
|
+
return this.liveAgent(rec, this.inventorySessionId);
|
|
539
|
+
}
|
|
540
|
+
|
|
197
541
|
/** 方法分发:goal/* 与 attachment/* 走上面/下面,其余交官方。 */
|
|
198
542
|
async handleRequest(method, params) {
|
|
199
543
|
switch (method) {
|
|
@@ -231,6 +575,20 @@ class DshGoalJsonRpcServer extends HarnessSdkJsonRpcServer {
|
|
|
231
575
|
return this.registerSkill(params);
|
|
232
576
|
case "skills/get":
|
|
233
577
|
return this.getSkill(params);
|
|
578
|
+
case "preset/list":
|
|
579
|
+
return this.listPresets();
|
|
580
|
+
case "preset/assign":
|
|
581
|
+
return this.assignPreset(params);
|
|
582
|
+
case "preset/select":
|
|
583
|
+
return this.selectPreset(params);
|
|
584
|
+
case "permission/get":
|
|
585
|
+
return this.getPermission(params);
|
|
586
|
+
case "permission/set":
|
|
587
|
+
return this.setPermission(params);
|
|
588
|
+
case "preset/tools":
|
|
589
|
+
return this.presetTools(params);
|
|
590
|
+
case "debug/assemble":
|
|
591
|
+
return this.debugAssemble(params);
|
|
234
592
|
default:
|
|
235
593
|
return super.handleRequest(method, params);
|
|
236
594
|
}
|
|
@@ -324,8 +682,17 @@ class DshGoalJsonRpcServer extends HarnessSdkJsonRpcServer {
|
|
|
324
682
|
};
|
|
325
683
|
}
|
|
326
684
|
|
|
327
|
-
/** 桥接工具的 execute
|
|
685
|
+
/** 桥接工具的 execute:minimal 门禁后把调用发给服务端并 await 结果。 */
|
|
328
686
|
invokeBridgedTool(exec, name, args) {
|
|
687
|
+
const agentId = exec?.agent?.id;
|
|
688
|
+
if (agentId !== undefined && this.minimalAgents.has(agentId)) {
|
|
689
|
+
return Promise.reject(
|
|
690
|
+
new Error(
|
|
691
|
+
`工具 ${name} unavailable under the minimal preset; use the shell instead ` +
|
|
692
|
+
`(极简模式下该插件工具不可用,请用 shell 命令推进)`,
|
|
693
|
+
),
|
|
694
|
+
);
|
|
695
|
+
}
|
|
329
696
|
const id = `t-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
330
697
|
const sessionId =
|
|
331
698
|
exec?.agent?.session?.id && typeof exec.agent.session.id === "string" ? String(exec.agent.session.id) : undefined;
|
|
@@ -404,8 +771,30 @@ class DshGoalJsonRpcServer extends HarnessSdkJsonRpcServer {
|
|
|
404
771
|
/** 禁用的技能名集合(skills/set-disabled 更新)。 */
|
|
405
772
|
disabledSkills = new Set();
|
|
406
773
|
|
|
407
|
-
/**
|
|
774
|
+
/** 列出运行时当前可见技能:roster 下读常驻 inventory agent 的合并目录(技能只活在
|
|
775
|
+
* 各预设的 agent 层,host 的 skill-filesystem 已 disable,host 直读恒为空);
|
|
776
|
+
* legacy 下保持原来的 host 直读。零 key 校验/前端显示用。 */
|
|
408
777
|
async listSkills() {
|
|
778
|
+
const roster = this.presetRoster();
|
|
779
|
+
if (roster) {
|
|
780
|
+
try {
|
|
781
|
+
const agent = await this.ensureInventoryAgent();
|
|
782
|
+
// scope 视图:host registry + agent 层合并;cwd 选工作区根。
|
|
783
|
+
const skills = await this.ctx.skills.list({ scope: agent, cwd: this.cwd });
|
|
784
|
+
if (Array.isArray(skills)) {
|
|
785
|
+
return {
|
|
786
|
+
skills: skills.map((s) => ({
|
|
787
|
+
name: s.name,
|
|
788
|
+
description: s.description ?? "",
|
|
789
|
+
...(s.invocation ? { invocation: s.invocation } : {}),
|
|
790
|
+
})),
|
|
791
|
+
};
|
|
792
|
+
}
|
|
793
|
+
} catch (err) {
|
|
794
|
+
return { skills: [], error: err?.message ?? String(err) };
|
|
795
|
+
}
|
|
796
|
+
return { skills: [] };
|
|
797
|
+
}
|
|
409
798
|
try {
|
|
410
799
|
const skills = await this.ctx.skills.list();
|
|
411
800
|
if (!Array.isArray(skills)) return { skills: [] };
|
|
@@ -593,25 +982,9 @@ function apply(ctx, config) {
|
|
|
593
982
|
const server = new DshGoalJsonRpcServer(ctx, transport, {
|
|
594
983
|
maxTokensAsSuccess: resolvedConfig.maxTokensAsSuccess,
|
|
595
984
|
});
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
|
|
599
|
-
ask: (request) => server.askUser(request),
|
|
600
|
-
});
|
|
601
|
-
ctx.effect(() => unregisterQuestions, "user-questions.bridge");
|
|
602
|
-
// 技能启停(#18):晚 agent/pre-step 钩子,在 dsh-tool-skill 注入技能目录后
|
|
603
|
-
// 按 server.disabledSkills 过滤 catalog 消息(剔除禁用技能条目)。
|
|
604
|
-
// 注册在 base 之后 → 本钩子后跑,能拿到已渲染的 catalog。
|
|
605
|
-
// eslint-disable-next-line no-empty-pattern -- only next is needed
|
|
606
|
-
ctx.on("agent/pre-step", async ({}, next) => {
|
|
607
|
-
const decision = await next();
|
|
608
|
-
if (server.disabledSkills.size && decision?.kind === "enter" && Array.isArray(decision.messages)) {
|
|
609
|
-
if (decision.messages.some((m) => m?.source?.kind === "skill-catalog")) {
|
|
610
|
-
decision.messages = decision.messages.map((m) => filterSkillCatalogMessage(m, server.disabledSkills));
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
return decision;
|
|
614
|
-
});
|
|
985
|
+
// 问卷/技能目录钩子不再挂 host:agent-scoped 事件 host 收不到(且新版
|
|
986
|
+
// UserQuestionService 已删 registerProvider,旧写法直接导致 boot 失败)。
|
|
987
|
+
// 二者改由每次会话创建的 setup() 挂到 agent scope(见 agentSetup)。
|
|
615
988
|
let exitTask;
|
|
616
989
|
const disposeAndExit = () => {
|
|
617
990
|
exitTask ??= (async () => {
|
|
@@ -640,6 +1013,17 @@ function apply(ctx, config) {
|
|
|
640
1013
|
}
|
|
641
1014
|
|
|
642
1015
|
const name = "sdk-jsonrpc-server";
|
|
643
|
-
const inject = [
|
|
1016
|
+
const inject = [
|
|
1017
|
+
"agents",
|
|
1018
|
+
"goals",
|
|
1019
|
+
"attachments",
|
|
1020
|
+
"userQuestions",
|
|
1021
|
+
"llm",
|
|
1022
|
+
"tools",
|
|
1023
|
+
"skills",
|
|
1024
|
+
"systemPrompt",
|
|
1025
|
+
"permissionPresets",
|
|
1026
|
+
"approval",
|
|
1027
|
+
];
|
|
644
1028
|
|
|
645
1029
|
export { Config, apply, inject, name };
|
|
@@ -60,6 +60,11 @@ if (userPatchDir) {
|
|
|
60
60
|
}
|
|
61
61
|
userPatchFiles.sort((a, b) => a.localeCompare(b));
|
|
62
62
|
|
|
63
|
+
// preset-plane patch 路径(服务端生成的 agent-plane 下沉层,见
|
|
64
|
+
// server/dsh/preset-clones.ts)。实际加载在 boot import 之后;缺失/不可读 =
|
|
65
|
+
// 回落 legacy 单组合(roster 不挂,会话走 host 直连),静默跳过。
|
|
66
|
+
const presetPlanePatch = process.env.PI_WEB_DSH_PRESET_PATCH ?? null;
|
|
67
|
+
|
|
63
68
|
// ---------------------------------------------------------------------------
|
|
64
69
|
// boot
|
|
65
70
|
// ---------------------------------------------------------------------------
|
|
@@ -105,11 +110,23 @@ const userPatchLists = userPatchFiles.map((file) => {
|
|
|
105
110
|
}
|
|
106
111
|
});
|
|
107
112
|
for (const list of userPatchLists) for (const patch of list) fixJsonrpcName(patch);
|
|
113
|
+
// preset-plane patch(服务端生成;override 之后、用户 patch 之前)。
|
|
114
|
+
let presetPlaneLists = [];
|
|
115
|
+
if (presetPlanePatch) {
|
|
116
|
+
try {
|
|
117
|
+
presetPlaneLists = loadOverlayPatches(BIN_NAME, presetPlanePatch);
|
|
118
|
+
} catch (err) {
|
|
119
|
+
process.stderr.write(`[${BIN_NAME}] 跳过 preset-plane patch ${presetPlanePatch}: ${err?.message ?? String(err)}\n`);
|
|
120
|
+
presetPlaneLists = [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (const patch of presetPlaneLists) fixJsonrpcName(patch);
|
|
108
124
|
// patches 参数必须是扁平列表(boot → mountRootInclude → Include.applyPatches →
|
|
109
125
|
// applyEntryPatches 逐个消费;数组嵌套会被当无 id 的 patch 跳过)。
|
|
110
126
|
const patches = [
|
|
111
127
|
...loadOverlayPatches(BIN_NAME, baseBundlePatch),
|
|
112
128
|
...overrideList,
|
|
129
|
+
...presetPlaneLists,
|
|
113
130
|
// 用户 patch:同样按文件展开为扁平 patch entry 列表(一个文件可含多 entry)。
|
|
114
131
|
...userPatchLists.flat(),
|
|
115
132
|
];
|
|
@@ -18,10 +18,11 @@
|
|
|
18
18
|
model: !!js process.env.DSH_DEFAULT_MODEL ?? 'deepseek-v4-flash'
|
|
19
19
|
|
|
20
20
|
# 部署人设(默认空;pi-web-ui 设置面板可注入)。
|
|
21
|
+
# 键名必须是 personaPrefix(dsh-system-prompt schema;persona: 会被 zod 静默丢弃)。
|
|
21
22
|
- id: system-prompt
|
|
22
23
|
name: "@deepseek-ai/dsh-system-prompt"
|
|
23
24
|
config:
|
|
24
|
-
|
|
25
|
+
personaPrefix: !!js process.env.DSH_PERSONA ?? ''
|
|
25
26
|
|
|
26
27
|
# 沙箱工作区根(workspace-write 权限模式的护栏边界)。
|
|
27
28
|
- id: sandbox-policy
|
|
@@ -59,6 +60,23 @@
|
|
|
59
60
|
|
|
60
61
|
# ── 新行(plain insert)─────────────────────────────────────────────────────
|
|
61
62
|
- insert:
|
|
63
|
+
# pi-web-ui 自定义系统提示词(设置面板):独立 host section,随 standard/ptc/
|
|
64
|
+
# cordis 下发;minimal(complete:true)按官方语义压住。文本走环境变量
|
|
65
|
+
# PI_WEB_DSH_CUSTOM_PROMPT(Node 侧维护,变化即重启运行时)。
|
|
66
|
+
- id: pi-webui-custom-prompt
|
|
67
|
+
name: ./custom-prompt.mjs
|
|
68
|
+
# PTC mode 的 TypeScript 运行时(preset 的 tool-presentation 行等它;
|
|
69
|
+
# 无此行时 ptc 预设能挂但不出 run_code,PTC 名存实亡)。
|
|
70
|
+
- id: code-runtime
|
|
71
|
+
name: "@deepseek-ai/dsh-code-runtime-worker-thread"
|
|
72
|
+
# 子代理模型选择开关(standard/ptc/cordis 的 tool-subagent 行
|
|
73
|
+
# modelSelectionSettings: true 要求它在 Host scope;缺失则整组 mount 失败)。
|
|
74
|
+
- id: subagent-model-selection-settings
|
|
75
|
+
name: "@deepseek-ai/dsh-tool-subagent/model-selection-settings"
|
|
76
|
+
# cordis 模式的宿主执行器(tool-cordis 等 dynamicCordisRunner/cordisInspect;
|
|
77
|
+
# 缺失则 cordis 预设 mount 失败)。与官方 dsh-web-app 的 cordis-host-runner 同位。
|
|
78
|
+
- id: cordis-host-runner
|
|
79
|
+
name: "@deepseek-ai/dsh-cordis-host-runner"
|
|
62
80
|
# 模型提问工具:ask_user_question → ctx.userQuestions(base 已挂 user-questions
|
|
63
81
|
# 服务)→ goal-rpc wrapper 注册的 provider 桥到浏览器对话框。
|
|
64
82
|
- id: tool-ask-user
|