pi-web-ui 0.92.0 → 0.94.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 +74 -2
- package/README.md +69 -14
- package/README.zh-CN.md +36 -10
- package/bin/pi-web-ui.mjs +18 -6
- package/dist/server/agent-service.js +577 -127
- package/dist/server/auth-cookie.js +26 -0
- package/dist/server/claim-files-tool.js +6 -7
- package/dist/server/client-state.js +125 -8
- package/dist/server/conversation-read-tool.js +2 -1
- package/dist/server/delegate-task.js +10 -3
- package/dist/server/dsh/dsh-agent-service.js +43 -1
- package/dist/server/files-service.js +79 -27
- package/dist/server/http-proxy.js +72 -0
- package/dist/server/index.js +84 -17
- package/dist/server/patch-node-pty.js +11 -0
- package/dist/server/plugins.js +1 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +37 -3
- package/dist/server/resolve-global-sdk.js +72 -0
- package/dist/server/sdk-origin.js +64 -0
- package/dist/server/serialize.js +59 -21
- package/dist/server/settings-service.js +12 -1
- package/dist/server/skill-tool.js +2 -1
- package/dist/server/soft-cap.js +38 -4
- package/dist/server/subagents.js +30 -13
- package/dist/server/terminals.js +38 -18
- package/dist/server/themes.js +15 -0
- package/dist/server/tool-info.js +89 -0
- package/dist/server/tool-manager.js +107 -14
- package/package.json +2 -2
- package/plugins/catalog.json +10 -0
- package/themes/ayu-light.css +136 -0
- package/themes/catppuccin-latte.css +4 -0
- package/themes/catppuccin.css +4 -0
- package/themes/codex.css +136 -0
- package/themes/cyberpunk.css +4 -0
- package/themes/dazzle.css +4 -0
- package/themes/everforest-light.css +136 -0
- package/themes/geist.css +136 -0
- package/themes/gruvbox-light.css +136 -0
- package/themes/kanagawa-lotus.css +136 -0
- package/themes/md-preview.css +4 -0
- package/themes/mist.css +4 -0
- package/themes/nord.css +4 -0
- package/themes/one-dark.css +4 -0
- package/themes/paper.css +4 -0
- package/themes/rose-pine-dawn.css +136 -0
- package/themes/sakura.css +4 -0
- package/themes/solarized-light.css +4 -0
- package/themes/tokyo-night.css +4 -0
- package/themes/white.css +4 -0
- package/web/dist/assets/{TerminalPanel-RACpk6AX.js → TerminalPanel-B-CMQuzm.js} +1 -1
- package/web/dist/assets/index-18jK6al2.js +364 -0
- package/web/dist/assets/index-B3S9MxnN.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BgrnP9Jz.js +0 -364
- package/web/dist/assets/index-Cy8dSTZt.css +0 -1
|
@@ -21,3 +21,29 @@ export function isTlsRequest(req) {
|
|
|
21
21
|
export function buildPiWebTokenCookie(encodedValue, maxAge, secure) {
|
|
22
22
|
return `pi_web_token=${encodedValue}; Path=/; HttpOnly; SameSite=Strict; Max-Age=${maxAge}${secure ? "; Secure" : ""}`;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* `pi_web_token` cookie 值 → 原始口令。
|
|
26
|
+
*
|
|
27
|
+
* 下发时按 `encodeURIComponent` 编码(RFC 6265 的 cookie-value 只允许 ASCII,
|
|
28
|
+
* `=` / `+` / 空格 / 非 ASCII 都必须转义),所以**比对前必须先解码** —— 否则
|
|
29
|
+
* 含这些字符的口令永远匹配不上,表现为「`?token=` 那一次 200,之后所有资源与
|
|
30
|
+
* WS 全 401」(issue #261:base64 口令尾巴上的 `=` 被存成 `%3D`,服务端却拿
|
|
31
|
+
* `%3D` 去和 `=` 比)。
|
|
32
|
+
*
|
|
33
|
+
* 浏览器回来的一定是编码值,但手写 cookie / 旧客户端可能是明文(`=` 在值里
|
|
34
|
+
* 合法,我们按首个 `=` 切分),所以调用方应把「原样」与「解码后」两种候选都
|
|
35
|
+
* 试一遍(见 index.ts 的 requestTokens)。
|
|
36
|
+
*
|
|
37
|
+
* 不是合法百分号编码时原样返回:`decodeURIComponent` 会抛 `URIError`,
|
|
38
|
+
* 而一个手写的脏 cookie 绝不该把请求打成 500。
|
|
39
|
+
*/
|
|
40
|
+
export function decodeCookieToken(raw) {
|
|
41
|
+
if (!raw)
|
|
42
|
+
return "";
|
|
43
|
+
try {
|
|
44
|
+
return decodeURIComponent(raw);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -6,18 +6,17 @@
|
|
|
6
6
|
// 强制锁会死锁(见 claim-store.ts 文件头)。先到先得;自己的可刷新,
|
|
7
7
|
// 别人的动不了;过期自动释放 + 发 prompt 心跳续期 + 对话关闭释放。
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// 设置行等 web/src 空出来后补(follow-up)。DSH 引擎无 customTool 注册面,
|
|
13
|
-
// 只有 pi 引擎对话能认领(提醒是服务端算的,DSH 照样能看到别人的认领)。
|
|
9
|
+
// 开关走统一工具 tab(AGENT_TOOL_CATALOG 目录 + 设置页开关行;ActiveSet 门控,
|
|
10
|
+
// live 生效):认领是纯 advisory(关掉只会少提醒),默认开。DSH 引擎无 customTool
|
|
11
|
+
// 注册面,只有 pi 引擎对话能认领(提醒是服务端算的,DSH 照样能看到别人的认领)。
|
|
14
12
|
// ---------------------------------------------------------------------------
|
|
15
13
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
16
14
|
import { Type } from "typebox";
|
|
17
15
|
import { bilingual, pick } from "./i18n.js";
|
|
16
|
+
import { CLAIM_FILES_TOOL_NAME } from "./tool-manager.js";
|
|
18
17
|
import { CLAIM_TTL_MS, resolveClaimPath } from "./claim-store.js";
|
|
19
|
-
/**
|
|
20
|
-
export
|
|
18
|
+
/** 工具名(唯一定义在 tool-manager.ts,本模块 re-export 供单测沿用,模式同 present-files-tool.ts)。 */
|
|
19
|
+
export { CLAIM_FILES_TOOL_NAME };
|
|
21
20
|
function fmtTtl(ms) {
|
|
22
21
|
const m = Math.round(ms / 60000);
|
|
23
22
|
return m >= 60 ? `${Math.floor(m / 60)}h${m % 60 ? `${m % 60}m` : ""}` : `${m}m`;
|
|
@@ -12,6 +12,40 @@ import { normalizeSoftCapByModel, normalizeSoftCapTokens } from "./soft-cap.js";
|
|
|
12
12
|
import { deriveLegacy, legacyToDisabled, normalizeDisabledAgentTools } from "./tool-manager.js";
|
|
13
13
|
/** 大模型 API 出错自动重试次数的默认值(SDK 默认 3)。 */
|
|
14
14
|
export const DEFAULT_RETRY_MAX_ATTEMPTS = 6;
|
|
15
|
+
/** 工具执行看门狗超时的默认值(默认 20 分钟;环境变量 PI_WEB_TOOL_TIMEOUT_MS 覆盖)。 */
|
|
16
|
+
export const DEFAULT_TOOL_WATCHDOG_TIMEOUT_MS = (() => {
|
|
17
|
+
const v = Number(process.env.PI_WEB_TOOL_TIMEOUT_MS);
|
|
18
|
+
return Number.isFinite(v) && v >= 0 ? v : 20 * 60_000;
|
|
19
|
+
})();
|
|
20
|
+
/** 归一化工具看门狗超时(毫秒):0 = 禁用;非数值、负数或空值回落默认值,非负整数保留。 */
|
|
21
|
+
export function normalizeToolWatchdogTimeoutMs(v) {
|
|
22
|
+
if (v === null || v === undefined || v === "")
|
|
23
|
+
return DEFAULT_TOOL_WATCHDOG_TIMEOUT_MS;
|
|
24
|
+
const n = Math.floor(Number(v));
|
|
25
|
+
if (!Number.isFinite(n) || n < 0)
|
|
26
|
+
return DEFAULT_TOOL_WATCHDOG_TIMEOUT_MS;
|
|
27
|
+
return n;
|
|
28
|
+
}
|
|
29
|
+
/** 工具自己声明超时时留给它的余量(毫秒):让工具先自己超时并返回错误,
|
|
30
|
+
* 而不是看门狗先下手把整轮对话 abort 掉。 */
|
|
31
|
+
export const TOOL_WATCHDOG_EXPLICIT_GRACE_MS = 5_000;
|
|
32
|
+
/** 计算单次工具调用的有效看门狗超时(毫秒;≤0 = 不布看门狗)。
|
|
33
|
+
* 基础值来自设置/环境变量(见 DEFAULT_TOOL_WATCHDOG_TIMEOUT_MS);**若工具调用
|
|
34
|
+
* 自己声明了更长的超时**(目前只有 bash 的 `args.timeout`,单位秒),自动顺延到
|
|
35
|
+
* 「工具超时 + 余量」—— 否则 AI 明确要求跑 90 分钟的命令,会被 20 分钟的看门狗
|
|
36
|
+
* 连同整轮对话一起剁掉。纯函数,可单测。 */
|
|
37
|
+
export function effectiveToolWatchdogMs(baseMs, toolName, args) {
|
|
38
|
+
if (!Number.isFinite(baseMs) || baseMs <= 0)
|
|
39
|
+
return 0;
|
|
40
|
+
let ms = baseMs;
|
|
41
|
+
if (toolName === "bash" && args && typeof args === "object") {
|
|
42
|
+
const raw = args.timeout;
|
|
43
|
+
const sec = typeof raw === "number" ? raw : Number(raw);
|
|
44
|
+
if (Number.isFinite(sec) && sec > 0)
|
|
45
|
+
ms = Math.max(ms, sec * 1000 + TOOL_WATCHDOG_EXPLICIT_GRACE_MS);
|
|
46
|
+
}
|
|
47
|
+
return ms;
|
|
48
|
+
}
|
|
15
49
|
/** 归一化插件 AI 工具禁用名单:只收非空字符串(去重,上限 256 个)。
|
|
16
50
|
* 与 disabledAgentTools 不同——插件工具名是动态的(注册才知道),不能按
|
|
17
51
|
* 固定目录校验;未知/已卸载插件的条目刻意保留(重装后仍保持关闭)。
|
|
@@ -285,15 +319,20 @@ export class ClientStateStore {
|
|
|
285
319
|
state.lastCwd = cwd;
|
|
286
320
|
const now = Date.now();
|
|
287
321
|
state.projects = [{ path: cwd, lastUsed: now }, ...state.projects.filter((p) => p.path !== cwd)].slice(0, 30);
|
|
288
|
-
// Opening the workspace again clears its removal tombstone.
|
|
289
|
-
|
|
290
|
-
|
|
322
|
+
// Opening the workspace again clears its removal tombstone across all clients and global settings.
|
|
323
|
+
for (const cState of Object.values(all)) {
|
|
324
|
+
if (cState.removedProjects?.length) {
|
|
325
|
+
cState.removedProjects = cState.removedProjects.filter((p) => p !== cwd);
|
|
326
|
+
}
|
|
291
327
|
}
|
|
292
328
|
this.save();
|
|
293
329
|
}
|
|
294
330
|
/** Drop one workspace from the recent-project list (user-requested removal).
|
|
295
331
|
* Records a tombstone too: pushProjects() re-discovers cwds from session
|
|
296
|
-
* files on every listing, so without it the entry would instantly reappear.
|
|
332
|
+
* files on every listing, so without it the entry would instantly reappear.
|
|
333
|
+
* Recorded in both the requesting client and GLOBAL_SETTINGS_KEY so the
|
|
334
|
+
* removal stays across all browser tabs and server restarts until the user
|
|
335
|
+
* explicitly opens that project again. */
|
|
297
336
|
removeProject(clientId, cwd) {
|
|
298
337
|
const all = this.load();
|
|
299
338
|
const state = (all[clientId] ??= { projects: [] });
|
|
@@ -303,12 +342,28 @@ export class ClientStateStore {
|
|
|
303
342
|
const removed = new Set(state.removedProjects ?? []);
|
|
304
343
|
removed.add(cwd);
|
|
305
344
|
state.removedProjects = [...removed];
|
|
345
|
+
const globalState = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
|
|
346
|
+
const globalRemoved = new Set(globalState.removedProjects ?? []);
|
|
347
|
+
globalRemoved.add(cwd);
|
|
348
|
+
globalState.removedProjects = [...globalRemoved];
|
|
349
|
+
for (const [id, cState] of Object.entries(all)) {
|
|
350
|
+
if (id !== ClientStateStore.GLOBAL_SETTINGS_KEY && cState.projects) {
|
|
351
|
+
cState.projects = cState.projects.filter((p) => p.path !== cwd);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
306
354
|
this.save();
|
|
307
355
|
}
|
|
308
356
|
/** Tombstoned projects (explicitly removed by the user) for filtering the
|
|
309
|
-
* merged recent-project list. */
|
|
357
|
+
* merged recent-project list across clients. */
|
|
310
358
|
getRemovedProjects(clientId) {
|
|
311
|
-
|
|
359
|
+
const all = this.load();
|
|
360
|
+
const clientRemoved = all[clientId]?.removedProjects ?? [];
|
|
361
|
+
const globalRemoved = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.removedProjects ?? [];
|
|
362
|
+
if (globalRemoved.length === 0)
|
|
363
|
+
return clientRemoved;
|
|
364
|
+
if (clientRemoved.length === 0)
|
|
365
|
+
return globalRemoved;
|
|
366
|
+
return [...new Set([...clientRemoved, ...globalRemoved])];
|
|
312
367
|
}
|
|
313
368
|
/** Last-used goal/review prefs for a client, or undefined if never set. */
|
|
314
369
|
getGoalPrefs(clientId) {
|
|
@@ -419,6 +474,7 @@ export class ClientStateStore {
|
|
|
419
474
|
: (stored?.terminalToolsEnabled ?? false),
|
|
420
475
|
terminalBash: stored?.terminalBash ?? false,
|
|
421
476
|
terminalBashIdleMs: stored?.terminalBashIdleMs ?? 15_000,
|
|
477
|
+
toolWatchdogTimeoutMs: normalizeToolWatchdogTimeoutMs(stored?.toolWatchdogTimeoutMs),
|
|
422
478
|
readDirEnabled: stored?.readDirEnabled ?? true,
|
|
423
479
|
editSoftEnabled: stored?.disabledAgentTools !== undefined
|
|
424
480
|
? deriveLegacy(legacyToDisabled(stored)).editSoftEnabled
|
|
@@ -432,6 +488,7 @@ export class ClientStateStore {
|
|
|
432
488
|
devNoCache: stored?.devNoCache,
|
|
433
489
|
autoReload: stored?.autoReload,
|
|
434
490
|
toolsWrap: stored?.toolsWrap ?? true,
|
|
491
|
+
toolImagesEnabled: stored?.toolImagesEnabled ?? true,
|
|
435
492
|
skillsFullText: normalizeSkillList(stored?.skillsFullText),
|
|
436
493
|
visionBridgeEnabled: stored?.visionBridgeEnabled ?? true,
|
|
437
494
|
visionBridgeModel: stored?.visionBridgeModel ?? null,
|
|
@@ -474,6 +531,7 @@ export class ClientStateStore {
|
|
|
474
531
|
terminalToolsEnabled: settings.terminalToolsEnabled ?? cur.terminalToolsEnabled ?? false,
|
|
475
532
|
terminalBash: settings.terminalBash ?? cur.terminalBash ?? false,
|
|
476
533
|
terminalBashIdleMs: settings.terminalBashIdleMs ?? cur.terminalBashIdleMs ?? 15_000,
|
|
534
|
+
toolWatchdogTimeoutMs: normalizeToolWatchdogTimeoutMs(settings.toolWatchdogTimeoutMs ?? cur.toolWatchdogTimeoutMs ?? DEFAULT_TOOL_WATCHDOG_TIMEOUT_MS),
|
|
477
535
|
readDirEnabled: settings.readDirEnabled ?? cur.readDirEnabled ?? true,
|
|
478
536
|
editSoftEnabled: settings.editSoftEnabled ?? cur.editSoftEnabled ?? false,
|
|
479
537
|
questionnaireEnabled: settings.questionnaireEnabled ?? cur.questionnaireEnabled ?? true,
|
|
@@ -483,6 +541,7 @@ export class ClientStateStore {
|
|
|
483
541
|
devNoCache: settings.devNoCache ?? cur.devNoCache,
|
|
484
542
|
autoReload: settings.autoReload ?? cur.autoReload,
|
|
485
543
|
toolsWrap: settings.toolsWrap ?? cur.toolsWrap ?? true,
|
|
544
|
+
toolImagesEnabled: settings.toolImagesEnabled ?? cur.toolImagesEnabled ?? true,
|
|
486
545
|
skillsFullText: normalizeSkillList(settings.skillsFullText ?? cur.skillsFullText),
|
|
487
546
|
visionBridgeEnabled: settings.visionBridgeEnabled ?? cur.visionBridgeEnabled ?? true,
|
|
488
547
|
visionBridgeModel: settings.visionBridgeModel ?? cur.visionBridgeModel ?? null,
|
|
@@ -532,11 +591,28 @@ export class ClientStateStore {
|
|
|
532
591
|
}
|
|
533
592
|
/** Get a single provider's saved key for a project. */
|
|
534
593
|
getProjectProviderKey(clientId, cwd, provider) {
|
|
535
|
-
|
|
594
|
+
const all = this.load();
|
|
595
|
+
const globalHit = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectProviderKeys?.[cwd]?.[provider];
|
|
596
|
+
if (globalHit)
|
|
597
|
+
return globalHit;
|
|
598
|
+
const direct = all[clientId]?.projectProviderKeys?.[cwd]?.[provider];
|
|
599
|
+
if (direct)
|
|
600
|
+
return direct;
|
|
601
|
+
for (const [id, state] of Object.entries(all)) {
|
|
602
|
+
if (id === clientId || id === ClientStateStore.GLOBAL_SETTINGS_KEY)
|
|
603
|
+
continue;
|
|
604
|
+
const hit = state.projectProviderKeys?.[cwd]?.[provider];
|
|
605
|
+
if (hit)
|
|
606
|
+
return hit;
|
|
607
|
+
}
|
|
608
|
+
return undefined;
|
|
536
609
|
}
|
|
537
610
|
/** Remember which key was last used for a provider in a project. */
|
|
538
611
|
saveProjectProviderKey(clientId, cwd, provider, keyName) {
|
|
539
612
|
const all = this.load();
|
|
613
|
+
const globalState = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
|
|
614
|
+
const gMap = (globalState.projectProviderKeys ??= {});
|
|
615
|
+
(gMap[cwd] ??= {})[provider] = keyName;
|
|
540
616
|
const state = (all[clientId] ??= { projects: [] });
|
|
541
617
|
const map = (state.projectProviderKeys ??= {});
|
|
542
618
|
const inner = (map[cwd] ??= {});
|
|
@@ -546,6 +622,13 @@ export class ClientStateStore {
|
|
|
546
622
|
/** Delete a per-project provider key (e.g. when the key is removed). */
|
|
547
623
|
deleteProjectProviderKey(clientId, cwd, provider) {
|
|
548
624
|
const all = this.load();
|
|
625
|
+
const gInner = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectProviderKeys?.[cwd];
|
|
626
|
+
if (gInner && provider in gInner) {
|
|
627
|
+
delete gInner[provider];
|
|
628
|
+
if (Object.keys(gInner).length === 0) {
|
|
629
|
+
delete all[ClientStateStore.GLOBAL_SETTINGS_KEY].projectProviderKeys[cwd];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
549
632
|
const inner = all[clientId]?.projectProviderKeys?.[cwd];
|
|
550
633
|
if (!inner || !(provider in inner))
|
|
551
634
|
return;
|
|
@@ -613,11 +696,39 @@ export class ClientStateStore {
|
|
|
613
696
|
}
|
|
614
697
|
/** Get the model the user last selected in a project, or undefined. */
|
|
615
698
|
getProjectModel(clientId, cwd) {
|
|
616
|
-
|
|
699
|
+
const all = this.load();
|
|
700
|
+
// 1. 全局项目模型记忆(跨标签页、跨客户端、重启浏览器共享,优先级最高)
|
|
701
|
+
const globalHit = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectModels?.[cwd];
|
|
702
|
+
if (globalHit)
|
|
703
|
+
return globalHit;
|
|
704
|
+
// 2. 本客户端记忆
|
|
705
|
+
const direct = all[clientId]?.projectModels?.[cwd];
|
|
706
|
+
if (direct)
|
|
707
|
+
return direct;
|
|
708
|
+
// 3. 其他客户端按最近活跃时间倒序查找兜底
|
|
709
|
+
const candidates = [];
|
|
710
|
+
for (const [id, state] of Object.entries(all)) {
|
|
711
|
+
if (id === clientId || id === ClientStateStore.GLOBAL_SETTINGS_KEY)
|
|
712
|
+
continue;
|
|
713
|
+
const m = state.projectModels?.[cwd];
|
|
714
|
+
if (m) {
|
|
715
|
+
const p = state.projects?.find((proj) => proj.path === cwd);
|
|
716
|
+
candidates.push({ model: m, lastUsed: p?.lastUsed ?? 0 });
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (candidates.length > 0) {
|
|
720
|
+
candidates.sort((a, b) => b.lastUsed - a.lastUsed);
|
|
721
|
+
return candidates[0].model;
|
|
722
|
+
}
|
|
723
|
+
return undefined;
|
|
617
724
|
}
|
|
618
725
|
/** Remember the model last selected in a project (immediate, not after a turn). */
|
|
619
726
|
saveProjectModel(clientId, cwd, modelId) {
|
|
620
727
|
const all = this.load();
|
|
728
|
+
// 写入全局项目记忆(保证新标签页/重启浏览器时确定可用)
|
|
729
|
+
const globalState = (all[ClientStateStore.GLOBAL_SETTINGS_KEY] ??= { projects: [] });
|
|
730
|
+
(globalState.projectModels ??= {})[cwd] = modelId;
|
|
731
|
+
// 同时写入本客户端
|
|
621
732
|
const state = (all[clientId] ??= { projects: [] });
|
|
622
733
|
(state.projectModels ??= {})[cwd] = modelId;
|
|
623
734
|
this.save();
|
|
@@ -626,6 +737,12 @@ export class ClientStateStore {
|
|
|
626
737
|
* removed from the catalog). */
|
|
627
738
|
deleteProjectModel(clientId, cwd) {
|
|
628
739
|
const all = this.load();
|
|
740
|
+
const gMap = all[ClientStateStore.GLOBAL_SETTINGS_KEY]?.projectModels;
|
|
741
|
+
if (gMap && cwd in gMap) {
|
|
742
|
+
delete gMap[cwd];
|
|
743
|
+
if (Object.keys(gMap).length === 0)
|
|
744
|
+
delete all[ClientStateStore.GLOBAL_SETTINGS_KEY].projectModels;
|
|
745
|
+
}
|
|
629
746
|
const map = all[clientId]?.projectModels;
|
|
630
747
|
if (!map || !(cwd in map))
|
|
631
748
|
return;
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { Type } from "typebox";
|
|
24
24
|
import { bilingual, pick } from "./i18n.js";
|
|
25
|
+
import { CONVERSATION_READ_TOOL_NAME } from "./tool-manager.js";
|
|
25
26
|
import { extractTouches, formatTouchEntry, formatTouchesCompact, toolCallRefsOfContent, unionTouchLists, } from "./conversation-touches.js";
|
|
26
27
|
/** 由 content 块提 toolCalls 字段(additive;格式化输出不受影响,见接口注释)。 */
|
|
27
28
|
function fillToolCalls(target, content) {
|
|
@@ -366,7 +367,7 @@ export function makeConversationReadTool(host, lang, extras) {
|
|
|
366
367
|
details,
|
|
367
368
|
});
|
|
368
369
|
return defineTool({
|
|
369
|
-
name:
|
|
370
|
+
name: CONVERSATION_READ_TOOL_NAME,
|
|
370
371
|
label: "Read another conversation",
|
|
371
372
|
description: bilingual('Read ANOTHER conversation: a running conversation of this client (including subagents — use its conversation id like "c3", see action=list) or a persisted history session transcript (use its file path). ' +
|
|
372
373
|
'Use it when the user references another chat (a quoted conversation, a pasted conversation id, or "see the other chat about X"). ' +
|
|
@@ -17,9 +17,10 @@
|
|
|
17
17
|
import { Type } from "typebox";
|
|
18
18
|
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { bilingual, pick } from "./i18n.js";
|
|
20
|
+
import { DELEGATE_TASK_TOOL_NAME } from "./tool-manager.js";
|
|
20
21
|
import { subagentTitle } from "./subagents.js";
|
|
21
|
-
/**
|
|
22
|
-
export const DELEGATE_TOOL_NAME =
|
|
22
|
+
/** 工具名(唯一值在 tool-manager.ts;本别名保留:前端派单卡片与旧导入沿用它)。 */
|
|
23
|
+
export const DELEGATE_TOOL_NAME = DELEGATE_TASK_TOOL_NAME;
|
|
23
24
|
/** 各段最小长度(trim 后字符数):TASK 必须具体,OUTCOME 必须可验收,其余段不许空着。 */
|
|
24
25
|
const MIN_TASK = 20;
|
|
25
26
|
const MIN_OUTCOME = 10;
|
|
@@ -40,6 +41,7 @@ export function normalizeDelegation(params) {
|
|
|
40
41
|
must_not_do: str(p.must_not_do),
|
|
41
42
|
context: str(p.context),
|
|
42
43
|
model: str(p.model).trim() || undefined,
|
|
44
|
+
persist: typeof p.persist === "boolean" ? p.persist : undefined,
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
/**
|
|
@@ -122,6 +124,11 @@ const delegateSchema = Type.Object({
|
|
|
122
124
|
model: Type.Optional(Type.String({
|
|
123
125
|
description: bilingual('Optional model "provider/id" for this delegation; omit = template model → panel default → follow the main conversation model.', "可选:本次派单的子代理模型(provider/id);不传 = 模板模型 → 面板默认 → 跟随主对话模型。"),
|
|
124
126
|
})),
|
|
127
|
+
persist: Type.Optional(Type.Boolean({
|
|
128
|
+
description: bilingual("Optional: persist this conversation to disk as a regular session (saved in history, resumable). " +
|
|
129
|
+
"Default false (lightweight in-memory subagent). Use true for tasks that need long-term retention.", "可选:是否将该对话持久化落盘为普通对话(保存在历史会话中,可随时回顾与继续)。" +
|
|
130
|
+
"默认 false(轻量内存子代理)。需要长期留存的任务建议设为 true。"),
|
|
131
|
+
})),
|
|
125
132
|
});
|
|
126
133
|
/** 结构化派单工具:校验六段 → 拼装标准 prompt → 走 host.spawnSubagent 真子代理。 */
|
|
127
134
|
export function makeDelegateTaskTool(host, lang) {
|
|
@@ -163,7 +170,7 @@ export function makeDelegateTaskTool(host, lang) {
|
|
|
163
170
|
// 创建失败都经由 host 抛错),转成返回文本让 AI 能读到原因并修正重试。
|
|
164
171
|
let convId;
|
|
165
172
|
try {
|
|
166
|
-
convId = await host.spawnSubagent(prompt, "delegate", ctx.cwd, input.agent, input.model);
|
|
173
|
+
convId = await host.spawnSubagent(prompt, "delegate", ctx.cwd, input.agent, input.model, undefined, input.persist);
|
|
167
174
|
}
|
|
168
175
|
catch (err) {
|
|
169
176
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -29,12 +29,14 @@ import { basename, dirname, join, resolve, sep } from "node:path";
|
|
|
29
29
|
import { randomUUID } from "node:crypto";
|
|
30
30
|
import { homedir } from "node:os";
|
|
31
31
|
import { BgServerTracker } from "../bg-servers.js";
|
|
32
|
-
import { ClientStateStore, DEFAULT_RETRY_MAX_ATTEMPTS } from "../client-state.js";
|
|
32
|
+
import { ClientStateStore, DEFAULT_RETRY_MAX_ATTEMPTS, normalizeToolWatchdogTimeoutMs } from "../client-state.js";
|
|
33
33
|
import { normalizeUiLayout } from "../client-state.js";
|
|
34
34
|
import { FilesService, workspacePath, desktopDirWire } from "../files-service.js";
|
|
35
35
|
import { QuiesceRejectedError } from "../agent-service.js";
|
|
36
36
|
import { NATIVE_COMMANDS, parseSlash } from "../slash-commands.js";
|
|
37
37
|
import { bilingual, pick, resolveServerLang } from "../i18n.js";
|
|
38
|
+
// 工具定义说明的归一化(工具卡右键 → 「显示工具详细信息」,见 getToolInfo)。
|
|
39
|
+
import { normalizeToolInfo, unsupportedToolInfo } from "../tool-info.js";
|
|
38
40
|
import { TerminalManager, loadCommands, saveCommandsFile } from "../terminals.js";
|
|
39
41
|
import { saveUpload } from "../uploads.js";
|
|
40
42
|
import { checkAll as checkAllUpdates, collectTargets, resolveNpmRegistry } from "../update-check.js";
|
|
@@ -122,12 +124,14 @@ const DEFAULT_SETTINGS = {
|
|
|
122
124
|
terminalToolsEnabled: false,
|
|
123
125
|
terminalBash: false,
|
|
124
126
|
terminalBashIdleMs: 15_000,
|
|
127
|
+
toolWatchdogTimeoutMs: 20 * 60_000,
|
|
125
128
|
editSoftEnabled: false,
|
|
126
129
|
questionnaireEnabled: true,
|
|
127
130
|
goalModeEnabled: true,
|
|
128
131
|
parallelReminderEnabled: true,
|
|
129
132
|
thinkingWrap: false,
|
|
130
133
|
toolsWrap: true,
|
|
134
|
+
toolImagesEnabled: true,
|
|
131
135
|
disabledPlugins: [],
|
|
132
136
|
uiLayout: {},
|
|
133
137
|
reviewPrompt: "",
|
|
@@ -267,12 +271,14 @@ export class DshClientSession {
|
|
|
267
271
|
terminalToolsEnabled: savedSettings.terminalToolsEnabled,
|
|
268
272
|
terminalBash: savedSettings.terminalBash,
|
|
269
273
|
terminalBashIdleMs: savedSettings.terminalBashIdleMs,
|
|
274
|
+
toolWatchdogTimeoutMs: savedSettings.toolWatchdogTimeoutMs ?? 20 * 60_000,
|
|
270
275
|
editSoftEnabled: savedSettings.editSoftEnabled,
|
|
271
276
|
questionnaireEnabled: savedSettings.questionnaireEnabled ?? true,
|
|
272
277
|
goalModeEnabled: savedSettings.goalModeEnabled ?? true,
|
|
273
278
|
parallelReminderEnabled: savedSettings.parallelReminderEnabled ?? true,
|
|
274
279
|
thinkingWrap: savedSettings.thinkingWrap,
|
|
275
280
|
toolsWrap: savedSettings.toolsWrap,
|
|
281
|
+
toolImagesEnabled: savedSettings.toolImagesEnabled ?? true,
|
|
276
282
|
disabledPlugins: savedSettings.disabledPlugins ?? [],
|
|
277
283
|
uiLayout: normalizeUiLayout(savedSettings.uiLayout),
|
|
278
284
|
reviewPrompt: savedSettings.reviewPrompt,
|
|
@@ -563,16 +569,22 @@ export class DshClientSession {
|
|
|
563
569
|
// 记下待答问卷:`question_pending` 只推给「当时在线」的连接,刷新页面
|
|
564
570
|
// /WS 重连后靠快照(UiState.pendingQuestion)把对话框恢复出来。
|
|
565
571
|
// DSH 的提问桥是 runtime 级的(无 conversationId),故不分对话。
|
|
572
|
+
const dshConv = this.convs.get(this.activeId);
|
|
573
|
+
const conversationTitle = dshConv?.title;
|
|
566
574
|
this.pendingQuestion = {
|
|
567
575
|
id: params0.id,
|
|
568
576
|
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
569
577
|
questions: mapped,
|
|
578
|
+
...(this.activeId ? { conversationId: this.activeId } : {}),
|
|
579
|
+
...(conversationTitle ? { conversationTitle } : {}),
|
|
570
580
|
};
|
|
571
581
|
this.emit({
|
|
572
582
|
type: "question_pending",
|
|
573
583
|
id: params0.id,
|
|
574
584
|
...(typeof params0.deadline === "number" ? { deadline: params0.deadline } : {}),
|
|
575
585
|
questions: mapped,
|
|
586
|
+
...(this.activeId ? { conversationId: this.activeId } : {}),
|
|
587
|
+
...(conversationTitle ? { conversationTitle } : {}),
|
|
576
588
|
});
|
|
577
589
|
}
|
|
578
590
|
else if (method === "tools.call.request") {
|
|
@@ -658,6 +670,28 @@ export class DshClientSession {
|
|
|
658
670
|
console.error(`[dsh] syncPluginTools 失败 (client=${this.clientId}):`, err);
|
|
659
671
|
}
|
|
660
672
|
}
|
|
673
|
+
/**
|
|
674
|
+
* 取一条工具的**定义说明**(工具卡右键 → 「显示工具详细信息」)→ `tool_info`。
|
|
675
|
+
*
|
|
676
|
+
* DSH 侧的定义来自运行时(`tools/list`:DSH 原生工具 + 桥接进来的插件工具)。
|
|
677
|
+
* 拿不到(运行时没起来 / RPC 报错)回 `unsupported`,让前端说「当前引擎不支持」
|
|
678
|
+
* 而不是「未找到工具定义」—— 后者会让用户以为工具名写错了。
|
|
679
|
+
*/
|
|
680
|
+
async getToolInfo(name) {
|
|
681
|
+
if (!this.runtime.alive) {
|
|
682
|
+
this.emit({ type: "tool_info", ...unsupportedToolInfo(name) });
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const res = await this.runtime.listTools();
|
|
687
|
+
const found = (res.tools ?? []).find((t) => t.name === name);
|
|
688
|
+
this.emit({ type: "tool_info", ...normalizeToolInfo(name, found) });
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
console.error(`[dsh] listTools 失败 (client=${this.clientId}):`, err);
|
|
692
|
+
this.emit({ type: "tool_info", ...unsupportedToolInfo(name) });
|
|
693
|
+
}
|
|
694
|
+
}
|
|
661
695
|
/** 从运行时拉取技能清单 → 缓存 → 重推 settings_state(enabled 由 disabledSkills 推导)。 */
|
|
662
696
|
async refreshSkillsFromRuntime() {
|
|
663
697
|
if (!this.runtime.alive)
|
|
@@ -2590,6 +2624,7 @@ export class DshClientSession {
|
|
|
2590
2624
|
terminalToolsEnabled: this.settings.terminalToolsEnabled,
|
|
2591
2625
|
terminalBash: this.settings.terminalBash,
|
|
2592
2626
|
terminalBashIdleMs: this.settings.terminalBashIdleMs,
|
|
2627
|
+
toolWatchdogTimeoutMs: this.settings.toolWatchdogTimeoutMs,
|
|
2593
2628
|
// DSH 引擎无 customTool 注册面(工具来自 shipped preset),read 目录覆盖面不存在。
|
|
2594
2629
|
readDirEnabled: true,
|
|
2595
2630
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
@@ -2603,6 +2638,7 @@ export class DshClientSession {
|
|
|
2603
2638
|
parallelReminderEnabled: this.settings.parallelReminderEnabled,
|
|
2604
2639
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2605
2640
|
toolsWrap: this.settings.toolsWrap,
|
|
2641
|
+
toolImagesEnabled: this.settings.toolImagesEnabled,
|
|
2606
2642
|
// DSH 无 skill 全文注入概念,给空保协议完整。
|
|
2607
2643
|
skillsFullText: [],
|
|
2608
2644
|
visionBridgeEnabled: false,
|
|
@@ -2659,6 +2695,8 @@ export class DshClientSession {
|
|
|
2659
2695
|
this.settings.terminalBash = partial.terminalBash;
|
|
2660
2696
|
if (partial.terminalBashIdleMs !== undefined)
|
|
2661
2697
|
this.settings.terminalBashIdleMs = partial.terminalBashIdleMs;
|
|
2698
|
+
if (partial.toolWatchdogTimeoutMs !== undefined)
|
|
2699
|
+
this.settings.toolWatchdogTimeoutMs = normalizeToolWatchdogTimeoutMs(partial.toolWatchdogTimeoutMs);
|
|
2662
2700
|
if (partial.editSoftEnabled !== undefined)
|
|
2663
2701
|
this.settings.editSoftEnabled = partial.editSoftEnabled;
|
|
2664
2702
|
if (partial.questionnaireEnabled !== undefined)
|
|
@@ -2671,6 +2709,8 @@ export class DshClientSession {
|
|
|
2671
2709
|
this.settings.thinkingWrap = partial.thinkingWrap;
|
|
2672
2710
|
if (partial.toolsWrap !== undefined)
|
|
2673
2711
|
this.settings.toolsWrap = partial.toolsWrap;
|
|
2712
|
+
if (partial.toolImagesEnabled !== undefined)
|
|
2713
|
+
this.settings.toolImagesEnabled = partial.toolImagesEnabled;
|
|
2674
2714
|
if (partial.disabledPlugins !== undefined)
|
|
2675
2715
|
this.settings.disabledPlugins = partial.disabledPlugins;
|
|
2676
2716
|
if (partial.uiLayout !== undefined)
|
|
@@ -2706,6 +2746,7 @@ export class DshClientSession {
|
|
|
2706
2746
|
parallelReminderEnabled: this.settings.parallelReminderEnabled,
|
|
2707
2747
|
thinkingWrap: this.settings.thinkingWrap,
|
|
2708
2748
|
toolsWrap: this.settings.toolsWrap,
|
|
2749
|
+
toolImagesEnabled: this.settings.toolImagesEnabled,
|
|
2709
2750
|
disabledPlugins: this.settings.disabledPlugins,
|
|
2710
2751
|
uiLayout: normalizeUiLayout(this.settings.uiLayout),
|
|
2711
2752
|
reviewPrompt: this.settings.reviewPrompt,
|
|
@@ -3071,6 +3112,7 @@ export class DshClientSession {
|
|
|
3071
3112
|
editSoftEnabled: this.settings.editSoftEnabled,
|
|
3072
3113
|
thinkingWrap: this.settings.thinkingWrap,
|
|
3073
3114
|
toolsWrap: this.settings.toolsWrap,
|
|
3115
|
+
toolImagesEnabled: this.settings.toolImagesEnabled,
|
|
3074
3116
|
disabledPlugins: this.settings.disabledPlugins,
|
|
3075
3117
|
uiLayout: normalizeUiLayout(this.settings.uiLayout),
|
|
3076
3118
|
reviewPrompt: this.settings.reviewPrompt,
|