pi-web-ui 0.94.0 → 0.95.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 +62 -0
- package/README.md +5 -6
- package/README.zh-CN.md +4 -5
- package/bin/pi-web-ui.mjs +751 -206
- package/dist/server/agent-service.js +1869 -85
- package/dist/server/approval-rules.js +550 -0
- package/dist/server/attachment-store.js +112 -0
- package/dist/server/attachments.js +62 -171
- package/dist/server/client-state.js +4 -0
- package/dist/server/compact-context-tool.js +125 -0
- package/dist/server/context-budget.js +317 -0
- package/dist/server/dangling-tools.js +162 -0
- package/dist/server/dsh/dsh-agent-service.js +50 -60
- package/dist/server/edit-soft-tool.js +70 -5
- package/dist/server/eval-tool.js +535 -0
- package/dist/server/files-service.js +35 -7
- package/dist/server/goal-service.js +240 -68
- package/dist/server/hashline-engine.js +665 -0
- package/dist/server/host-metrics.js +26 -2
- package/dist/server/http-proxy.js +11 -1
- package/dist/server/index.js +157 -9
- package/dist/server/lsp-tool.js +679 -0
- package/dist/server/model-admin.js +20 -0
- package/dist/server/office-parse.js +309 -0
- package/dist/server/patch-tool.js +91 -0
- package/dist/server/plan-manager.js +104 -0
- package/dist/server/plugin-api-catalog.js +296 -0
- package/dist/server/plugin-install-spec.js +196 -0
- package/dist/server/plugin-installer.js +73 -0
- package/dist/server/plugin-manifest-validate.js +305 -0
- package/dist/server/plugin-project.js +47 -11
- package/dist/server/plugin-tool-guard.js +120 -0
- package/dist/server/plugins.js +745 -161
- package/dist/server/process-utils.js +12 -6
- package/dist/server/scheduler-tasks.js +6 -0
- package/dist/server/serialize.js +13 -3
- package/dist/server/settings-service.js +108 -1
- package/dist/server/subagents.js +41 -0
- package/dist/server/terminals.js +69 -14
- package/dist/server/tool-approval.js +84 -0
- package/dist/server/tool-manager.js +214 -6
- package/dist/server/workspace-snapshot.js +113 -0
- package/package.json +3 -2
- package/plugin-sdk/README.md +25 -0
- package/plugin-sdk/index.d.ts +31 -38
- package/plugin-sdk/index.mjs +35 -19
- package/plugins/catalog.json +20 -0
- package/themes/ayu-light.css +6 -6
- package/themes/catppuccin-latte.css +6 -6
- package/themes/claude-code-dark.css +144 -0
- package/themes/codex.css +6 -6
- package/themes/dark-teal.css +20 -6
- package/themes/everforest-light.css +6 -6
- package/themes/geist.css +6 -6
- package/themes/gruvbox-light.css +6 -6
- package/themes/kanagawa-lotus.css +6 -6
- package/themes/rose-pine-dawn.css +6 -6
- package/themes/solarized-light.css +6 -6
- package/themes/vs-code-dark.css +146 -0
- package/web/dist/assets/{TerminalPanel-B-CMQuzm.js → TerminalPanel-DlGBF2MD.js} +1 -1
- package/web/dist/assets/index-BXnCQL9p.js +364 -0
- package/web/dist/assets/index-RpIGFyi_.css +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-18jK6al2.js +0 -364
- package/web/dist/assets/index-B3S9MxnN.css +0 -1
|
@@ -34,7 +34,21 @@ export function calculateHostMetrics(previous, current) {
|
|
|
34
34
|
memoryPercent,
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
/** CPU 平滑窗口:上报最近 N 次有效读数的均值。心跳 2s 一次,默认 5 次 ≈ 10s 平均
|
|
38
|
+
* —— 单次 2s 采样的毛刺(编译/构建抖动)不再直接上底栏,观感与任务管理器的
|
|
39
|
+
* 曲线对齐。内存变化慢,不平滑,沿用瞬时值。 */
|
|
40
|
+
export const CPU_SMOOTH_WINDOW = 5;
|
|
41
|
+
/** 有效 CPU 读数序列的均值(空序列 → null,前端显示 —)。纯函数,可单测。 */
|
|
42
|
+
export function meanCpuPercent(readings) {
|
|
43
|
+
if (readings.length === 0)
|
|
44
|
+
return null;
|
|
45
|
+
let sum = 0;
|
|
46
|
+
for (const v of readings)
|
|
47
|
+
sum += v;
|
|
48
|
+
return sum / readings.length;
|
|
49
|
+
}
|
|
50
|
+
export function createHostMetricsSampler(readSnapshot = defaultReadSnapshot, windowSize = CPU_SMOOTH_WINDOW) {
|
|
51
|
+
const window = Math.max(1, Math.floor(windowSize));
|
|
38
52
|
let previous = null;
|
|
39
53
|
try {
|
|
40
54
|
previous = readSnapshot();
|
|
@@ -43,6 +57,8 @@ export function createHostMetricsSampler(readSnapshot = defaultReadSnapshot) {
|
|
|
43
57
|
// 初始基线读取异常时不击穿服务启动,待后续心跳重试
|
|
44
58
|
previous = null;
|
|
45
59
|
}
|
|
60
|
+
/** 最近 window 次有效 CPU 读数(totalDelta<=0 的 null 读数不进序列,避免空洞拉低均值)。 */
|
|
61
|
+
const history = [];
|
|
46
62
|
return () => {
|
|
47
63
|
const current = readSnapshot();
|
|
48
64
|
if (!previous) {
|
|
@@ -54,6 +70,14 @@ export function createHostMetricsSampler(readSnapshot = defaultReadSnapshot) {
|
|
|
54
70
|
}
|
|
55
71
|
const metrics = calculateHostMetrics(previous, current);
|
|
56
72
|
previous = current;
|
|
57
|
-
|
|
73
|
+
if (metrics.cpuPercent !== null && Number.isFinite(metrics.cpuPercent)) {
|
|
74
|
+
history.push(metrics.cpuPercent);
|
|
75
|
+
if (history.length > window)
|
|
76
|
+
history.splice(0, history.length - window);
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
cpuPercent: meanCpuPercent(history),
|
|
80
|
+
memoryPercent: metrics.memoryPercent,
|
|
81
|
+
};
|
|
58
82
|
};
|
|
59
83
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { existsSync, readFileSync } from "node:fs";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
|
-
import
|
|
12
|
+
import { createRequire } from "node:module";
|
|
13
13
|
/** Read proxy settings from settings.json and environment. */
|
|
14
14
|
export function resolveProxySettings(agentDir) {
|
|
15
15
|
let httpProxy;
|
|
@@ -49,6 +49,16 @@ export function initHttpProxy(agentDir) {
|
|
|
49
49
|
return { active: false };
|
|
50
50
|
}
|
|
51
51
|
try {
|
|
52
|
+
// Lazy-load undici so a missing optional dep never crashes server startup;
|
|
53
|
+
// proxy simply stays inactive with a warning.
|
|
54
|
+
let undici;
|
|
55
|
+
try {
|
|
56
|
+
undici = createRequire(import.meta.url)("undici");
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
console.warn("[proxy] undici not installed, HTTP proxy support disabled:", err);
|
|
60
|
+
return { active: false, proxyUrl: effectiveProxy };
|
|
61
|
+
}
|
|
52
62
|
const dispatcher = withUndiciErrorListener(new undici.EnvHttpProxyAgent({
|
|
53
63
|
allowH2: false,
|
|
54
64
|
proxyTunnel: true,
|
package/dist/server/index.js
CHANGED
|
@@ -33,6 +33,7 @@ import { PROTOCOL_VERSION } from "./protocol-version.js";
|
|
|
33
33
|
import { AgentService, workspacePath, QuiesceRejectedError } from "./agent-service.js";
|
|
34
34
|
import { WS_MAX_PAYLOAD_BYTES, isAbsoluteWirePath, wireToAbs } from "./files-service.js";
|
|
35
35
|
import { registerFileTransferRoutes } from "./file-transfer-routes.js";
|
|
36
|
+
import { initAttachmentStore, readAttachment } from "./attachment-store.js";
|
|
36
37
|
import { isAudioFile, previewKind } from "./text-sniff.js";
|
|
37
38
|
import { startControlServer } from "./control-socket.js";
|
|
38
39
|
import { scheduleUploadCleanup } from "./uploads.js";
|
|
@@ -44,13 +45,14 @@ import { parseTabs, tabsRefusal } from "./tabs.js";
|
|
|
44
45
|
import { recordUnknownWsType } from "./ws-unknown-types.js";
|
|
45
46
|
import { installPack, isKnownPack, listPacks, loadServerStrings, readPackFile, removePack, unloadServerStrings, } from "./locales.js";
|
|
46
47
|
import { PluginManager, resolvePluginClientFile, } from "./plugins.js";
|
|
47
|
-
import { PluginInstaller } from "./plugin-installer.js";
|
|
48
|
+
import { inspectInstallSpec, PluginInstaller } from "./plugin-installer.js";
|
|
48
49
|
import { syncPluginCatalog } from "./plugin-catalog-sync.js";
|
|
49
50
|
import { McpBridge } from "./mcp-bridge.js";
|
|
50
51
|
import { createMcpHotReload } from "./mcp-hot-reload.js";
|
|
51
52
|
import { createHostMetricsSampler } from "./host-metrics.js";
|
|
52
53
|
import { SchedulerStore } from "./scheduler-tasks.js";
|
|
53
54
|
import { initHttpProxy } from "./http-proxy.js";
|
|
55
|
+
import { globalLspPool } from "./lsp-tool.js";
|
|
54
56
|
import { buildPiWebTokenCookie, decodeCookieToken, isTlsRequest } from "./auth-cookie.js";
|
|
55
57
|
/** Strip npm-injected env (`npm start` exports `npm_config_*` / `npm_package_*` /
|
|
56
58
|
* `npm_lifecycle_*` into every child). Anything this server spawns — shells,
|
|
@@ -95,6 +97,19 @@ try {
|
|
|
95
97
|
catch (err) {
|
|
96
98
|
console.warn(`[data] 无法创建数据目录 ${DATA_DIR}: ${err.message}`);
|
|
97
99
|
}
|
|
100
|
+
// issue #295:工作区直接就是家目录时,SDK 初始化期的同步目录扫描会落在 $HOME 上
|
|
101
|
+
// (iCloud 占位符/外部卷坏挂载 → scandir/open 内核挂起 → 事件循环假死,hello 后无
|
|
102
|
+
// ready)。新装服务默认已是 ~/pi-web-ui(见 bin/pi-web-ui.mjs serviceOptions);
|
|
103
|
+
// 老服务若仍指着家目录,在此提示一次,`server install` 重装即迁移。
|
|
104
|
+
try {
|
|
105
|
+
if (resolve(CWD) === resolve(homedir())) {
|
|
106
|
+
console.warn(`[ws] 工作区为用户主目录 ${CWD}:目录扫描可能因外部卷/同步盘挂载而长时间阻塞,` +
|
|
107
|
+
`建议用 \`pi-web-ui server install --cwd <项目目录>\` 重装迁移。`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* 路径比较失败不影响启动 */
|
|
112
|
+
}
|
|
98
113
|
// Dev-no-cache setting read without a ClientStateStore instance (the index.html
|
|
99
114
|
// route runs before any client attaches). Reads the same global settings blob.
|
|
100
115
|
function readDevNoCacheSetting() {
|
|
@@ -316,6 +331,27 @@ app.get("/api/health", (_req, res) => {
|
|
|
316
331
|
engine: ENGINE,
|
|
317
332
|
});
|
|
318
333
|
});
|
|
334
|
+
/**
|
|
335
|
+
* 基于 SHA-256 内容寻址的附件静态服务:
|
|
336
|
+
* 永久强缓存(immutable),支持图片和文件读取。
|
|
337
|
+
*/
|
|
338
|
+
app.get("/api/attachment/:hash", async (req, res) => {
|
|
339
|
+
try {
|
|
340
|
+
const hash = String(req.params.hash ?? "").trim();
|
|
341
|
+
const hit = await readAttachment(hash);
|
|
342
|
+
if (!hit) {
|
|
343
|
+
res.status(404).end("attachment not found");
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
res.setHeader("Content-Type", hit.mimeType);
|
|
347
|
+
res.setHeader("Content-Length", hit.buffer.length);
|
|
348
|
+
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
|
|
349
|
+
res.end(hit.buffer);
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
res.status(500).end(err.message);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
319
355
|
/**
|
|
320
356
|
* Stream a workspace file over HTTP.
|
|
321
357
|
*
|
|
@@ -918,6 +954,7 @@ const service = ENGINE === "dsh"
|
|
|
918
954
|
join(DATA_DIR, "client-state.json"));
|
|
919
955
|
// Server-string tables (issue #91 v2): packs' `serverStrings` sections feed
|
|
920
956
|
// pick() lookup for non-zh/en UI languages (missing key → English fallback).
|
|
957
|
+
initAttachmentStore(DATA_DIR);
|
|
921
958
|
loadServerStrings(DATA_DIR);
|
|
922
959
|
// Optional UI plugins (<dataDir>/plugins/<id>/): scanned on every client
|
|
923
960
|
// attach so freshly dropped plugins appear without a server restart.
|
|
@@ -1139,6 +1176,17 @@ const scheduler = new SchedulerStore(DATA_DIR, {
|
|
|
1139
1176
|
},
|
|
1140
1177
|
onChange: () => pushSchedulerTasks(),
|
|
1141
1178
|
notify: (level, text, textEn) => pushNoticeToAll(level, text, textEn ?? text),
|
|
1179
|
+
// issue #291:任务删除(含单次任务跑完自删)后回收其伪客户端,
|
|
1180
|
+
// 否则 scheduler:<taskId> 会话会永久占据其他客户端的 elsewhere 列表。
|
|
1181
|
+
onTaskRemoved: (taskId) => {
|
|
1182
|
+
const svc = service;
|
|
1183
|
+
try {
|
|
1184
|
+
svc.releaseSchedulerClient?.(taskId);
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
// 回收失败不影响任务删除
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1142
1190
|
});
|
|
1143
1191
|
scheduler.start();
|
|
1144
1192
|
/** 把调度器任务列表推给所有在线客户端(设置面板展示 + 变更后刷新)。 */
|
|
@@ -1194,6 +1242,12 @@ void mcpBridge.load().then(() => {
|
|
|
1194
1242
|
});
|
|
1195
1243
|
// 插件扩展点:SDK 工具执行事件(bash/读文件等 start+end)转发给已注册的插件。
|
|
1196
1244
|
service.onToolEvent = (ev) => pluginMgr.emitToolEvent(ev);
|
|
1245
|
+
// 插件扩展点(P1-5):bash/read 执行前后的拦截(pre 拒/问即拦、post 脱敏补上下文;
|
|
1246
|
+
// 只覆盖已接管的这两处,DSH 引擎无 customTool 注册面不接)。
|
|
1247
|
+
service.toolGuard = {
|
|
1248
|
+
pre: (req, lang) => pluginMgr.evaluateToolPre(req, lang),
|
|
1249
|
+
post: (req, lang) => pluginMgr.evaluateToolPost(req, lang),
|
|
1250
|
+
};
|
|
1197
1251
|
service.onRunEvent = (ev) => pluginMgr.emitRunEvent(ev);
|
|
1198
1252
|
// 插件扩展点:对话切换通知(轨迹视图切会话后即重拉;dsh 引擎暂无)。
|
|
1199
1253
|
service.onConversationChanged = () => pluginMgr.emitConversationChanged();
|
|
@@ -1384,6 +1438,10 @@ const SNAPSHOT_BACKPRESSURE_FACTOR = 3;
|
|
|
1384
1438
|
const SNAPSHOT_BACKPRESSURE_MIN_BYTES = 262_144;
|
|
1385
1439
|
/** 背压丢弃后的延迟重发间隔。 */
|
|
1386
1440
|
const SNAPSHOT_RETRY_MS = 250;
|
|
1441
|
+
/** issue #295:attach(会话初始化)超过此时长未完成,先给浏览器一句可见提示,
|
|
1442
|
+
* 避免界面永久停在「正在连接」而用户不知发生了什么。attach 本体继续等(不取消),
|
|
1443
|
+
* 完成后照常走快照流程。 */
|
|
1444
|
+
const ATTACH_SLOW_NOTICE_MS = 8_000;
|
|
1387
1445
|
/**
|
|
1388
1446
|
* Multi-tab serialization sharing: emit() hands the SAME message object to
|
|
1389
1447
|
* every socket of a client, but each send() used to JSON.stringify it
|
|
@@ -1509,11 +1567,17 @@ wss.on("connection", (ws) => {
|
|
|
1509
1567
|
void cs.listBgServers();
|
|
1510
1568
|
break;
|
|
1511
1569
|
case "new_chat":
|
|
1512
|
-
void cs.newChat(msg.preset);
|
|
1570
|
+
void cs.newChat(msg.preset, msg.ephemeral);
|
|
1513
1571
|
break;
|
|
1514
1572
|
case "edit_message":
|
|
1515
1573
|
void cs.editMessage(msg.messageId, msg.text, msg.attachments);
|
|
1516
1574
|
break;
|
|
1575
|
+
case "fork_session":
|
|
1576
|
+
void cs.forkSession?.(msg.messageId, msg.position, msg.conversationId);
|
|
1577
|
+
break;
|
|
1578
|
+
case "rollback_session":
|
|
1579
|
+
void cs.rollbackSession?.(msg.messageId, msg.conversationId, msg.restoreWorkspace);
|
|
1580
|
+
break;
|
|
1517
1581
|
case "cycle_model":
|
|
1518
1582
|
void cs.cycleModel();
|
|
1519
1583
|
break;
|
|
@@ -1568,6 +1632,9 @@ wss.on("connection", (ws) => {
|
|
|
1568
1632
|
case "dismiss_finished_subagents":
|
|
1569
1633
|
void cs.dismissFinishedSubagents(msg.parentId);
|
|
1570
1634
|
break;
|
|
1635
|
+
case "subagent_handoff":
|
|
1636
|
+
void cs.handoffSubagent?.(msg.fromRunId, msg.toRunId, msg.payload);
|
|
1637
|
+
break;
|
|
1571
1638
|
case "switch_session":
|
|
1572
1639
|
void cs.switchSession(msg.path);
|
|
1573
1640
|
break;
|
|
@@ -1782,6 +1849,9 @@ wss.on("connection", (ws) => {
|
|
|
1782
1849
|
case "fetch_models":
|
|
1783
1850
|
void cs.fetchModelsList(msg.reqId, msg.baseUrl, msg.apiKey, msg.authHeader, msg.api);
|
|
1784
1851
|
break;
|
|
1852
|
+
case "test_model_connection":
|
|
1853
|
+
void cs.testModelConnection?.(msg.reqId, msg.baseUrl, msg.apiKey, msg.authHeader, msg.api);
|
|
1854
|
+
break;
|
|
1785
1855
|
case "refresh_provider_models":
|
|
1786
1856
|
void cs.refreshProviderModels(msg.providerId, msg.reqId);
|
|
1787
1857
|
break;
|
|
@@ -1887,8 +1957,10 @@ wss.on("connection", (ws) => {
|
|
|
1887
1957
|
terminalToolsEnabled: msg.terminalToolsEnabled,
|
|
1888
1958
|
terminalBash: msg.terminalBash,
|
|
1889
1959
|
terminalBashIdleMs: msg.terminalBashIdleMs,
|
|
1960
|
+
terminalBashMaxForegroundMs: msg.terminalBashMaxForegroundMs,
|
|
1890
1961
|
toolWatchdogTimeoutMs: msg.toolWatchdogTimeoutMs,
|
|
1891
1962
|
readDirEnabled: msg.readDirEnabled,
|
|
1963
|
+
toolApprovalEnabled: msg.toolApprovalEnabled,
|
|
1892
1964
|
editSoftEnabled: msg.editSoftEnabled,
|
|
1893
1965
|
questionnaireEnabled: msg.questionnaireEnabled,
|
|
1894
1966
|
parallelReminderEnabled: msg.parallelReminderEnabled,
|
|
@@ -2000,6 +2072,36 @@ wss.on("connection", (ws) => {
|
|
|
2000
2072
|
case "plugin_job_cancel":
|
|
2001
2073
|
pluginInstaller.cancel(String(msg.jobId ?? ""));
|
|
2002
2074
|
break;
|
|
2075
|
+
// 注册面目录(DSH P2-7):只读装配(slot/工具/宿主方法表+当前占用者),按需拉取。
|
|
2076
|
+
case "plugin_api_catalog": {
|
|
2077
|
+
const requestId = String(msg.requestId ?? "");
|
|
2078
|
+
send({ type: "plugin_api_catalog_result", requestId, catalog: pluginMgr.getApiCatalog() });
|
|
2079
|
+
break;
|
|
2080
|
+
}
|
|
2081
|
+
// 安装前先读 spec(DSH P0-3):不联网也能查(形状/已装),GitHub 源再探一次
|
|
2082
|
+
// raw manifest。结果只作引导(UI 把 problem 渲染成输入框下的一句话),不阻塞安装。
|
|
2083
|
+
case "plugin_install_inspect": {
|
|
2084
|
+
const requestId = String(msg.requestId ?? "");
|
|
2085
|
+
const source = String(msg.source ?? "");
|
|
2086
|
+
void inspectInstallSpec(source, {
|
|
2087
|
+
pluginsDir: pluginMgr.pluginsDir,
|
|
2088
|
+
...(typeof msg.explicitId === "string" && msg.explicitId.trim() ? { explicitId: msg.explicitId.trim() } : {}),
|
|
2089
|
+
force: msg.force === true,
|
|
2090
|
+
}).then((r) => {
|
|
2091
|
+
send({
|
|
2092
|
+
type: "plugin_install_inspect_result",
|
|
2093
|
+
requestId,
|
|
2094
|
+
source,
|
|
2095
|
+
kind: r.spec.kind,
|
|
2096
|
+
suggestedId: r.suggestedId,
|
|
2097
|
+
installed: r.installed,
|
|
2098
|
+
...(r.problem ? { problem: r.problem } : {}),
|
|
2099
|
+
...(r.detail ? { detail: r.detail } : {}),
|
|
2100
|
+
...(r.manifest ? { manifest: r.manifest } : {}),
|
|
2101
|
+
});
|
|
2102
|
+
});
|
|
2103
|
+
break;
|
|
2104
|
+
}
|
|
2003
2105
|
// -- 插件目录授权(issue #146)------------------------------------------
|
|
2004
2106
|
case "plugin_path_response": {
|
|
2005
2107
|
const pending = pendingPathRequests.get(String(msg.id ?? ""));
|
|
@@ -2075,7 +2177,8 @@ wss.on("connection", (ws) => {
|
|
|
2075
2177
|
customCatalogPath: pluginMgr.customCatalogPath,
|
|
2076
2178
|
pluginsDir: join(DATA_DIR, "plugins"),
|
|
2077
2179
|
installer: pluginInstaller,
|
|
2078
|
-
|
|
2180
|
+
// 只更新市场列表时无需重启已激活插件,避免重复广播工作目录。
|
|
2181
|
+
afterWrite: () => (msg.install === true ? reloadPluginsAndPush(syncLang) : pluginMgr.pushCatalog()),
|
|
2079
2182
|
lang: syncLang,
|
|
2080
2183
|
}).then((r) => {
|
|
2081
2184
|
send({
|
|
@@ -2129,6 +2232,19 @@ wss.on("connection", (ws) => {
|
|
|
2129
2232
|
void cs.answerQuestion?.(msg.id, msg.answers, msg.cancelled);
|
|
2130
2233
|
}
|
|
2131
2234
|
break;
|
|
2235
|
+
case "tool_approval_response":
|
|
2236
|
+
cs.resolveToolApproval?.(msg.id, msg.decision, msg.editedParams, msg.reason, msg.scope);
|
|
2237
|
+
break;
|
|
2238
|
+
case "set_approval_policy":
|
|
2239
|
+
cs.setApprovalPolicy?.({
|
|
2240
|
+
conversationId: msg.conversationId,
|
|
2241
|
+
allowAll: msg.allowAll,
|
|
2242
|
+
categories: msg.categories,
|
|
2243
|
+
});
|
|
2244
|
+
break;
|
|
2245
|
+
case "plan_update":
|
|
2246
|
+
cs.updatePlan?.(msg.steps, msg.activeStepId, msg.conversationId);
|
|
2247
|
+
break;
|
|
2132
2248
|
case "page_response":
|
|
2133
2249
|
// 浏览器(page-picker 扩展经前端)对 browser_page 的回包:恢复挂起的
|
|
2134
2250
|
// pageCall;id 不匹配(超时后迟到/页面刷新)由 resolvePageCall 静默忽略。
|
|
@@ -2143,6 +2259,18 @@ wss.on("connection", (ws) => {
|
|
|
2143
2259
|
case "delete_subagent_template":
|
|
2144
2260
|
void cs.deleteSubagentTemplate(msg.name);
|
|
2145
2261
|
break;
|
|
2262
|
+
case "save_approval_rule":
|
|
2263
|
+
void cs.saveApprovalRule?.(msg.rule);
|
|
2264
|
+
break;
|
|
2265
|
+
case "save_approval_rules":
|
|
2266
|
+
void cs.saveApprovalRules?.(msg.rules);
|
|
2267
|
+
break;
|
|
2268
|
+
case "delete_approval_rule":
|
|
2269
|
+
void cs.deleteApprovalRule?.(msg.id);
|
|
2270
|
+
break;
|
|
2271
|
+
case "reset_builtin_approval_rule":
|
|
2272
|
+
void cs.resetBuiltinApprovalRule?.(msg.id);
|
|
2273
|
+
break;
|
|
2146
2274
|
case "apply_preset":
|
|
2147
2275
|
void cs.applyPreset(msg.name);
|
|
2148
2276
|
break;
|
|
@@ -2229,11 +2357,10 @@ wss.on("connection", (ws) => {
|
|
|
2229
2357
|
if (msg.type === "hello") {
|
|
2230
2358
|
const cid = msg.clientId || randomUUID();
|
|
2231
2359
|
clientId = cid;
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
return;
|
|
2360
|
+
// issue #295:ready 先行 —— 传输握手不等待会话初始化。attach 会进 SDK 的
|
|
2361
|
+
// resourceLoader.reload 等同步目录扫描,坏挂载/家目录下可能阻塞数十秒;
|
|
2362
|
+
// ready 在握手里先发,前端立刻离开「正在连接」(快照随后到)。
|
|
2363
|
+
if (!closed) {
|
|
2237
2364
|
send({
|
|
2238
2365
|
type: "ready",
|
|
2239
2366
|
clientId: cid,
|
|
@@ -2249,6 +2376,24 @@ wss.on("connection", (ws) => {
|
|
|
2249
2376
|
tabs: TABS ? [...TABS] : undefined,
|
|
2250
2377
|
service: SERVICE_INFO ?? undefined,
|
|
2251
2378
|
});
|
|
2379
|
+
}
|
|
2380
|
+
// attach 慢提示:本体继续等,不取消;完成后照常走快照流程。
|
|
2381
|
+
const slowTimer = setTimeout(() => {
|
|
2382
|
+
if (closed)
|
|
2383
|
+
return;
|
|
2384
|
+
send({
|
|
2385
|
+
type: "notice",
|
|
2386
|
+
level: "warning",
|
|
2387
|
+
text: "会话初始化耗时较长(可能在扫描工作区目录),请稍候…",
|
|
2388
|
+
textEn: "Session init is taking a while (possibly scanning the workspace) — hang on…",
|
|
2389
|
+
});
|
|
2390
|
+
}, ATTACH_SLOW_NOTICE_MS);
|
|
2391
|
+
service
|
|
2392
|
+
.attach(cid, send)
|
|
2393
|
+
.then((cs) => {
|
|
2394
|
+
clearTimeout(slowTimer);
|
|
2395
|
+
if (closed)
|
|
2396
|
+
return;
|
|
2252
2397
|
// Plugin catalog: re-scan + activate new dirs on every attach so
|
|
2253
2398
|
// freshly dropped plugins show up without a server restart.
|
|
2254
2399
|
pluginMgr
|
|
@@ -2306,6 +2451,7 @@ wss.on("connection", (ws) => {
|
|
|
2306
2451
|
// Admission refused (quiesce): close the socket so the browser
|
|
2307
2452
|
// reconnect loop keeps retrying until admission reopens. Do NOT
|
|
2308
2453
|
// leave a half-alive connection that can only show an error.
|
|
2454
|
+
clearTimeout(slowTimer);
|
|
2309
2455
|
if (err instanceof QuiesceRejectedError) {
|
|
2310
2456
|
closed = true;
|
|
2311
2457
|
if (ws.readyState === WebSocket.OPEN) {
|
|
@@ -2398,7 +2544,8 @@ if (!bootCatalogDisabled) {
|
|
|
2398
2544
|
customCatalogPath: pluginMgr.customCatalogPath,
|
|
2399
2545
|
pluginsDir: join(DATA_DIR, "plugins"),
|
|
2400
2546
|
installer: pluginInstaller,
|
|
2401
|
-
|
|
2547
|
+
// 默认仅同步市场列表;重载插件会重复触发其激活广播。
|
|
2548
|
+
afterWrite: () => (autoInstall ? reloadPluginsAndPush() : pluginMgr.pushCatalog()),
|
|
2402
2549
|
}).then((r) => {
|
|
2403
2550
|
if (!r.ok) {
|
|
2404
2551
|
console.warn(`[catalog] 插件目录预同步失败(不阻断启动): ${r.error}`);
|
|
@@ -2485,6 +2632,7 @@ async function shutdown(signal = "SIGINT") {
|
|
|
2485
2632
|
pluginInstaller.dispose();
|
|
2486
2633
|
mcpHotReload.dispose();
|
|
2487
2634
|
mcpBridge.dispose();
|
|
2635
|
+
await globalLspPool.shutdownAll();
|
|
2488
2636
|
await service.disposeAll();
|
|
2489
2637
|
// Don't let dead browsers hold the exit open: half-open WebSocket /
|
|
2490
2638
|
// keep-alive HTTP connections (e.g. test clients killed without
|