pi-web-ui 0.80.1 → 0.81.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 -0
- package/README.md +257 -76
- package/README.zh-CN.md +236 -51
- package/deploy/nginx-subpath.conf +85 -88
- package/dist/server/mcp-bridge.js +45 -7
- package/dist/server/plugins.js +23 -2
- package/package.json +5 -3
- package/plugins/catalog.json +18 -0
- package/web/dist/assets/{TerminalPanel-DrYGGZSV.js → TerminalPanel-GQZf9XvH.js} +1 -1
- package/web/dist/assets/index-72bjWuR-.css +10 -0
- package/web/dist/assets/index-CiRSpf3e.js +347 -0
- package/web/dist/index.html +2 -2
- package/web/dist/assets/index-BFA8Pn_5.js +0 -347
- package/web/dist/assets/index-BduNm7_u.css +0 -10
|
@@ -1,88 +1,85 @@
|
|
|
1
|
-
# pi-web-ui behind nginx at a sub-path: http://<host>:83/pi/
|
|
2
|
-
# Backend app: http://127.0.0.1:8787 (default PI_WEB_PORT env)
|
|
3
|
-
#
|
|
4
|
-
# IMPORTANT: pi-web-ui 0.23+ checks the WebSocket Origin against the request
|
|
5
|
-
# Host (hostname AND port). Every proxied location MUST forward the original
|
|
6
|
-
# Host with $http_host (keeps the port). Using $host (drops the port) or
|
|
7
|
-
# leaving Host unset (defaults to 127.0.0.1:8787) makes the upgrade fail with
|
|
8
|
-
# 403 — the page loads but chat/terminal keep reconnecting.
|
|
9
|
-
#
|
|
10
|
-
# Topology (two listeners on one port: frp + LAN coexist):
|
|
11
|
-
# frp (public) -> <PUBLIC_IP>:<PUBLIC_PORT> -> 127.0.0.1:83 (PROXY protocol v2)
|
|
12
|
-
# LAN users -> http://<LAN_IP>:83/pi/ (plain HTTP listener)
|
|
13
|
-
#
|
|
14
|
-
# frpc sends PROXY v2 to nginx (transport.proxyProtocolVersion = "v2"):
|
|
15
|
-
# * 127.0.0.1:83 proxy_protocol — only the local frp client connects
|
|
16
|
-
# here (it speaks PROXY v2; nginx then sees the real visitor IP).
|
|
17
|
-
# * <LAN_IP>:83 plain HTTP — LAN browsers, no PROXY header needed.
|
|
18
|
-
#
|
|
19
|
-
# Simpler alternative (no real client IPs): drop proxy_protocol entirely and
|
|
20
|
-
# use a single `listen 83;` — then frpc must NOT set proxyProtocolVersion.
|
|
21
|
-
#
|
|
22
|
-
# The frontend uses absolute paths (/ws WebSocket, /assets/*, /favicon.svg,
|
|
23
|
-
# /api/file…) so those get their own proxied locations next to /pi/.
|
|
24
|
-
|
|
25
|
-
# Reuse for Upgrade/Connection headers (WebSocket).
|
|
26
|
-
map $http_upgrade $connection_upgrade {
|
|
27
|
-
default upgrade;
|
|
28
|
-
'' close;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
server {
|
|
32
|
-
listen 127.0.0.1:83 proxy_protocol;
|
|
33
|
-
listen <LAN_IP>:83;
|
|
34
|
-
|
|
35
|
-
server_name _;
|
|
36
|
-
|
|
37
|
-
# Trust PROXY-protocol headers only from the local frp client; plain
|
|
38
|
-
# (LAN) connections keep their real $remote_addr untouched.
|
|
39
|
-
set_real_ip_from 127.0.0.1;
|
|
40
|
-
real_ip_header proxy_protocol;
|
|
41
|
-
|
|
42
|
-
# ---- main entry: strip /pi/ and forward to the app root ----
|
|
43
|
-
location /pi/ {
|
|
44
|
-
proxy_pass http://127.0.0.1:8787/;
|
|
45
|
-
proxy_http_version 1.1;
|
|
46
|
-
# $http_host keeps the port — origin check compares hostname AND port.
|
|
47
|
-
proxy_set_header Host $http_host;
|
|
48
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
49
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
50
|
-
proxy_set_header X-Forwarded-Proto $scheme;
|
|
51
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
52
|
-
proxy_set_header Connection $connection_upgrade;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
# ---- WebSocket (the frontend connects to ws://<host>/ws) ----
|
|
56
|
-
location /ws {
|
|
57
|
-
proxy_pass http://127.0.0.1:8787;
|
|
58
|
-
proxy_http_version 1.1;
|
|
59
|
-
proxy_set_header Host $http_host;
|
|
60
|
-
proxy_set_header Upgrade $http_upgrade;
|
|
61
|
-
proxy_set_header Connection $connection_upgrade;
|
|
62
|
-
proxy_read_timeout 3600s;
|
|
63
|
-
proxy_send_timeout 3600s;
|
|
64
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
65
|
-
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
# ---- absolute asset paths baked into index.html ----
|
|
69
|
-
location /assets/ {
|
|
70
|
-
proxy_pass http://127.0.0.1:8787;
|
|
71
|
-
}
|
|
72
|
-
location = /favicon.svg {
|
|
73
|
-
proxy_pass http://127.0.0.1:8787;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return 302 /pi/;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
1
|
+
# pi-web-ui behind nginx at a sub-path: http://<host>:83/pi/
|
|
2
|
+
# Backend app: http://127.0.0.1:8787 (default PI_WEB_PORT env)
|
|
3
|
+
#
|
|
4
|
+
# IMPORTANT: pi-web-ui 0.23+ checks the WebSocket Origin against the request
|
|
5
|
+
# Host (hostname AND port). Every proxied location MUST forward the original
|
|
6
|
+
# Host with $http_host (keeps the port). Using $host (drops the port) or
|
|
7
|
+
# leaving Host unset (defaults to 127.0.0.1:8787) makes the upgrade fail with
|
|
8
|
+
# 403 — the page loads but chat/terminal keep reconnecting.
|
|
9
|
+
#
|
|
10
|
+
# Topology (two listeners on one port: frp + LAN coexist):
|
|
11
|
+
# frp (public) -> <PUBLIC_IP>:<PUBLIC_PORT> -> 127.0.0.1:83 (PROXY protocol v2)
|
|
12
|
+
# LAN users -> http://<LAN_IP>:83/pi/ (plain HTTP listener)
|
|
13
|
+
#
|
|
14
|
+
# frpc sends PROXY v2 to nginx (transport.proxyProtocolVersion = "v2"):
|
|
15
|
+
# * 127.0.0.1:83 proxy_protocol — only the local frp client connects
|
|
16
|
+
# here (it speaks PROXY v2; nginx then sees the real visitor IP).
|
|
17
|
+
# * <LAN_IP>:83 plain HTTP — LAN browsers, no PROXY header needed.
|
|
18
|
+
#
|
|
19
|
+
# Simpler alternative (no real client IPs): drop proxy_protocol entirely and
|
|
20
|
+
# use a single `listen 83;` — then frpc must NOT set proxyProtocolVersion.
|
|
21
|
+
#
|
|
22
|
+
# The frontend uses absolute paths (/ws WebSocket, /assets/*, /favicon.svg,
|
|
23
|
+
# /api/file…) so those get their own proxied locations next to /pi/.
|
|
24
|
+
|
|
25
|
+
# Reuse for Upgrade/Connection headers (WebSocket).
|
|
26
|
+
map $http_upgrade $connection_upgrade {
|
|
27
|
+
default upgrade;
|
|
28
|
+
'' close;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
server {
|
|
32
|
+
listen 127.0.0.1:83 proxy_protocol;
|
|
33
|
+
listen <LAN_IP>:83;
|
|
34
|
+
|
|
35
|
+
server_name _;
|
|
36
|
+
|
|
37
|
+
# Trust PROXY-protocol headers only from the local frp client; plain
|
|
38
|
+
# (LAN) connections keep their real $remote_addr untouched.
|
|
39
|
+
set_real_ip_from 127.0.0.1;
|
|
40
|
+
real_ip_header proxy_protocol;
|
|
41
|
+
|
|
42
|
+
# ---- main entry: strip /pi/ and forward to the app root ----
|
|
43
|
+
location /pi/ {
|
|
44
|
+
proxy_pass http://127.0.0.1:8787/;
|
|
45
|
+
proxy_http_version 1.1;
|
|
46
|
+
# $http_host keeps the port — origin check compares hostname AND port.
|
|
47
|
+
proxy_set_header Host $http_host;
|
|
48
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
49
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
50
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
51
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
52
|
+
proxy_set_header Connection $connection_upgrade;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ---- WebSocket (the frontend connects to ws://<host>/ws) ----
|
|
56
|
+
location /ws {
|
|
57
|
+
proxy_pass http://127.0.0.1:8787;
|
|
58
|
+
proxy_http_version 1.1;
|
|
59
|
+
proxy_set_header Host $http_host;
|
|
60
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
61
|
+
proxy_set_header Connection $connection_upgrade;
|
|
62
|
+
proxy_read_timeout 3600s;
|
|
63
|
+
proxy_send_timeout 3600s;
|
|
64
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
65
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
# ---- absolute asset paths baked into index.html ----
|
|
69
|
+
location /assets/ {
|
|
70
|
+
proxy_pass http://127.0.0.1:8787;
|
|
71
|
+
}
|
|
72
|
+
location = /favicon.svg {
|
|
73
|
+
proxy_pass http://127.0.0.1:8787;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# ---- media preview / download / health API ----
|
|
77
|
+
location /api/ {
|
|
78
|
+
proxy_pass http://127.0.0.1:8787;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# bare root → app entry
|
|
82
|
+
location = / {
|
|
83
|
+
return 302 /pi/;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -80,7 +80,10 @@ export class McpClient {
|
|
|
80
80
|
getTools() {
|
|
81
81
|
return this.tools.map((t) => ({ ...t }));
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* 调用一个工具,返回其结果。
|
|
85
|
+
* 纯文本块拼接成字符串(老形状,向后兼容);出现非文本块(image/resource/audio 等)时按序透传或退化提示,不再静默丢弃。
|
|
86
|
+
*/
|
|
84
87
|
async call(name, args, timeoutMs = 60000) {
|
|
85
88
|
const res = (await this.request("tools/call", { name, arguments: args }, timeoutMs));
|
|
86
89
|
if (res?.isError) {
|
|
@@ -90,14 +93,49 @@ export class McpClient {
|
|
|
90
93
|
.trim() || "MCP 工具错误";
|
|
91
94
|
throw new Error(msg);
|
|
92
95
|
}
|
|
93
|
-
//
|
|
96
|
+
// 结构化结果优先,其次内容块。
|
|
94
97
|
if (res?.structuredContent !== undefined)
|
|
95
98
|
return res.structuredContent;
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
.
|
|
100
|
-
|
|
99
|
+
const blocks = [];
|
|
100
|
+
let hasNonText = false;
|
|
101
|
+
for (const c of res.content ?? []) {
|
|
102
|
+
if (c.type === "image" && typeof c.data === "string" && c.data) {
|
|
103
|
+
// MCP image 块字段(type/data/mimeType)与 SDK 的 ImageContent 完全一致,原样透传;
|
|
104
|
+
// 超大图由 SDK 的 normalizeToolResultImages 统一缩放(afterToolCall 钩子,默认 autoResize)。
|
|
105
|
+
blocks.push({ type: "image", data: c.data, mimeType: c.mimeType?.trim() || "image/png" });
|
|
106
|
+
hasNonText = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (c.type && c.type !== "text") {
|
|
110
|
+
const r = typeof c.resource === "object" && c.resource !== null ? c.resource : {};
|
|
111
|
+
// MCP 的 EmbeddedResource 有两种承载:TextResourceContents(resource.text)与
|
|
112
|
+
// BlobResourceContents(resource.blob)。文本型带真实正文(filesystem 类 MCP 的
|
|
113
|
+
// read_text_file 就走这条),当文本透传 —— 退化成「已跳过」等于把文件内容吞掉。
|
|
114
|
+
if (typeof r.text === "string" && r.text) {
|
|
115
|
+
blocks.push({ type: "text", text: r.text });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// resource(blob)/audio 等块在 SDK 内容联合里没有载体(只有 text|image|thinking|toolCall),
|
|
119
|
+
// 退化为文本提示,让模型至少知道工具返回了什么,而不是看到一个空串。
|
|
120
|
+
const mime = (c.mimeType ?? r.mimeType ?? "").trim();
|
|
121
|
+
const blob = typeof r.blob === "string" && r.blob ? r.blob : c.data;
|
|
122
|
+
const size = typeof blob === "string" && blob ? `,约 ${Math.max(1, Math.round((blob.length * 3) / 4))} 字节` : "";
|
|
123
|
+
blocks.push({
|
|
124
|
+
type: "text",
|
|
125
|
+
text: `[MCP 工具返回了非文本内容块(${mime || c.type || "未知类型"}${size}),当前会话无法内联,已跳过。]`,
|
|
126
|
+
});
|
|
127
|
+
hasNonText = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const text = c.text ?? "";
|
|
131
|
+
if (text)
|
|
132
|
+
blocks.push({ type: "text", text });
|
|
133
|
+
}
|
|
134
|
+
if (!hasNonText) {
|
|
135
|
+
// 纯文本结果保持旧形状(拼接字符串),不破坏既有调用方。
|
|
136
|
+
return { content: blocks.map((b) => (b.type === "text" ? b.text : "")).join("\n"), isError: !!res.isError };
|
|
137
|
+
}
|
|
138
|
+
return { content: blocks, isError: !!res.isError };
|
|
101
139
|
}
|
|
102
140
|
/** 关闭:kill 子进程,拒绝所有在途请求。 */
|
|
103
141
|
close() {
|
package/dist/server/plugins.js
CHANGED
|
@@ -402,7 +402,17 @@ export class PluginManager {
|
|
|
402
402
|
return;
|
|
403
403
|
}
|
|
404
404
|
try {
|
|
405
|
-
handler(req, res)
|
|
405
|
+
// 异步 handler(`async (req, res) => …`)的 rejection 不会被这里的 try 接住,
|
|
406
|
+
// 会变成 unhandledRejection 直接杀掉整个服务(插件读文件失败、host.fs 越界
|
|
407
|
+
// 拒绝、上游超时…都会走到这条路上)——用 Promise.resolve().catch 兜住,
|
|
408
|
+
// 与同步抛错同样转 500。
|
|
409
|
+
void Promise.resolve(handler(req, res)).catch((err) => {
|
|
410
|
+
console.error(`[plugin:${pluginId}] http ${method} ${path} failed:`, err);
|
|
411
|
+
if (!res.headersSent)
|
|
412
|
+
res.status(500).end("internal error");
|
|
413
|
+
else
|
|
414
|
+
res.end();
|
|
415
|
+
});
|
|
406
416
|
}
|
|
407
417
|
catch (err) {
|
|
408
418
|
console.error(`[plugin:${pluginId}] http ${method} ${path} failed:`, err);
|
|
@@ -646,7 +656,13 @@ export class PluginManager {
|
|
|
646
656
|
}
|
|
647
657
|
return found.map((f) => this.loaded.get(f.id)?.info ?? f);
|
|
648
658
|
}
|
|
649
|
-
/** 反激活单个插件:deactivate + 注销 AI 工具 + 清缓存。
|
|
659
|
+
/** 反激活单个插件:deactivate + 注销 AI 工具 + 清缓存。
|
|
660
|
+
*
|
|
661
|
+
* 注意这里必须把 id 从 attempted 里摘掉:目录一时不在(`pi-web-ui install --force`
|
|
662
|
+
* 先 rm 再 cp,扫描正好撞上窗口期)只是「暂时看成卸载」,目录回来后还要能重新激活;
|
|
663
|
+
* 留在 attempted 里 = 本进程内永远不再激活,插件的 HTTP 路由 / AI 工具全没了,
|
|
664
|
+
* 前端只会看到「代理请求失败 404 <url>」(插件的 /proxy 路由不存在),且 CLI 承诺的
|
|
665
|
+
* 「刷新浏览器即可加载」失效,必须重启服务才能恢复。 */
|
|
650
666
|
deactivateEntry(id, p) {
|
|
651
667
|
try {
|
|
652
668
|
p.deactivate?.();
|
|
@@ -665,6 +681,11 @@ export class PluginManager {
|
|
|
665
681
|
}
|
|
666
682
|
this.loaded.delete(id);
|
|
667
683
|
this.messageHandlers.delete(id);
|
|
684
|
+
this.attempted.delete(id);
|
|
685
|
+
// 重新激活时会 import 磁盘上的 index.mjs:Node 的 ESM 缓存按 URL(含 ?e=)
|
|
686
|
+
// 命中,epoch 不变就会拿到旧模块(更新插件后还是旧代码)——所以这里也 +1,
|
|
687
|
+
// 顺带让浏览器端 ?e= 变化、重拉插件的 client bundle。
|
|
688
|
+
this.epochCounter += 1;
|
|
668
689
|
console.log(`[plugin:${id}] removed`);
|
|
669
690
|
}
|
|
670
691
|
/** 关机时反激活全部插件。 */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.0",
|
|
4
4
|
"description": "Web chat interface for the pi coding agent, powered by the pi SDK (@earendil-works/pi-coding-agent) — one-command run, Docker/systemd/launchd deployable",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -59,8 +59,10 @@
|
|
|
59
59
|
"build:server": "tsc -p tsconfig.server.json",
|
|
60
60
|
"build:dsh-runtime": "node scripts/copy-dsh-runtime.mjs",
|
|
61
61
|
"build:mermaid-vendor": "node scripts/build-mermaid-vendor.mjs",
|
|
62
|
+
"build:extension": "node plugins/page-picker/extension/build.mjs",
|
|
63
|
+
"pack:extension": "node plugins/page-picker/extension/pack.mjs",
|
|
62
64
|
"build:runtrace-vendor": "node scripts/build-runtrace-vendor.mjs",
|
|
63
|
-
"typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit && tsc -p desktop/tsconfig.json --noEmit",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit && tsc -p desktop/tsconfig.json --noEmit && tsc -p plugins/page-picker/extension/tsconfig.json --noEmit",
|
|
64
66
|
"test": "vitest run",
|
|
65
67
|
"test:unit": "vitest run",
|
|
66
68
|
"test:smoke": "node tests/run-smoke.mjs",
|
|
@@ -81,7 +83,7 @@
|
|
|
81
83
|
"dependencies": {
|
|
82
84
|
"@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.1-rc.2",
|
|
83
85
|
"@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
|
|
84
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
86
|
+
"@earendil-works/pi-coding-agent": "^0.85.1",
|
|
85
87
|
"@xterm/addon-fit": "^0.11.0",
|
|
86
88
|
"@xterm/xterm": "^6.0.0",
|
|
87
89
|
"compression": "^1.8.1",
|
package/plugins/catalog.json
CHANGED
|
@@ -43,5 +43,23 @@
|
|
|
43
43
|
"descriptionEn": "Run trajectory: task → thinking → tools → file changes → result timeline with replay and node details.",
|
|
44
44
|
"source": "xing-shuyin/pi-web-ui/plugins/run-trace",
|
|
45
45
|
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/run-trace"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "legado-web",
|
|
49
|
+
"name": "legado-web",
|
|
50
|
+
"icon": "📖",
|
|
51
|
+
"description": "Legado 阅读(文本源):搜索 / 发现 / 详情 / 目录 / 正文,书源 JSON 与安卓版兼容,可导入、检测、删废源;内置跨域+GBK 代理与本地存储,并给 AI 配了修源接口(读规则、读书源文件、逐步诊断链路、试规则验证)。",
|
|
52
|
+
"descriptionEn": "Legado reader (text book sources): search / explore / info / TOC / content with Android-compatible book source JSON, plus source import and health checking. Ships its own CORS+GBK proxy, local store, and AI tools to diagnose and repair book sources.",
|
|
53
|
+
"source": "xing-shuyin/pi-web-ui/plugins/legado-web",
|
|
54
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/legado-web"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"id": "image-toolkit",
|
|
58
|
+
"name": "image-toolkit",
|
|
59
|
+
"icon": "🖼",
|
|
60
|
+
"description": "图片处理工作台:压缩(可按目标体积二分逼近)、裁剪、缩放、旋转/翻转、格式转换(PNG/JPEG/WebP/AVIF)、批量导出 ZIP、水印、滤镜调色、图片信息与 EXIF,可直接读写工作区图片;另给 AI 配了 4 个工具,让 agent 自己压缩/裁剪/缩放/加水印工作区里的图。",
|
|
61
|
+
"descriptionEn": "Image workbench: compress (with target-size binary search), crop, resize, rotate/flip, format conversion (PNG/JPEG/WebP/AVIF), batch ZIP export, watermark, filters, image info + EXIF, and direct workspace read/write. Ships 4 AI tools so the agent can compress/crop/resize/watermark workspace images itself.",
|
|
62
|
+
"source": "xing-shuyin/pi-web-ui/plugins/image-toolkit",
|
|
63
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/image-toolkit"
|
|
46
64
|
}
|
|
47
65
|
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as l,j as n}from"./markdown-D3PKeHAZ.js";import{u as P,b as O,T as M,a as p,c as W,F as Y,d as z,e as Z,f as H,g as ee,h as ne,i as te,j as se,r as ae}from"./index-
|
|
1
|
+
import{a as l,j as n}from"./markdown-D3PKeHAZ.js";import{u as P,b as O,T as M,a as p,c as W,F as Y,d as z,e as Z,f as H,g as ee,h as ne,i as te,j as se,r as ae}from"./index-CiRSpf3e.js";import{D as re,o as ie}from"./xterm-B96xOxS9.js";import"./react-w24rH0km.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(h,u)=>(c=Number(u),`\r
|
|
2
2
|
`)).replace(/\r?\n?\x1b\[90m\[(?:进程已退出,退出码 |Process exited with code )-?\d+\]\x1b\[0m\r?\n?/g,`\r
|
|
3
3
|
`),exitCode:c}}function ce({conversationId:t,terminalId:c,command:s,cwd:h,title:u,active:w,running:f,exitCode:k,register:g}){const C=l.useRef(null),j=l.useRef(null),{locale:o}=P(),y=l.useRef(o);y.current=o;const E=s?JSON.stringify(s):"";l.useEffect(()=>{const m=C.current;if(!m)return;const r=new re({theme:O(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),v=new ie;r.loadAddon(v),r.open(m),j.current={term:r,fit:v},w&&r.focus();const b=()=>{r.options.theme=O()};window.addEventListener(M,b),r.attachCustomKeyEventHandler(d=>{if(d.type!=="keydown")return!0;const S=d.key?.toLowerCase();if((d.ctrlKey||d.metaKey)&&S==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&S==="c"&&r.hasSelection()){const D=r.textarea;return D&&(D.value=r.getSelection(),D.select()),!1}return!0});const _=g(t,c,{write:d=>r.write(le(d).clean),dispose:()=>r.dispose()}),$=()=>{try{v.fit(),p({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.cols,rows:r.rows})}catch{}},B=requestAnimationFrame(()=>{try{v.fit()}catch{}s?p({type:"run_command",terminalId:c,conversationId:t,command:s,cols:r.cols,rows:r.rows}):p({type:"terminal_create",terminalId:c,title:u,locale:y.current,conversationId:t,cwd:h,cols:r.cols,rows:r.rows})}),R=r.onData(d=>{p({type:"terminal_input",terminalId:c,conversationId:t,data:d})});let T=null;return typeof ResizeObserver<"u"&&(T=new ResizeObserver(()=>{m.offsetWidth>0&&m.offsetHeight>0&&$()}),T.observe(m)),()=>{cancelAnimationFrame(B),R.dispose(),window.removeEventListener(M,b),T?.disconnect(),_(),r.dispose(),j.current=null}},[t,c,E,g]),l.useEffect(()=>{if(!w)return;const m=requestAnimationFrame(()=>{const r=j.current;if(r){try{r.fit.fit(),p({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.term.cols,rows:r.term.rows})}catch{}r.term.focus()}});return()=>cancelAnimationFrame(m)},[w]);const{t:F}=P(),x=l.useRef(void 0);return l.useEffect(()=>{if(f===x.current||(x.current=f,f!==!1))return;const m=j.current;m&&m.term.write(`\r
|
|
4
4
|
\x1B[90m${F("exitBanner",{code:k??""})}\x1B[0m\r
|