pi-web-ui 0.94.1 → 0.96.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 +91 -2
- package/README.md +5 -6
- package/README.zh-CN.md +4 -5
- package/bin/pi-web-ui.mjs +910 -237
- package/dist/server/agent-service.js +2655 -231
- package/dist/server/approval-rules.js +596 -0
- package/dist/server/attachment-store.js +129 -0
- package/dist/server/attachments.js +92 -174
- package/dist/server/bg-servers.js +45 -7
- package/dist/server/claim-files-tool.js +4 -8
- package/dist/server/claim-store.js +4 -2
- package/dist/server/client-state.js +67 -14
- package/dist/server/compact-context-tool.js +126 -0
- package/dist/server/composer-drafts.js +9 -0
- package/dist/server/context-budget.js +317 -0
- package/dist/server/control-socket.js +57 -28
- package/dist/server/conversation-read-tool.js +7 -17
- package/dist/server/dangling-tools.js +229 -0
- package/dist/server/delegate-task.js +25 -28
- package/dist/server/dsh/dsh-agent-service.js +69 -66
- package/dist/server/edit-soft-tool.js +84 -16
- package/dist/server/eval-tool.js +588 -0
- package/dist/server/file-archives.js +16 -5
- package/dist/server/files-service.js +65 -18
- package/dist/server/goal-service.js +371 -77
- package/dist/server/hashline-engine.js +703 -0
- package/dist/server/host-guard.js +94 -0
- package/dist/server/host-metrics.js +26 -2
- package/dist/server/i18n.js +3 -3
- package/dist/server/index.js +456 -80
- package/dist/server/lsp-tool.js +1371 -0
- package/dist/server/mcp-bridge.js +47 -3
- package/dist/server/model-admin.js +112 -32
- package/dist/server/office-parse.js +375 -0
- package/dist/server/patch-tool.js +85 -0
- package/dist/server/permission-preset.js +25 -0
- package/dist/server/plan-manager.js +114 -0
- package/dist/server/plugin-api-catalog.js +296 -0
- package/dist/server/plugin-catalog-sync.js +36 -19
- package/dist/server/plugin-catalog.js +11 -4
- package/dist/server/plugin-facilities.js +92 -21
- package/dist/server/plugin-install-spec.js +196 -0
- package/dist/server/plugin-installer.js +73 -0
- package/dist/server/plugin-llm.js +71 -65
- package/dist/server/plugin-manifest-validate.js +305 -0
- package/dist/server/plugin-project.js +117 -13
- package/dist/server/plugin-tool-guard.js +120 -0
- package/dist/server/plugin-updater.js +115 -12
- package/dist/server/plugins.js +950 -205
- package/dist/server/present-files-tool.js +9 -12
- package/dist/server/process-utils.js +16 -6
- package/dist/server/prompt-composer.js +9 -0
- package/dist/server/protocol-version.js +1 -1
- package/dist/server/read-tool.js +69 -30
- package/dist/server/resolve-global-sdk.js +30 -16
- package/dist/server/schedule-agent-tool.js +12 -15
- package/dist/server/scheduler-tasks.js +6 -0
- package/dist/server/sdk-origin.js +18 -2
- package/dist/server/serialize.js +111 -14
- package/dist/server/settings-service.js +112 -3
- package/dist/server/skill-tool.js +5 -7
- package/dist/server/subagent-templates.js +51 -0
- package/dist/server/subagents.js +83 -66
- package/dist/server/terminals.js +248 -81
- package/dist/server/tool-approval.js +84 -0
- package/dist/server/tool-manager.js +237 -12
- package/dist/server/tool-overrides.js +59 -0
- package/dist/server/update-check.js +28 -1
- package/dist/server/uploads.js +17 -2
- package/dist/server/wait-subscription-scan.js +18 -21
- package/dist/server/workspace-snapshot.js +113 -0
- package/dist/server/ws-client-id.js +29 -0
- package/dist/server/ws-pending-queue.js +60 -0
- package/extensions/webui.ts +60 -2
- package/package.json +5 -3
- 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 +40 -0
- package/themes/aetheris.css +457 -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/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/themes/zhupi-dark.css +658 -0
- package/themes/zhupi.css +711 -0
- package/web/dist/assets/{TerminalPanel-MVoxpJOA.js → TerminalPanel-DDChcYOh.js} +1 -1
- package/web/dist/assets/index-Do9RgJC3.js +366 -0
- package/web/dist/assets/index-DryOsILO.css +1 -0
- package/web/dist/assets/{markdown-eUQn_o9D.js → markdown-DXwnfD9T.js} +1 -1
- package/web/dist/index.html +3 -3
- package/web/dist/assets/index-B3S9MxnN.css +0 -1
- package/web/dist/assets/index-DVLrHI2E.js +0 -364
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS hello 的 clientId 校验 —— 独立成模块是为了可单测
|
|
3
|
+
* (`server/index.ts` 起 WS + SDK,不适合在单测里 import)。
|
|
4
|
+
*
|
|
5
|
+
* 背景:hello.clientId 原样进入 service.attach → ClientSession → saveUpload
|
|
6
|
+
* (`<dataDir>/uploads/<clientId>/`,见 attachments.ts / dsh-agent-service.ts),
|
|
7
|
+
* 无校验时外部可传对象/数字/路径穿越串。合法客户端用 uuid(含连字符,在字符集
|
|
8
|
+
* 内);服务端伪客户端是 `scheduler:`/`plugin:` 前缀(冒号在字符集内),但那些
|
|
9
|
+
* 不走 WS hello——收进字符集只是不挡未来的复用,不构成放行面。
|
|
10
|
+
*/
|
|
11
|
+
/** clientId 长度上限:uuid 36 字符的数倍余量,防超长串写盘/进日志。 */
|
|
12
|
+
export const MAX_CLIENT_ID_LEN = 128;
|
|
13
|
+
/** clientId 字符集:uuid(含 -)、伪客户端前缀(含 :)、下划线。 */
|
|
14
|
+
const CLIENT_ID_RE = /^[A-Za-z0-9:_-]+$/;
|
|
15
|
+
/**
|
|
16
|
+
* 校验 hello.clientId:必须是 string、1-128 字符、限定字符集。
|
|
17
|
+
* 合法原样返回;不合法(类型不对/空串/超长/含非法字符)返回 null,
|
|
18
|
+
* 调用方回退 randomUUID()——校验必须在 hello 入口统一做(服务端所有把
|
|
19
|
+
* clientId 落盘/当目录名的路径都从 attach 这一条链来)。
|
|
20
|
+
*/
|
|
21
|
+
export function validateClientId(input) {
|
|
22
|
+
if (typeof input !== "string")
|
|
23
|
+
return null;
|
|
24
|
+
if (input.length < 1 || input.length > MAX_CLIENT_ID_LEN)
|
|
25
|
+
return null;
|
|
26
|
+
if (!CLIENT_ID_RE.test(input))
|
|
27
|
+
return null;
|
|
28
|
+
return input;
|
|
29
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* attach 完成前暂存客户端命令的队列 —— 独立成模块是为了可单测
|
|
3
|
+
* (`server/index.ts` 起 WS + SDK,不适合在单测里 import)。
|
|
4
|
+
*
|
|
5
|
+
* 背景:hello 先回 ready(issue #295)后,attach/插件链就绪前收到的命令一律进
|
|
6
|
+
* pending 队列等重放;attach 挂死(坏挂载/家目录扫描数十秒)或失败保活时队列
|
|
7
|
+
* 原先无上限——失控/恶意连接可在此期间灌消息把内存顶爆。加上限后超限丢最旧并
|
|
8
|
+
* 计数(做法对齐 ws-unknown-types 的「上限 + 溢出计数 + 节流 warn」先例)。
|
|
9
|
+
*/
|
|
10
|
+
/** 队列上限:正常前端在 attach 窗口内只发 get_state 等零星几条,256 绰绰有余。 */
|
|
11
|
+
export const MAX_PENDING_WS_MESSAGES = 256;
|
|
12
|
+
/** 同一进程两次超限告警的最小间隔(防刷屏,对齐 ws-unknown-types 的节流思路)。 */
|
|
13
|
+
export const PENDING_DROP_WARN_INTERVAL_MS = 60_000;
|
|
14
|
+
let lastWarnAt = 0;
|
|
15
|
+
/** 超限丢弃的节流告警:`PENDING_DROP_WARN_INTERVAL_MS` 内最多一条。log 注入便于单测。 */
|
|
16
|
+
export function warnPendingDrop(totalDropped, log) {
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
if (now - lastWarnAt < PENDING_DROP_WARN_INTERVAL_MS)
|
|
19
|
+
return;
|
|
20
|
+
lastWarnAt = now;
|
|
21
|
+
log(`[ws] attach 未完成,命令队列超限丢最旧(累计 ${totalDropped} 条)`);
|
|
22
|
+
}
|
|
23
|
+
/** 单测隔离用。 */
|
|
24
|
+
export function resetPendingDropWarn() {
|
|
25
|
+
lastWarnAt = 0;
|
|
26
|
+
}
|
|
27
|
+
/** attach 前的命令暂存队列:超限丢最旧(shift),丢弃只计数,不阻断新消息入队。 */
|
|
28
|
+
export class PendingCommandQueue {
|
|
29
|
+
cap;
|
|
30
|
+
items = [];
|
|
31
|
+
droppedTotal = 0;
|
|
32
|
+
constructor(cap = MAX_PENDING_WS_MESSAGES) {
|
|
33
|
+
this.cap = cap;
|
|
34
|
+
}
|
|
35
|
+
push(msg) {
|
|
36
|
+
if (this.items.length >= this.cap) {
|
|
37
|
+
this.items.shift();
|
|
38
|
+
this.droppedTotal++;
|
|
39
|
+
warnPendingDrop(this.droppedTotal, (line) => console.warn(line));
|
|
40
|
+
}
|
|
41
|
+
this.items.push(msg);
|
|
42
|
+
}
|
|
43
|
+
/** 取走全部并清空(attach 完成后的重放)。 */
|
|
44
|
+
drain() {
|
|
45
|
+
const out = this.items;
|
|
46
|
+
this.items = [];
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/** attach 失败保活 / 连接关闭时清空(用户修好后重发的 get_state 会重新入队)。 */
|
|
50
|
+
clear() {
|
|
51
|
+
this.items = [];
|
|
52
|
+
}
|
|
53
|
+
/** 累计丢弃条数(排障用)。 */
|
|
54
|
+
get dropped() {
|
|
55
|
+
return this.droppedTotal;
|
|
56
|
+
}
|
|
57
|
+
get size() {
|
|
58
|
+
return this.items.length;
|
|
59
|
+
}
|
|
60
|
+
}
|
package/extensions/webui.ts
CHANGED
|
@@ -17,8 +17,9 @@
|
|
|
17
17
|
* - 每个 pi 会话管理一个子进程;session_shutdown 时清理,避免孤儿进程。
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { spawn } from "node:child_process";
|
|
20
|
+
import { spawn, execSync } from "node:child_process";
|
|
21
21
|
import { existsSync } from "node:fs";
|
|
22
|
+
import { homedir } from "node:os";
|
|
22
23
|
import { dirname, join, resolve } from "node:path";
|
|
23
24
|
import { fileURLToPath } from "node:url";
|
|
24
25
|
import net from "node:net";
|
|
@@ -27,7 +28,63 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
|
|
|
27
28
|
// 本文件位于 <pkg>/extensions/webui.ts → 包根在上一级
|
|
28
29
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
30
|
const SERVER_ENTRY = join(PKG_ROOT, "dist", "server", "index.js");
|
|
30
|
-
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 解析真实的 node 可执行路径。
|
|
34
|
+
*
|
|
35
|
+
* pi 0.87+ 的 TUI 版本是 Bun 打包的二进制(pi.exe),此时 process.execPath 指向
|
|
36
|
+
* pi.exe 而非 node。用 pi.exe 去 spawn(index.js) 会静默失败(pi.exe 不能当
|
|
37
|
+
* node 执行脚本)。此函数在 Bun 环境或 execPath 非 node 时,从 PATH 和常见安装
|
|
38
|
+
* 位置回退查找真实 node。
|
|
39
|
+
*/
|
|
40
|
+
function resolveNode(): string {
|
|
41
|
+
const base = process.execPath.toLowerCase();
|
|
42
|
+
// 普通 node 直接返回(未被 Bun 接管)
|
|
43
|
+
if (!(process as any).isBun && /(^|[/\\])(node|nodejs)(\.exe)?$/.test(base)) {
|
|
44
|
+
return process.execPath;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const candidates: string[] = [];
|
|
48
|
+
// 1. 从 PATH 里找(跨平台:Windows 用 where,其余用 which)
|
|
49
|
+
try {
|
|
50
|
+
const cmd = process.platform === "win32" ? "where node" : "which node";
|
|
51
|
+
const out = execSync(cmd, { windowsHide: true }).toString().trim();
|
|
52
|
+
for (const line of out.split(/\r?\n/)) {
|
|
53
|
+
const p = line.trim();
|
|
54
|
+
// 过滤掉 pi 自身
|
|
55
|
+
if (p && !/pi[/\\]pi(\.exe)?$/i.test(p)) candidates.push(p);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
/* node 不在 PATH */
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. 常见安装位置兜底(Windows 为主,也兼顾 Unix)
|
|
62
|
+
const home = homedir();
|
|
63
|
+
const extra: string[] = [];
|
|
64
|
+
if (process.env.PNPM_HOME) extra.push(join(process.env.PNPM_HOME, "node.exe"));
|
|
65
|
+
for (const rel of [
|
|
66
|
+
"scoop/apps/nodejs/current/node.exe",
|
|
67
|
+
"AppData/Roaming/nvm/current/node.exe",
|
|
68
|
+
".nvm/versions/node/current/bin/node",
|
|
69
|
+
".local/share/pnpm/node.exe",
|
|
70
|
+
".local/bin/node",
|
|
71
|
+
]) {
|
|
72
|
+
extra.push(join(home, ...rel.split("/")));
|
|
73
|
+
}
|
|
74
|
+
for (const c of extra) {
|
|
75
|
+
if (existsSync(c) && !/pi[/\\]pi(\.exe)?$/i.test(c)) candidates.push(c);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (candidates.length > 0) return candidates[0];
|
|
79
|
+
|
|
80
|
+
console.warn(
|
|
81
|
+
"[webui] 未找到真实 node(当前 process.execPath 指向非 node 进程,疑似 Bun 环境)。" +
|
|
82
|
+
"将退回 process.execPath,子进程可能无法启动。请确保 node 在 PATH 中。",
|
|
83
|
+
);
|
|
84
|
+
return process.execPath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const NODE = resolveNode();
|
|
31
88
|
|
|
32
89
|
/** 每个会话的服务器子进程 + 元数据 */
|
|
33
90
|
interface RunningServer {
|
|
@@ -161,6 +218,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
161
218
|
const env = {
|
|
162
219
|
...process.env,
|
|
163
220
|
PORT: String(port),
|
|
221
|
+
PI_WEB_PORT: String(port), // server/index.js 读取 PI_WEB_PORT
|
|
164
222
|
PI_WEB_CWD: cwd,
|
|
165
223
|
...(process.env.PI_WEB_DATA_DIR ? {} : { PI_WEB_DATA_DIR: join(cwd, ".pi-web") }),
|
|
166
224
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.96.0",
|
|
4
4
|
"description": "Just open your browser to get all your work done — browser cockpit for AI coding agents (chat, code, files, terminal, Git in one tab)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -67,12 +67,13 @@
|
|
|
67
67
|
"test": "vitest run",
|
|
68
68
|
"test:unit": "vitest run",
|
|
69
69
|
"test:smoke": "node tests/run-smoke.mjs",
|
|
70
|
+
"test:smoke:core": "node tests/run-smoke.mjs --core",
|
|
70
71
|
"check:protocol": "node scripts/check-protocol-sync.mjs",
|
|
71
72
|
"i18n:diff": "node scripts/i18n-diff.mjs",
|
|
72
73
|
"release:notes": "node scripts/release-notes.mjs",
|
|
73
74
|
"changelog:i18n": "node scripts/release-notes.mjs --unreleased",
|
|
74
75
|
"test:freeze": "node tests/freeze-test.mjs",
|
|
75
|
-
"start": "node dist/server/index.js",
|
|
76
|
+
"start": "node --import ./dist/server/resolve-global-sdk.js dist/server/index.js",
|
|
76
77
|
"build:desktop": "tsc -p desktop/tsconfig.json",
|
|
77
78
|
"desktop:dev": "npm run build:desktop && electron dist/desktop/main.js",
|
|
78
79
|
"desktop:dist": "npm run build && npm run build:desktop && electron-builder -c desktop/electron-builder.yml",
|
|
@@ -84,7 +85,7 @@
|
|
|
84
85
|
"dependencies": {
|
|
85
86
|
"@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.1-rc.2",
|
|
86
87
|
"@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
|
|
87
|
-
"@earendil-works/pi-coding-agent": ">=0.85.1
|
|
88
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1",
|
|
88
89
|
"@xterm/addon-fit": "^0.11.0",
|
|
89
90
|
"@xterm/xterm": "^6.0.0",
|
|
90
91
|
"compression": "^1.8.2",
|
|
@@ -101,6 +102,7 @@
|
|
|
101
102
|
"rehype-highlight": "^7.0.2",
|
|
102
103
|
"rehype-katex": "^7.0.1",
|
|
103
104
|
"rehype-raw": "^7.0.0",
|
|
105
|
+
"rehype-sanitize": "^6.0.0",
|
|
104
106
|
"remark-breaks": "^4.0.0",
|
|
105
107
|
"remark-gfm": "^4.0.1",
|
|
106
108
|
"remark-math": "^6.0.0",
|
package/plugin-sdk/README.md
CHANGED
|
@@ -125,4 +125,29 @@ describe("my-plugin", () => {
|
|
|
125
125
|
| `schedule` 持久版(`{id, persistent, catchUp, label}`,落盘+补跑+进后台面板) | ✅ | — |
|
|
126
126
|
| `host.requestPermission`(动态授权:net 补主机 / llm 限模型,用户逐条确认可记住) | ✅(基础族须已声明) | 确认框 + 设置面板「已授权能力」 |
|
|
127
127
|
|
|
128
|
+
## 反激活与清理:`host.effect(label, dispose)`
|
|
129
|
+
|
|
130
|
+
宿主的每个注册面(`ui.register` / `registerAgentTool` / `registerCommand` / `route` /
|
|
131
|
+
`registerProxy` / `fs.watch` / `schedule` / `registerBackgroundTask` / `events.on` /
|
|
132
|
+
`onStats` / `onStreaming` / `onSettingsChanged` …)都返回注销函数,**并且**已经在宿主内部
|
|
133
|
+
登记进该插件的 effect 栈:反激活(禁用、卸载、`plugins_reload`)时**逆序回卷**,插件忘了
|
|
134
|
+
调用返回的注销函数也不会留下孤儿订阅/定时器/路由。
|
|
135
|
+
|
|
136
|
+
你自己建的东西(`setInterval` / `addEventListener` / WebSocket / 自建缓存)挂在
|
|
137
|
+
`host.effect` 上同样享受这条保障:
|
|
138
|
+
|
|
139
|
+
```js
|
|
140
|
+
export default definePlugin({
|
|
141
|
+
async activate(host) {
|
|
142
|
+
const timer = setInterval(() => host.log("info", "tick"), 60_000);
|
|
143
|
+
host.effect("my-timer", () => clearInterval(timer));
|
|
144
|
+
// 也可以两者都写(幂等:dispose 写成本身幂等即可)
|
|
145
|
+
const off = host.effect("my-ws", () => ws.close());
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
清理抛错只记一条诊断、不阻断其它清理;`host.effect` 返回的注销函数与反激活都会调
|
|
151
|
+
`dispose`,所以 `dispose` 请写成**幂等**的。
|
|
152
|
+
|
|
128
153
|
完整契约见 `docs/architecture-plugins.md`。
|
package/plugin-sdk/index.d.ts
CHANGED
|
@@ -5,16 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
export type PluginPermissionFamily =
|
|
8
|
-
| "fs"
|
|
9
|
-
| "fs:read"
|
|
10
|
-
| "fs:write"
|
|
11
|
-
| "ui"
|
|
12
|
-
| "tools"
|
|
13
|
-
| "http"
|
|
14
|
-
| "chat"
|
|
15
|
-
| "net"
|
|
16
|
-
| "dom"
|
|
17
|
-
| "dom:anchor";
|
|
8
|
+
"fs" | "fs:read" | "fs:write" | "ui" | "tools" | "http" | "chat" | "net" | "dom" | "dom:anchor";
|
|
18
9
|
|
|
19
10
|
export interface WsEntry {
|
|
20
11
|
name: string;
|
|
@@ -35,17 +26,7 @@ export interface UiSelectOption {
|
|
|
35
26
|
}
|
|
36
27
|
|
|
37
28
|
export type UiItemKind =
|
|
38
|
-
| "
|
|
39
|
-
| "action"
|
|
40
|
-
| "badge"
|
|
41
|
-
| "menu"
|
|
42
|
-
| "page"
|
|
43
|
-
| "organizer"
|
|
44
|
-
| "divider"
|
|
45
|
-
| "toggle"
|
|
46
|
-
| "input"
|
|
47
|
-
| "progress"
|
|
48
|
-
| "select";
|
|
29
|
+
"view" | "action" | "badge" | "menu" | "page" | "organizer" | "divider" | "toggle" | "input" | "progress" | "select";
|
|
49
30
|
|
|
50
31
|
export interface UiContribution {
|
|
51
32
|
id: string;
|
|
@@ -152,13 +133,7 @@ export interface PluginHostUi {
|
|
|
152
133
|
/** host.llm.complete:孤立无工具的一次性补全(不建对话、不进历史)。
|
|
153
134
|
* 需要 manifest.permissions 含 "llm"(花用户自己的模型额度)。 */
|
|
154
135
|
export interface PluginHostLlm {
|
|
155
|
-
complete(req: {
|
|
156
|
-
prompt: string;
|
|
157
|
-
system?: string;
|
|
158
|
-
model?: string;
|
|
159
|
-
maxChars?: number;
|
|
160
|
-
timeoutMs?: number;
|
|
161
|
-
}): Promise<{
|
|
136
|
+
complete(req: { prompt: string; system?: string; model?: string; maxChars?: number; timeoutMs?: number }): Promise<{
|
|
162
137
|
ok: boolean;
|
|
163
138
|
text?: string;
|
|
164
139
|
model?: string;
|
|
@@ -167,6 +142,19 @@ export interface PluginHostLlm {
|
|
|
167
142
|
}>;
|
|
168
143
|
}
|
|
169
144
|
|
|
145
|
+
/** 工具拦截守卫看到的 pre 请求(只对 bash/read 生效,全量见 server/plugin-tool-guard.ts)。 */
|
|
146
|
+
export interface ToolGuardPreRequest {
|
|
147
|
+
toolName: "bash" | "read";
|
|
148
|
+
params: unknown;
|
|
149
|
+
conversationId?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** 工具 pre 决策:allow 放行 / deny 拒绝 / ask 待确认(暂按拒绝执行)。 */
|
|
153
|
+
export type ToolGuardPreDecision =
|
|
154
|
+
| { decision: "allow" }
|
|
155
|
+
| { decision: "deny"; reason?: string; reasonEn?: string }
|
|
156
|
+
| { decision: "ask"; reason?: string; reasonEn?: string };
|
|
157
|
+
|
|
170
158
|
/** 插件服务端入口拿到的宿主接口(精简:全量见 server/plugins.ts PluginHost)。 */
|
|
171
159
|
export interface PluginHost {
|
|
172
160
|
broadcast(payload: unknown): void;
|
|
@@ -175,6 +163,12 @@ export interface PluginHost {
|
|
|
175
163
|
sendTo(clientId: string, payload: unknown): void;
|
|
176
164
|
onAttach(handler: (clientId: string) => void): () => void;
|
|
177
165
|
onToolEvent(handler: (ev: unknown) => void): () => void;
|
|
166
|
+
/** 注册工具 pre 拦截守卫(只对 bash/read 生效;要 "tools" 能力)。 */
|
|
167
|
+
onToolPre(
|
|
168
|
+
handler: (req: ToolGuardPreRequest) => ToolGuardPreDecision | void | Promise<ToolGuardPreDecision | void>,
|
|
169
|
+
): () => void;
|
|
170
|
+
/** 注册工具 post 编辑守卫(只对 bash/read 生效;要 "tools" 能力)。 */
|
|
171
|
+
onToolPost(handler: (req: unknown) => unknown): () => void;
|
|
178
172
|
onRunEvent(handler: (ev: unknown) => void): () => void;
|
|
179
173
|
getActiveConversation(): unknown;
|
|
180
174
|
onConversationChanged(handler: () => void): () => void;
|
|
@@ -219,6 +213,11 @@ export interface PluginHost {
|
|
|
219
213
|
* 200 条,单条截断 500 字符,不落盘);error 级同时走 console.error。
|
|
220
214
|
* 用户在设置面板“界面插件”页点某插件的“日志”按需查看(级别过滤 + 清空)。 */
|
|
221
215
|
log(level?: "debug" | "info" | "warn" | "error", ...args: unknown[]): void;
|
|
216
|
+
/** 登记一条**自建**的可逆副作用(自建 setInterval / event 监听 / WebSocket…):
|
|
217
|
+
* 返回的注销函数与插件反激活**都会**调 dispose。宿主自己的每个注册面已在内部
|
|
218
|
+
* 走同一个 effect 栈,这里只用于「宿主管不到的那些」—— 挂进来就不怕漏注销
|
|
219
|
+
* (热重载后定时器叠加、监听器堆积都是这个漏法的症状)。dispose 请写成幂等的。 */
|
|
220
|
+
effect(label: string, dispose: () => void): () => void;
|
|
222
221
|
/** 无头调用:把外部通道文本投给 agent(要 "chat" 能力;宿主未接 chatProvider
|
|
223
222
|
* 时 reject(由 chatWait 包成 {ok:false},插件侧用 chatWait 更省心)。 */
|
|
224
223
|
chat(req: { text: string; accountId?: string }): Promise<{
|
|
@@ -259,12 +258,7 @@ export interface PluginHost {
|
|
|
259
258
|
}>;
|
|
260
259
|
};
|
|
261
260
|
/** 常驻后台任务(顶栏「后台任务」面板;返回 update/unregister)。 */
|
|
262
|
-
registerBackgroundTask(task: {
|
|
263
|
-
id: string;
|
|
264
|
-
label: string;
|
|
265
|
-
stop?: () => void;
|
|
266
|
-
status?: string;
|
|
267
|
-
}): {
|
|
261
|
+
registerBackgroundTask(task: { id: string; label: string; stop?: () => void; status?: string }): {
|
|
268
262
|
update(next: Partial<{ label: string; status: string; stop: () => void }>): void;
|
|
269
263
|
unregister(): void;
|
|
270
264
|
};
|
|
@@ -340,10 +334,9 @@ export declare function defineView(view: PluginViewModule): PluginViewModule;
|
|
|
340
334
|
export declare function defineRenderer(
|
|
341
335
|
renderers: Record<string, (code: string, ctx: PluginViewContext) => HTMLElement | null>,
|
|
342
336
|
): Pick<PluginViewModule, "renderers">;
|
|
343
|
-
export declare function actionHandler(
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
) => void;
|
|
337
|
+
export declare function actionHandler(
|
|
338
|
+
map: Record<string, (value?: string) => void>,
|
|
339
|
+
): (itemId: string, value?: string) => void;
|
|
347
340
|
export declare function onUiAction(action: string, handler: (itemId: string, value?: string) => void): () => void;
|
|
348
341
|
export declare function getSetting<T>(host: PluginHost, key: string, fallback: T): T;
|
|
349
342
|
export declare function selectOptions(list: Array<string | UiSelectOption>): UiSelectOption[];
|
package/plugin-sdk/index.mjs
CHANGED
|
@@ -211,18 +211,20 @@ export function createMockHost(overrides) {
|
|
|
211
211
|
// 订阅类:存 handler + 返回注销函数(调用本身由外层统一记进 calls)。
|
|
212
212
|
// handler 缺省取首参;fs.watch(path, handler) / events.on(topic, handler) /
|
|
213
213
|
// shortcuts.register(key, handler) 这类回调在第二参,用 sub(method, 1)。
|
|
214
|
-
const sub =
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
214
|
+
const sub =
|
|
215
|
+
(method, argIndex = 0) =>
|
|
216
|
+
(...args) => {
|
|
217
|
+
const handler = args[argIndex];
|
|
218
|
+
if (typeof handler === "function") handlersOf(method).push(handler);
|
|
219
|
+
let done = false;
|
|
220
|
+
return () => {
|
|
221
|
+
if (done) return;
|
|
222
|
+
done = true;
|
|
223
|
+
const arr = handlersOf(method);
|
|
224
|
+
const i = arr.indexOf(handler);
|
|
225
|
+
if (i >= 0) arr.splice(i, 1);
|
|
226
|
+
};
|
|
224
227
|
};
|
|
225
|
-
};
|
|
226
228
|
const logImpl = (levelOrArg, ...args) => {
|
|
227
229
|
const levels = ["debug", "info", "warn", "error"];
|
|
228
230
|
const level = levels.includes(levelOrArg) ? levelOrArg : "info";
|
|
@@ -243,12 +245,16 @@ export function createMockHost(overrides) {
|
|
|
243
245
|
try {
|
|
244
246
|
console.error("[mock-host]", text);
|
|
245
247
|
} catch {
|
|
246
|
-
|
|
248
|
+
/* 打印失败不影响单测 */
|
|
247
249
|
}
|
|
248
250
|
}
|
|
249
251
|
};
|
|
250
252
|
const statImpl = (p) => ({
|
|
251
|
-
name:
|
|
253
|
+
name:
|
|
254
|
+
String(p ?? "")
|
|
255
|
+
.split(/[\\/]/)
|
|
256
|
+
.filter(Boolean)
|
|
257
|
+
.pop() ?? "",
|
|
252
258
|
type: "file",
|
|
253
259
|
size: 0,
|
|
254
260
|
mtime: 0,
|
|
@@ -264,6 +270,7 @@ export function createMockHost(overrides) {
|
|
|
264
270
|
const routeStore = [];
|
|
265
271
|
const proxyStore = [];
|
|
266
272
|
const scheduleStore = [];
|
|
273
|
+
const effectStore = [];
|
|
267
274
|
// 注册类:存定义体 + 返回只摘除本次注册的注销函数(与宿主同语义)。
|
|
268
275
|
const remember = (store, entry) => {
|
|
269
276
|
if (entry !== undefined) store.push(entry);
|
|
@@ -283,6 +290,8 @@ export function createMockHost(overrides) {
|
|
|
283
290
|
sendTo: noop,
|
|
284
291
|
onAttach: sub("onAttach"),
|
|
285
292
|
onToolEvent: sub("onToolEvent"),
|
|
293
|
+
onToolPre: sub("onToolPre"),
|
|
294
|
+
onToolPost: sub("onToolPost"),
|
|
286
295
|
onRunEvent: sub("onRunEvent"),
|
|
287
296
|
getActiveConversation: () => null,
|
|
288
297
|
chat: async () => okFalse("chatProvider(无头调用未接入)"),
|
|
@@ -313,8 +322,7 @@ export function createMockHost(overrides) {
|
|
|
313
322
|
const list = Array.isArray(items) ? items : [items];
|
|
314
323
|
const added = [];
|
|
315
324
|
for (const raw of list.slice(0, 32)) {
|
|
316
|
-
const id =
|
|
317
|
-
raw && typeof raw.id === "string" && raw.id ? raw.id : `mock-${++uiAutoId}`;
|
|
325
|
+
const id = raw && typeof raw.id === "string" && raw.id ? raw.id : `mock-${++uiAutoId}`;
|
|
318
326
|
uiItems.set(id, isPlainObject(raw) ? { ...raw, id } : { id });
|
|
319
327
|
added.push(id);
|
|
320
328
|
}
|
|
@@ -421,6 +429,12 @@ export function createMockHost(overrides) {
|
|
|
421
429
|
on: sub("events.on", 1),
|
|
422
430
|
},
|
|
423
431
|
log: logImpl,
|
|
432
|
+
// effect 栈:mock 里只记调用 + 返回可撤函数(真实宿主在反激活时逆序回卷)。
|
|
433
|
+
// 单测里测试 dispose 真跑的写法:拿到 off() 后调它,或断言 host.calls 里有 effect。
|
|
434
|
+
effect: (label, dispose) => {
|
|
435
|
+
const entry = { label, dispose };
|
|
436
|
+
return remember(effectStore, entry);
|
|
437
|
+
},
|
|
424
438
|
// 浏览器桥兼容(client 侧逻辑单测也能用同一个 mock):无注入回退语义。
|
|
425
439
|
dialogs: {
|
|
426
440
|
select: async () => ({ ok: false }),
|
|
@@ -527,10 +541,12 @@ export function createMockHost(overrides) {
|
|
|
527
541
|
}
|
|
528
542
|
}
|
|
529
543
|
// 记录层:所有 host 方法(mock/reset 除外)先记 calls 再委托。
|
|
530
|
-
const record =
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
544
|
+
const record =
|
|
545
|
+
(method, fn) =>
|
|
546
|
+
(...args) => {
|
|
547
|
+
calls.push({ method, args, seq: calls.length });
|
|
548
|
+
return fn(...args);
|
|
549
|
+
};
|
|
534
550
|
for (const [key, value] of Object.entries(host)) {
|
|
535
551
|
if (key === "calls" || key === "logs" || key === "mock" || key === "reset") continue;
|
|
536
552
|
if (typeof value === "function") {
|
package/plugins/catalog.json
CHANGED
|
@@ -109,6 +109,16 @@
|
|
|
109
109
|
"source": "xing-shuyin/pi-web-ui/plugins/notes",
|
|
110
110
|
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/notes"
|
|
111
111
|
},
|
|
112
|
+
{
|
|
113
|
+
"id": "desktop-use",
|
|
114
|
+
"name": "桌面控制",
|
|
115
|
+
"icon": "🖱️",
|
|
116
|
+
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"5\" y=\"2\" width=\"14\" height=\"20\" rx=\"7\"/><line x1=\"12\" y1=\"6\" x2=\"12\" y2=\"10\"/></svg>",
|
|
117
|
+
"description": "让 AI 操作 Windows 桌面:按元素名查控件坐标(UI Automation,无需截图)→ 点击 / 输入(含中文粘贴)/ 按键 / 聚焦窗口。输入库缺失时自动安装,卸载时连带删除。",
|
|
118
|
+
"descriptionEn": "Let the AI drive the Windows desktop: find controls by name via UI Automation (no screenshots) → click / type (CJK via paste) / hotkeys / focus windows. The input library auto-installs on activate and is removed with the plugin.",
|
|
119
|
+
"source": "xing-shuyin/pi-web-ui/plugins/desktop-use",
|
|
120
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/desktop-use"
|
|
121
|
+
},
|
|
112
122
|
{
|
|
113
123
|
"id": "pi-web-multigit",
|
|
114
124
|
"name": "multi-git",
|
|
@@ -118,5 +128,35 @@
|
|
|
118
128
|
"descriptionEn": "Multi-repo Git overview: changes, diffs, history, branches, stashes, cross-repo search and a timeline for every repository in the workspace, plus a workspace-wide branch view, Fetch/Pull and an optional terminal.",
|
|
119
129
|
"source": "EinErste/pi-web-multigit",
|
|
120
130
|
"homepage": "https://github.com/EinErste/pi-web-multigit"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"id": "reconnect",
|
|
134
|
+
"name": "重连",
|
|
135
|
+
"icon": "🔄",
|
|
136
|
+
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M3 21v-5h5\"/></svg>",
|
|
137
|
+
"description": "断连恢复:查看网页服务与重启守护状态,一键重新连接或重启网页服务(支持单服务/全服务重启,配套可选独立守护进程)。",
|
|
138
|
+
"descriptionEn": "Reconnection & service recovery: inspect web service and companion watchdog health, reconnect or restart services in one click (supports multi-service restarts with companion watchdog).",
|
|
139
|
+
"source": "xieweimo/pi-web-ui-reconnect-watchdog/reconnect-plugin",
|
|
140
|
+
"homepage": "https://github.com/xieweimo/pi-web-ui-reconnect-watchdog"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"id": "sol-savings",
|
|
144
|
+
"name": "SoL-Pi 状态",
|
|
145
|
+
"icon": "⚡",
|
|
146
|
+
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"/></svg>",
|
|
147
|
+
"description": "SoL-Pi 会话节省统计与规划底栏:实时追踪大工具输出打包(Observation Pack)节省的上下文 Token 与在线计划(Plan)进度,借鉴 pi-sol-plan-footer 在底栏显示。",
|
|
148
|
+
"descriptionEn": "SoL-Pi session savings and plan status in the footer bar: real-time tracking of context tokens saved by Observation Pack and active Plan progress, inspired by pi-sol-plan-footer.",
|
|
149
|
+
"source": "xing-shuyin/pi-web-ui/plugins/sol-savings",
|
|
150
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/sol-savings"
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"id": "session-migrate",
|
|
154
|
+
"name": "会话迁移",
|
|
155
|
+
"icon": "🔄",
|
|
156
|
+
"iconSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8\"/><path d=\"M21 3v5h-5\"/><path d=\"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16\"/><path d=\"M8 16H3v5\"/></svg>",
|
|
157
|
+
"description": "扫描 ~/.omp/agent/sessions 与 ~/.pi/agent/sessions 的历史会话,只读预览后一键导入 pi 会话库(issue 286 Phase 1)。",
|
|
158
|
+
"descriptionEn": "Scan ~/.omp/agent/sessions and ~/.pi/agent/sessions history, preview read-only, then import into the pi session store (issue 286 Phase 1).",
|
|
159
|
+
"source": "xing-shuyin/pi-web-ui/plugins/session-migrate",
|
|
160
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/session-migrate"
|
|
121
161
|
}
|
|
122
162
|
]
|