pi-web-ui 0.96.0 → 0.97.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 +69 -2
- package/README.md +26 -0
- package/README.zh-CN.md +25 -0
- package/bin/pi-web-ui.mjs +11 -2
- package/dist/server/agent-service.js +1195 -120
- package/dist/server/client-state.js +89 -1
- package/dist/server/compacted-history.js +146 -0
- package/dist/server/delegate-mode.js +149 -0
- package/dist/server/delegate-task.js +10 -10
- package/dist/server/dsh/dsh-agent-service.js +35 -2
- package/dist/server/dsh/dsh-client.js +3 -1
- package/dist/server/eval-tool.js +3 -1
- package/dist/server/goal-review-gate.js +263 -0
- package/dist/server/goal-service.js +1225 -445
- package/dist/server/index.js +79 -20
- package/dist/server/lsp-tool.js +13 -10
- package/dist/server/markers/builtins/todo.js +0 -2
- package/dist/server/mcp-bridge.js +13 -0
- package/dist/server/patch-tool.js +1 -0
- package/dist/server/patch-turn-end-boundary.js +77 -0
- package/dist/server/permission-preset.js +44 -0
- package/dist/server/plan-mode.js +376 -0
- package/dist/server/plugin-facilities.js +36 -3
- package/dist/server/plugin-installer.js +30 -0
- package/dist/server/plugins.js +6 -3
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/serialize.js +11 -0
- package/dist/server/settings-service.js +20 -8
- package/dist/server/subagents.js +360 -408
- package/dist/server/text-sniff.js +15 -9
- package/dist/server/tool-manager.js +48 -4
- package/dist/server/update-check.js +11 -3
- package/dist/server/wait-subscription-scan.js +5 -1
- package/package.json +3 -3
- package/themes/zhupi-dark.css +79 -44
- package/themes/zhupi.css +79 -44
- package/web/dist/assets/{TerminalPanel-DDChcYOh.js → TerminalPanel-CITBqQrb.js} +1 -1
- package/web/dist/assets/index-DwToli0J.css +1 -0
- package/web/dist/assets/index-oI7UKOqJ.js +368 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-Do9RgJC3.js +0 -366
- package/web/dist/assets/index-DryOsILO.css +0 -1
package/dist/server/index.js
CHANGED
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
* all share one conversation list per project.
|
|
17
17
|
* PI_CODING_AGENT_DIR pi config dir (auth/models/skills) — passed to the SDK
|
|
18
18
|
*/
|
|
19
|
+
// MUST be imported before the SDK is loaded so on-disk patches apply cleanly.
|
|
20
|
+
import "./patch-remote-catalog.js";
|
|
21
|
+
import "./patch-turn-end-boundary.js";
|
|
19
22
|
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
20
23
|
import { stat } from "node:fs/promises";
|
|
21
24
|
import { createServer, request as proxyRequest } from "node:http";
|
|
@@ -48,7 +51,7 @@ import { validateClientId } from "./ws-client-id.js";
|
|
|
48
51
|
import { PendingCommandQueue } from "./ws-pending-queue.js";
|
|
49
52
|
import { installPack, isKnownPack, listPacks, loadServerStrings, readPackFile, removePack, unloadServerStrings, } from "./locales.js";
|
|
50
53
|
import { PluginManager, resolvePluginClientFile, } from "./plugins.js";
|
|
51
|
-
import { buildPluginJobArgs, inspectInstallSpec, PluginInstaller } from "./plugin-installer.js";
|
|
54
|
+
import { buildPluginJobArgs, confirmPluginInstall, inspectInstallSpec, PluginInstaller } from "./plugin-installer.js";
|
|
52
55
|
import { syncPluginCatalog } from "./plugin-catalog-sync.js";
|
|
53
56
|
import { McpBridge } from "./mcp-bridge.js";
|
|
54
57
|
import { createMcpHotReload } from "./mcp-hot-reload.js";
|
|
@@ -1129,22 +1132,14 @@ function findDomConsentById(id) {
|
|
|
1129
1132
|
}
|
|
1130
1133
|
// ---------------------------------------------------------------------------
|
|
1131
1134
|
// 插件安装的用户确认门(P0):plugin_catalog_sync 的 install:true 与 plugin_job
|
|
1132
|
-
// 的 install/update
|
|
1133
|
-
// 这两类消息,没有这道门就能借 WS 静默安装任意插件。复用 permissionRequester
|
|
1134
|
-
// 的同一条「plugin_permission_request 弹窗 + 120s 超时视为拒绝」管线 —— 不新增
|
|
1135
|
-
// 协议消息,将安装清单放在 reason 里展示;这里直接调 requester(不写授权表,
|
|
1136
|
-
// remember 标志被忽略)。拒绝 / 超时 / 未接弹窗设施(无头 DSH)一律 fail-closed。
|
|
1135
|
+
// 的 install/update 在真正动安装器之前必须拿到用户确认。
|
|
1137
1136
|
// ---------------------------------------------------------------------------
|
|
1138
|
-
async function
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
const ans = await ask("plugin-installer", {
|
|
1144
|
-
family: "net",
|
|
1145
|
-
reason: `安装确认:将安装/更新以下插件(id ← source):\n${list}\n拒绝或 120 秒未确认则不安装。`,
|
|
1137
|
+
async function confirmPluginInstallHelper(items) {
|
|
1138
|
+
return confirmPluginInstall(items, {
|
|
1139
|
+
permGrants: pluginMgr.permGrants,
|
|
1140
|
+
permissionRequester: pluginMgr.permissionRequester,
|
|
1141
|
+
onGrantsChanged: pushPluginPermissions,
|
|
1146
1142
|
});
|
|
1147
|
-
return ans.ok === true;
|
|
1148
1143
|
}
|
|
1149
1144
|
// 内置定时任务(issue #184):全局 <dataDir>/scheduler-tasks.json,TTL 与
|
|
1150
1145
|
// client-state 同级;Agent 工具建的任务优先唤醒发起对话(issue #193:
|
|
@@ -1641,9 +1636,20 @@ wss.on("connection", (ws) => {
|
|
|
1641
1636
|
return;
|
|
1642
1637
|
}
|
|
1643
1638
|
switch (msg.type) {
|
|
1644
|
-
case "prompt":
|
|
1639
|
+
case "prompt": {
|
|
1640
|
+
const hasAttach = Boolean(msg.attachments && msg.attachments.length > 0);
|
|
1641
|
+
if (!msg.text?.trim() && !hasAttach) {
|
|
1642
|
+
send({
|
|
1643
|
+
type: "notice",
|
|
1644
|
+
level: "warning",
|
|
1645
|
+
text: "发送已忽略:提示词为空且未附带文件或上下文引用。",
|
|
1646
|
+
textEn: "Prompt ignored: text is empty and no attachments were provided.",
|
|
1647
|
+
});
|
|
1648
|
+
break;
|
|
1649
|
+
}
|
|
1645
1650
|
void cs.prompt(msg.text, msg.attachments, msg.queue);
|
|
1646
1651
|
break;
|
|
1652
|
+
}
|
|
1647
1653
|
case "queue_remove":
|
|
1648
1654
|
cs.removeQueued(msg.kind, msg.text, msg.index);
|
|
1649
1655
|
break;
|
|
@@ -1705,6 +1711,9 @@ wss.on("connection", (ws) => {
|
|
|
1705
1711
|
send({ type: "tool_info", name: msg.name, found: false, unsupported: true });
|
|
1706
1712
|
}
|
|
1707
1713
|
break;
|
|
1714
|
+
case "get_compacted_messages":
|
|
1715
|
+
void cs.getCompactedMessages?.(msg.compactionMessageId, msg.conversationId);
|
|
1716
|
+
break;
|
|
1708
1717
|
case "list_sessions":
|
|
1709
1718
|
void cs.refreshSessions();
|
|
1710
1719
|
break;
|
|
@@ -1731,6 +1740,11 @@ wss.on("connection", (ws) => {
|
|
|
1731
1740
|
void cs.persistConversation(msg.id);
|
|
1732
1741
|
}
|
|
1733
1742
|
break;
|
|
1743
|
+
case "pin_conversation":
|
|
1744
|
+
if (typeof cs.setConversationPinned === "function") {
|
|
1745
|
+
void cs.setConversationPinned(msg.id, msg.pinned);
|
|
1746
|
+
}
|
|
1747
|
+
break;
|
|
1734
1748
|
case "dismiss_finished_subagents":
|
|
1735
1749
|
void cs.dismissFinishedSubagents(msg.parentId);
|
|
1736
1750
|
break;
|
|
@@ -2027,6 +2041,7 @@ wss.on("connection", (ws) => {
|
|
|
2027
2041
|
reviewModel: msg.reviewModel,
|
|
2028
2042
|
maxRounds: msg.maxRounds,
|
|
2029
2043
|
locked: msg.locked,
|
|
2044
|
+
execModel: msg.execModel,
|
|
2030
2045
|
});
|
|
2031
2046
|
break;
|
|
2032
2047
|
case "clear_goal":
|
|
@@ -2044,6 +2059,7 @@ wss.on("connection", (ws) => {
|
|
|
2044
2059
|
reviewModel: msg.reviewModel,
|
|
2045
2060
|
maxRounds: msg.maxRounds,
|
|
2046
2061
|
locked: msg.locked,
|
|
2062
|
+
execModel: msg.execModel,
|
|
2047
2063
|
});
|
|
2048
2064
|
break;
|
|
2049
2065
|
case "get_settings":
|
|
@@ -2174,15 +2190,35 @@ wss.on("connection", (ws) => {
|
|
|
2174
2190
|
// plugin_job;拒绝/超时直接回一条 done,让面板上的作业就地结束(不占
|
|
2175
2191
|
// 安装锁、不弹「失败」之外的噪音)。卸载不在本门范围内(由面板本身发起)。
|
|
2176
2192
|
if (jobAction === "install" || jobAction === "update") {
|
|
2177
|
-
|
|
2193
|
+
const alreadyGranted = pluginMgr.permGrants.has("plugin-installer", "net", { host: "github.com" });
|
|
2194
|
+
if (!alreadyGranted) {
|
|
2195
|
+
send({
|
|
2196
|
+
type: "plugin_job",
|
|
2197
|
+
jobId,
|
|
2198
|
+
action: jobAction,
|
|
2199
|
+
pluginId,
|
|
2200
|
+
phase: "start",
|
|
2201
|
+
});
|
|
2202
|
+
send({
|
|
2203
|
+
type: "plugin_job",
|
|
2204
|
+
jobId,
|
|
2205
|
+
action: jobAction,
|
|
2206
|
+
pluginId,
|
|
2207
|
+
phase: "log",
|
|
2208
|
+
line: jobLang() === "zh" ? "等待确认安装授权…" : "Waiting for install confirmation…",
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
void confirmPluginInstallHelper([{ id: pluginId, source: String(msg.source ?? "") }])
|
|
2178
2212
|
.then((confirmed) => {
|
|
2179
2213
|
if (!confirmed) {
|
|
2180
|
-
jobDone(false,
|
|
2214
|
+
jobDone(false, jobLang() === "zh"
|
|
2215
|
+
? "用户未确认安装(拒绝或 120 秒超时)"
|
|
2216
|
+
: "Installation not confirmed (rejected or timed out after 120s)");
|
|
2181
2217
|
return;
|
|
2182
2218
|
}
|
|
2183
2219
|
startPluginJob();
|
|
2184
2220
|
})
|
|
2185
|
-
.catch(() => jobDone(false, "安装确认流程异常"));
|
|
2221
|
+
.catch(() => jobDone(false, jobLang() === "zh" ? "安装确认流程异常" : "Installation confirmation error"));
|
|
2186
2222
|
break;
|
|
2187
2223
|
}
|
|
2188
2224
|
startPluginJob();
|
|
@@ -2195,6 +2231,19 @@ wss.on("connection", (ws) => {
|
|
|
2195
2231
|
done: async (ok, info) => {
|
|
2196
2232
|
if (ok) {
|
|
2197
2233
|
await reloadPluginsAndPush(jobLang);
|
|
2234
|
+
const isZh = jobLang() === "zh";
|
|
2235
|
+
const actionLabel = jobAction === "uninstall"
|
|
2236
|
+
? isZh
|
|
2237
|
+
? "卸载"
|
|
2238
|
+
: "uninstalled"
|
|
2239
|
+
: jobAction === "update"
|
|
2240
|
+
? isZh
|
|
2241
|
+
? "更新"
|
|
2242
|
+
: "updated"
|
|
2243
|
+
: isZh
|
|
2244
|
+
? "安装"
|
|
2245
|
+
: "installed";
|
|
2246
|
+
cs?.emitNotice("info", `插件「${pluginId}」${actionLabel}完成`, `Plugin "${pluginId}" ${actionLabel} successfully`);
|
|
2198
2247
|
}
|
|
2199
2248
|
else if (info.error) {
|
|
2200
2249
|
cs?.emitNotice("error", `插件操作失败:${info.error}`, `Plugin operation failed: ${info.error}`);
|
|
@@ -2354,7 +2403,7 @@ wss.on("connection", (ws) => {
|
|
|
2354
2403
|
// 本地文件来源只允许工作区内(防任意路径文件探测 oracle)。
|
|
2355
2404
|
workspaceRoot: cs?.cwd ?? CWD,
|
|
2356
2405
|
// 安装确认门(P0):拒绝/超时只写目录不安装。
|
|
2357
|
-
confirmInstall: (items) =>
|
|
2406
|
+
confirmInstall: (items) => confirmPluginInstallHelper(items),
|
|
2358
2407
|
}).then((r) => {
|
|
2359
2408
|
if (r.installRefused) {
|
|
2360
2409
|
cs?.emitNotice("warning", "目录已同步,但安装未获用户确认(拒绝或超时),未安装任何插件", "Catalog synced, but installation was not confirmed (denied or timed out) — nothing was installed");
|
|
@@ -2423,6 +2472,16 @@ wss.on("connection", (ws) => {
|
|
|
2423
2472
|
case "plan_update":
|
|
2424
2473
|
cs.updatePlan?.(msg.steps, msg.activeStepId, msg.conversationId);
|
|
2425
2474
|
break;
|
|
2475
|
+
case "set_plan_mode":
|
|
2476
|
+
// 计划模式(只规划不实施):会话级开关,热生效(提示词重建 + 工具硬闸门)。
|
|
2477
|
+
void cs.setPlanMode?.(msg.enabled, msg.conversationId);
|
|
2478
|
+
break;
|
|
2479
|
+
case "set_delegate_mode":
|
|
2480
|
+
// 审查者模式(自动委派):会话级开关,默认关。开启后本对话只审阅,
|
|
2481
|
+
// 用户 prompt 由服务端转给常驻落盘执行对话。DSH 引擎的实现在
|
|
2482
|
+
// setDelegateMode 里直接拒(无 customTools 注册面 → 闸门无处可挂)。
|
|
2483
|
+
void cs.setDelegateMode?.(msg.enabled, msg.conversationId);
|
|
2484
|
+
break;
|
|
2426
2485
|
case "page_response":
|
|
2427
2486
|
// 浏览器(page-picker 扩展经前端)对 browser_page 的回包:恢复挂起的
|
|
2428
2487
|
// pageCall;id 不匹配(超时后迟到/页面刷新)由 resolvePageCall 静默忽略。
|
package/dist/server/lsp-tool.js
CHANGED
|
@@ -795,6 +795,7 @@ export function makeLspTool(options) {
|
|
|
795
795
|
const cwd = options.cwd;
|
|
796
796
|
return defineTool({
|
|
797
797
|
name: LSP_TOOL_NAME,
|
|
798
|
+
promptSnippet: "IDE-grade semantic analysis (LSP) across the workspace: definition, references, hover, diagnostics, symbols, impact check",
|
|
798
799
|
label: "LSP code intelligence",
|
|
799
800
|
description: `IDE-grade semantic analysis (LSP) across the workspace. Actions:
|
|
800
801
|
- \`definition\`: definition of the symbol at \`line\`/\`character\` in \`path\` (file, line, snippet).
|
|
@@ -807,16 +808,18 @@ export function makeLspTool(options) {
|
|
|
807
808
|
- \`cascade\`: impact check for \`path\` — report diagnostics of files referencing it.
|
|
808
809
|
Lines are 1-indexed.`,
|
|
809
810
|
parameters: Type.Object({
|
|
810
|
-
action: Type.
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
811
|
+
action: Type.Unsafe({
|
|
812
|
+
type: "string",
|
|
813
|
+
enum: [
|
|
814
|
+
"definition",
|
|
815
|
+
"references",
|
|
816
|
+
"hover",
|
|
817
|
+
"diagnostics",
|
|
818
|
+
"documentSymbol",
|
|
819
|
+
"read_symbol",
|
|
820
|
+
"workspaceSymbol",
|
|
821
|
+
"cascade",
|
|
822
|
+
],
|
|
820
823
|
description: "The LSP operation to perform.",
|
|
821
824
|
}),
|
|
822
825
|
path: Type.Optional(Type.String({
|
|
@@ -40,14 +40,12 @@ export function describeTodos(state, includeDeleted = false, lang = "en") {
|
|
|
40
40
|
.join("\n");
|
|
41
41
|
}
|
|
42
42
|
const TODO_GUIDANCE_ZH = [
|
|
43
|
-
"# 内联标记工具(状态类操作请写在回答正文,不要调用工具)",
|
|
44
43
|
"- 标记语法:[[todo:new:<主题>]] 新建;[[todo:set:<id>,completed|in_progress|pending]] 状态;[[todo:remove:<id>]] 删除;[[todo:dep:<id>,blocks=<依赖id,逗号分隔>]] 设依赖。",
|
|
45
44
|
"- 状态变化全部用上面的 [[todo:...]] 内联标记表达,不会中断回答,无需等待返回。",
|
|
46
45
|
"- 想查看/list 当前任务列表时,才用 `todo_list` 工具(读操作走工具)。",
|
|
47
46
|
"- 不要编造不存在的任务 id;id 由 [[todo:new:...]] 分配,首次分配是自增整数。",
|
|
48
47
|
];
|
|
49
48
|
const TODO_GUIDANCE_EN = [
|
|
50
|
-
"# Inline marker tools (express state changes inline in your reply text — never call a tool for them)",
|
|
51
49
|
"- Marker syntax: [[todo:new:<subject>]] to create; [[todo:set:<id>,completed|in_progress|pending]] for status; [[todo:remove:<id>]] to delete; [[todo:dep:<id>,blocks=<dep ids, comma-separated>]] to set dependencies.",
|
|
52
50
|
"- Express all status changes with the [[todo:...]] inline markers above; they never interrupt your reply and need no waiting for a result.",
|
|
53
51
|
"- Only use the `todo_list` tool (the read path goes through the tool) when you want to list the current tasks.",
|
|
@@ -393,12 +393,25 @@ function adaptMcpTool(serverName, mcpTool, client) {
|
|
|
393
393
|
name,
|
|
394
394
|
label: `${serverName} · ${mcpTool.name}`,
|
|
395
395
|
description: mcpTool.description ?? `Tool ${mcpTool.name} provided by MCP server "${serverName}"`,
|
|
396
|
+
// 无 snippet 的工具不会进系统提示词的「Available tools」清单(SDK 只列有
|
|
397
|
+
// snippet 的工具),MCP 工具会变成 schema-only:模型读清单以为自己没有
|
|
398
|
+
// 这个能力(#403)。这里从 description 提取首句做极简 snippet。
|
|
399
|
+
promptSnippet: firstSentenceSnippet(mcpTool.description) ?? `MCP tool ${mcpTool.name} (server: ${serverName})`,
|
|
396
400
|
parameters: mcpTool.inputSchema ?? {},
|
|
397
401
|
execute: async (_toolCallId, params, _signal) => {
|
|
398
402
|
return client.call(mcpTool.name, params ?? {});
|
|
399
403
|
},
|
|
400
404
|
};
|
|
401
405
|
}
|
|
406
|
+
/** 从 MCP 工具 description 提取首句做 promptSnippet(≤100 字符);空描述返回 undefined。 */
|
|
407
|
+
function firstSentenceSnippet(description) {
|
|
408
|
+
const text = (description ?? "").replace(/\s+/g, " ").trim();
|
|
409
|
+
if (!text)
|
|
410
|
+
return undefined;
|
|
411
|
+
const cut = text.search(/[.。!?!?]/);
|
|
412
|
+
const head = cut > 0 ? text.slice(0, cut + 1) : text;
|
|
413
|
+
return head.length > 100 ? `${head.slice(0, 99)}…` : head;
|
|
414
|
+
}
|
|
402
415
|
/** 工具名必须是 [A-Za-z0-9_-]+(与插件工具同规则),MCP 可能含冒号/斜杠 — 归一化。 */
|
|
403
416
|
function sanitizeToolName(name) {
|
|
404
417
|
const cleaned = (name || "").replace(/[^A-Za-z0-9_-]/g, "_");
|
|
@@ -18,6 +18,7 @@ export function makePatchTool(options) {
|
|
|
18
18
|
const cwd = options.cwd;
|
|
19
19
|
return defineTool({
|
|
20
20
|
name: PATCH_TOOL_NAME,
|
|
21
|
+
promptSnippet: "apply content-hashed, line-anchored patches to workspace files (prevents stale edits, line drift, and indentation hallucination)",
|
|
21
22
|
label: "Apply Hashline patch",
|
|
22
23
|
description: `Apply content-hashed, line-anchored patches to workspace files — prevents stale edits, line drift, and indentation hallucination.
|
|
23
24
|
Each file section starts with \`[path#TAG]\` (or \`[path]\` if the hash is unknown yet). Operations:
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turn_end boundary error patch for @earendil-works/pi-coding-agent (issue #411).
|
|
3
|
+
*
|
|
4
|
+
* In SDK 0.87+, when an in-flight turn is aborted or torn down before its assistant
|
|
5
|
+
* message is persisted, `_dispatchTurnEndBoundary()` in `agent-session.js` calls
|
|
6
|
+
* `this._extensionRunner.emitError({ event: "turn_end", error: "turn_end could not resolve..." })`
|
|
7
|
+
* right before returning `false`, causing false-positive extension errors to flood
|
|
8
|
+
* both the main conversation and subagents.
|
|
9
|
+
*
|
|
10
|
+
* This module idempotently patches `agent-session.js` in the installed SDK to
|
|
11
|
+
* silently skip the boundary (`return false`) without emitting an error, matching
|
|
12
|
+
* the upstream issue expectation (earendil-works/pi#10149).
|
|
13
|
+
*
|
|
14
|
+
* Same pattern as patch-remote-catalog.ts and patch-node-pty.ts: best-effort, idempotent,
|
|
15
|
+
* and skips gracefully if the SDK code structure differs or is already fixed upstream.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { sdkCopies } from "./sdk-origin.js";
|
|
21
|
+
export const TARGET_PATTERN = /if\s*\(!messageEntryId\)\s*\{\s*this\._extensionRunner\.emitError\(\{\s*extensionPath:\s*"<boundary>",\s*event:\s*"turn_end",\s*error:\s*"turn_end could not resolve the persisted assistant entry ID",\s*\}\);\s*return false;\s*\}/;
|
|
22
|
+
export const PATCHED_SENTINEL = "// pi-web-ui patch (issue #411: silent turn_end on aborted turns)";
|
|
23
|
+
export const REPLACEMENT = `if (!messageEntryId) {
|
|
24
|
+
${PATCHED_SENTINEL}
|
|
25
|
+
return false;
|
|
26
|
+
}`;
|
|
27
|
+
/**
|
|
28
|
+
* 纯函数:给定 agent-session.js 内容,返回打补丁后的内容。
|
|
29
|
+
* - 命中目标代码段 → 替换并返回新内容;
|
|
30
|
+
* - 已打过补丁(含 PATCHED_SENTINEL)→ 原样返回;
|
|
31
|
+
* - 目标代码段未命中(上游已修复或版本不匹配)→ 原样返回,静默跳过。
|
|
32
|
+
*/
|
|
33
|
+
export function patchAgentSessionSource(src) {
|
|
34
|
+
if (src.includes(PATCHED_SENTINEL)) {
|
|
35
|
+
return { patched: false, code: src };
|
|
36
|
+
}
|
|
37
|
+
if (!TARGET_PATTERN.test(src)) {
|
|
38
|
+
return { patched: false, code: src };
|
|
39
|
+
}
|
|
40
|
+
return { patched: true, code: src.replace(TARGET_PATTERN, REPLACEMENT) };
|
|
41
|
+
}
|
|
42
|
+
export function candidateAgentSessionFiles() {
|
|
43
|
+
const files = new Set();
|
|
44
|
+
try {
|
|
45
|
+
const entry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
|
|
46
|
+
files.add(join(dirname(entry), "core", "agent-session.js"));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Ignore resolve failures
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
for (const copy of sdkCopies()) {
|
|
53
|
+
files.add(join(dirname(copy.path), "dist", "core", "agent-session.js"));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Ignore copy inspection failures
|
|
58
|
+
}
|
|
59
|
+
return [...files];
|
|
60
|
+
}
|
|
61
|
+
export function applyPatch() {
|
|
62
|
+
for (const file of candidateAgentSessionFiles()) {
|
|
63
|
+
try {
|
|
64
|
+
if (!existsSync(file))
|
|
65
|
+
continue;
|
|
66
|
+
const src = readFileSync(file, "utf8");
|
|
67
|
+
const { patched, code } = patchAgentSessionSource(src);
|
|
68
|
+
if (patched) {
|
|
69
|
+
writeFileSync(file, code, "utf8");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// best-effort
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
applyPatch();
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 权限预设相关辅助逻辑与会话回放。
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* 从会话 sessionManager entries 回放恢复最后设定的**计划模式**状态(同 permission/preset
|
|
6
|
+
* 的落盘口径:切会话/重载不丢)。从未设定或非布尔 → false。
|
|
7
|
+
*/
|
|
8
|
+
export function readPlanModeFromSession(sm) {
|
|
9
|
+
try {
|
|
10
|
+
const mgr = sm;
|
|
11
|
+
if (typeof mgr?.getEntries !== "function")
|
|
12
|
+
return false;
|
|
13
|
+
const entries = mgr.getEntries();
|
|
14
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
15
|
+
const e = entries[i];
|
|
16
|
+
if (e?.customType === "plan/mode" && typeof e.data?.enabled === "boolean")
|
|
17
|
+
return e.data.enabled;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 从会话 sessionManager entries 回放恢复最后设定的**审查者模式**状态(同 plan/mode
|
|
27
|
+
* 口径:customType "delegate/mode")。从未设定或非布尔 → false。
|
|
28
|
+
* ⚠️ 执行对话 id **不回放**:常驻执行对话是本进程内的活对象,重启后
|
|
29
|
+
* 首条请求会重新建一个(宁可多一个执行对话,也别指向一个不存在的 id)。
|
|
30
|
+
*/
|
|
31
|
+
export function readDelegateModeFromSession(sm) {
|
|
32
|
+
try {
|
|
33
|
+
const mgr = sm;
|
|
34
|
+
if (typeof mgr?.getEntries !== "function")
|
|
35
|
+
return false;
|
|
36
|
+
const entries = mgr.getEntries();
|
|
37
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
38
|
+
const e = entries[i];
|
|
39
|
+
if (e?.customType === "delegate/mode" && typeof e.data?.enabled === "boolean")
|
|
40
|
+
return e.data.enabled;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
4
48
|
/**
|
|
5
49
|
* 从会话 sessionManager entries 回放恢复最后设定的权限预设(若从未设定则返回 undefined)。
|
|
6
50
|
* customType 为 "permission/preset",载荷结构为 { preset: string }。
|