pi-web-ui 0.64.7 → 0.65.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/README.md +8 -0
- package/dist/server/index.js +40 -3
- package/dist/server/plugin-catalog.js +159 -0
- package/dist/server/plugins.js +57 -1
- package/package.json +111 -108
- package/plugins/catalog.json +38 -0
- package/web/dist/assets/{TerminalPanel-DL8sTcRC.js → TerminalPanel-B_ACe6Lp.js} +1 -1
- package/web/dist/assets/index-Bq4P6EhG.css +10 -0
- package/web/dist/assets/index-CaJbqnT7.js +326 -0
- package/web/dist/assets/{markdown-BJJLXG-8.js → markdown-DRBrS2Nf.js} +1 -1
- package/web/dist/assets/{react-ezqUM1Or.js → react-C9ovnpIm.js} +1 -1
- package/web/dist/index.html +4 -4
- package/web/dist/assets/abnfDiagram-VCTEODGH-BF9usV5C.js +0 -1
- package/web/dist/assets/arc-BHXahvV5.js +0 -1
- package/web/dist/assets/architectureDiagram-5GKGNRK7-V2QNH_BW.js +0 -36
- package/web/dist/assets/blockDiagram-I7D4REHJ-DNWe8D5y.js +0 -129
- package/web/dist/assets/c4Diagram-7LVT6UL2-Cd7YCdDn.js +0 -38
- package/web/dist/assets/channel-DDCgqHU5.js +0 -1
- package/web/dist/assets/chunk-2Q5K7J3B-Dn4M1g1T.js +0 -1
- package/web/dist/assets/chunk-5VM5RSS4-U5uyVkTI.js +0 -15
- package/web/dist/assets/chunk-F27PBJKO-lMqDdknq.js +0 -1
- package/web/dist/assets/chunk-IMKFNOWR-DlzpFcKz.js +0 -231
- package/web/dist/assets/chunk-JWPE2WC7-BdI7zobW.js +0 -1
- package/web/dist/assets/chunk-POPQ4Y6H-Dk7oA6BE.js +0 -1
- package/web/dist/assets/chunk-SVP7TREG-BhXOKkNm.js +0 -88
- package/web/dist/assets/chunk-TICWLB2K-DFW_5Q5s.js +0 -206
- package/web/dist/assets/chunk-XXDRQBXY-d-B9YyVA.js +0 -1
- package/web/dist/assets/classDiagram-ZZMXUADV-DnlRT21K.js +0 -1
- package/web/dist/assets/classDiagram-v2-VYDZK3BY-DnlRT21K.js +0 -1
- package/web/dist/assets/cose-bilkent-JH36ORCC-W3IYYiNN.js +0 -1
- package/web/dist/assets/cynefin-OW5HDTMX-DVcVxvWj.js +0 -178
- package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DdUHHDle.js +0 -62
- package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +0 -331
- package/web/dist/assets/dagre-GXQ25YYZ-DMqzPbm8.js +0 -4
- package/web/dist/assets/defaultLocale-DX6XiGOO.js +0 -1
- package/web/dist/assets/diagram-S7CK7UJ4-g-T7ZpRp.js +0 -30
- package/web/dist/assets/diagram-UQ7AKVKN-Cj1HB5mV.js +0 -41
- package/web/dist/assets/diagram-VSXAHHWV-D4Xgm5yq.js +0 -3
- package/web/dist/assets/diagram-VX7I27RA-CPaDZ0sg.js +0 -24
- package/web/dist/assets/diagram-Z3DM3KII-BwroUmR-.js +0 -24
- package/web/dist/assets/ebnfDiagram-PWID7BFC-CymjTfGP.js +0 -1
- package/web/dist/assets/erDiagram-RLTQ6QDP-CkCv1f0Q.js +0 -99
- package/web/dist/assets/flowDiagram-HODETNUW-IrWWLHqn.js +0 -168
- package/web/dist/assets/ganttDiagram-EL5Y4UJY-DhHylBej.js +0 -292
- package/web/dist/assets/gitGraphDiagram-WWUBYQGX-C1O_4b0m.js +0 -106
- package/web/dist/assets/index-B-D8dBbi.js +0 -326
- package/web/dist/assets/index-DWUw-5kk.css +0 -10
- package/web/dist/assets/infoDiagram-27XIBGKW-B3SzbIHm.js +0 -2
- package/web/dist/assets/init-Gi6I4Gst.js +0 -1
- package/web/dist/assets/ishikawaDiagram-5VMMS53U-CM5U90LI.js +0 -70
- package/web/dist/assets/journeyDiagram-3NMN7TZE-BIWbaonv.js +0 -139
- package/web/dist/assets/kanban-definition-UXKFOSKX-Cg0FRvzj.js +0 -89
- package/web/dist/assets/katex-C5jXJg4s.js +0 -257
- package/web/dist/assets/layout-BMZQTQDF.js +0 -1
- package/web/dist/assets/linear--htAmwBy.js +0 -1
- package/web/dist/assets/mermaid.core-Bxpg8jiH.js +0 -312
- package/web/dist/assets/mindmap-definition-YA3MSWOX-CNp9C6Gy.js +0 -96
- package/web/dist/assets/ordinal-Cboi1Yqb.js +0 -1
- package/web/dist/assets/pegDiagram-XKGWAZYB-B4mLGlJp.js +0 -1
- package/web/dist/assets/pieDiagram-E7YTZNPT-hP4zuAzM.js +0 -39
- package/web/dist/assets/quadrantDiagram-AXDQQJYC-DVN-GEzm.js +0 -7
- package/web/dist/assets/railroadDiagram-O6MQD6OU-BjBVdCWk.js +0 -1
- package/web/dist/assets/requirementDiagram-BXWQKSXE-BOwNWcER.js +0 -84
- package/web/dist/assets/sankeyDiagram-P5KCCOFB-CkK6Y6CK.js +0 -40
- package/web/dist/assets/sequenceDiagram-WJ2MYXX4-D49cIkIS.js +0 -162
- package/web/dist/assets/sizeCapture-INFHLROL-D9R6YbVj.js +0 -1
- package/web/dist/assets/stateDiagram-D77RDMKH-DvwH4KPJ.js +0 -1
- package/web/dist/assets/stateDiagram-v2-MP3YSRHH-CHVwI1ZU.js +0 -1
- package/web/dist/assets/swimlanes-42K2YHIH-DgpCt172.js +0 -1
- package/web/dist/assets/swimlanesDiagram-VR7AAH4N-L7ZmzB2n.js +0 -8
- package/web/dist/assets/timeline-definition-24CTP7MA-Cy-6c2rf.js +0 -120
- package/web/dist/assets/vennDiagram-4TSXK5OY-D05mFAgh.js +0 -34
- package/web/dist/assets/wardleyDiagram-VM6X3IG4--c9A1xky.js +0 -78
- package/web/dist/assets/xychartDiagram-S5SC5T6Z-F6B_ZHcI.js +0 -7
package/README.md
CHANGED
|
@@ -280,6 +280,14 @@ Example — install the webmail plugin:
|
|
|
280
280
|
pi-web-ui install https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/webmail
|
|
281
281
|
```
|
|
282
282
|
|
|
283
|
+
You can also install from inside the UI: **设置 → 界面插件 → 插件市场** lists
|
|
284
|
+
maintainable plugins (the same set, shipped in `plugins/catalog.json`) with
|
|
285
|
+
full **安装 / 更新 / 卸载** controls (updates keep `config.json`), and lets you
|
|
286
|
+
drop any third-party plugin into the list via **添加插件** (paste `owner/repo`
|
|
287
|
+
or an `owner/repo/subdir` source) — your additions are stored in
|
|
288
|
+
`<dataDir>/plugin-catalog.json`. A plugin author can contribute to the built-in
|
|
289
|
+
list with a one-line PR to `plugins/catalog.json`.
|
|
290
|
+
|
|
283
291
|
Each plugin's directory in the repo has its own `README.md` with full feature
|
|
284
292
|
lists, configuration and per-plugin caveats.
|
|
285
293
|
|
package/dist/server/index.js
CHANGED
|
@@ -437,7 +437,7 @@ const service = ENGINE === "dsh"
|
|
|
437
437
|
join(DATA_DIR, "client-state.json"));
|
|
438
438
|
// Optional UI plugins (<dataDir>/plugins/<id>/): scanned on every client
|
|
439
439
|
// attach so freshly dropped plugins appear without a server restart.
|
|
440
|
-
const pluginMgr = new PluginManager(DATA_DIR, CWD);
|
|
440
|
+
const pluginMgr = new PluginManager(DATA_DIR, CWD, join(pkgRoot, "plugins", "catalog.json"));
|
|
441
441
|
// MCP 工具桥:读取 <dataDir>/mcp.json 启动外部 MCP 服务器(stdio),把它们的
|
|
442
442
|
// 工具并入与插件工具相同的 customTools 管线;单服务器失败不炸进程。
|
|
443
443
|
const mcpBridge = new McpBridge(DATA_DIR, (...a) => console.log("[mcp]", ...a));
|
|
@@ -849,6 +849,26 @@ wss.on("connection", (ws) => {
|
|
|
849
849
|
case "plugins_reload":
|
|
850
850
|
void pluginMgr.reload().then(() => pluginMgr.pushToAll());
|
|
851
851
|
break;
|
|
852
|
+
case "plugin_catalog_add": {
|
|
853
|
+
const r = pluginMgr.addCatalogEntry(msg.entry ?? {});
|
|
854
|
+
if (r.error) {
|
|
855
|
+
cs?.emitNotice("error", `添加到插件列表失败:${r.error}`, `Failed to add to plugin list: ${r.error}`);
|
|
856
|
+
}
|
|
857
|
+
else {
|
|
858
|
+
cs?.emitNotice("info", "已添加到插件列表", "Added to the plugin list");
|
|
859
|
+
}
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
case "plugin_catalog_remove": {
|
|
863
|
+
const r = pluginMgr.removeCatalogEntry(msg.id);
|
|
864
|
+
if (r.error) {
|
|
865
|
+
cs?.emitNotice("error", `从插件列表移除失败:${r.error}`, `Failed to remove from plugin list: ${r.error}`);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
cs?.emitNotice("info", "已从插件列表移除", "Removed from the plugin list");
|
|
869
|
+
}
|
|
870
|
+
break;
|
|
871
|
+
}
|
|
852
872
|
case "dsh_patches_list":
|
|
853
873
|
void cs.listDshPatches?.();
|
|
854
874
|
break;
|
|
@@ -900,21 +920,38 @@ wss.on("connection", (ws) => {
|
|
|
900
920
|
protocolVersion: PROTOCOL_VERSION,
|
|
901
921
|
engine: ENGINE,
|
|
902
922
|
});
|
|
903
|
-
cs.flushSnapshot();
|
|
904
923
|
// Plugin catalog: re-scan + activate new dirs on every attach so
|
|
905
924
|
// freshly dropped plugins show up without a server restart.
|
|
906
925
|
pluginMgr
|
|
907
926
|
.ensureLoaded()
|
|
908
927
|
.then((plugins) => {
|
|
928
|
+
if (closed)
|
|
929
|
+
return;
|
|
909
930
|
send({ type: "plugins", plugins, epoch: pluginMgr.epoch });
|
|
931
|
+
// 插件市场列表(可一键安装的清单)随附推一次。
|
|
932
|
+
send({
|
|
933
|
+
type: "plugin_catalog",
|
|
934
|
+
entries: pluginMgr.catalog(),
|
|
935
|
+
epoch: pluginMgr.catalogEpochValue,
|
|
936
|
+
});
|
|
910
937
|
// 让各插件向新接入的客户端推送自身初始状态(onAttach 钩子)——
|
|
911
938
|
// 插件不要依赖客户端挂载后自己拉(见 plugins.ts onAttach 注释)。
|
|
912
939
|
pluginMgr.notifyAttach(cid);
|
|
913
940
|
// 插件命令可能在本客户端 attach 过程中才注册(首载竞态)——
|
|
914
941
|
// 重推一次目录,保证选择器完整。
|
|
915
942
|
service.applyPluginCommandCatalog();
|
|
943
|
+
// 插件清单【先于】快照推送:前端渲染历史消息前就拿到 renderer
|
|
944
|
+
// 注册表(plugin-fence.ts),`` ```lang `` 围栏才能立即命中插件;
|
|
945
|
+
// 否则消息先落成普通代码块,清单后到也不会重渲。
|
|
946
|
+
cs.flushSnapshot();
|
|
916
947
|
})
|
|
917
|
-
.catch(() => {
|
|
948
|
+
.catch(() => {
|
|
949
|
+
if (closed)
|
|
950
|
+
return;
|
|
951
|
+
// ensureLoaded 失败(如磁盘读错)不能卡死快照——前端 30s 无消息
|
|
952
|
+
// 会重连,重连又失败会陷入循环。至少把状态推下去。
|
|
953
|
+
cs.flushSnapshot();
|
|
954
|
+
});
|
|
918
955
|
// Replay anything that arrived while the session was starting.
|
|
919
956
|
const queued = pending;
|
|
920
957
|
pending = [];
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 插件市场 / 插件列表 —— 可一键安装的插件清单。
|
|
3
|
+
*
|
|
4
|
+
* 两层来源(合并后经协议 plugin_catalog 推给前端):
|
|
5
|
+
* - builtin : <pkgRoot>/plugins/catalog.json(随包发布,官方维护列表;
|
|
6
|
+
* 插件作者把新插件加进来 = 往这个文件加一条 + PR)
|
|
7
|
+
* - custom : <dataDir>/plugin-catalog.json(用户在设置面板「添加到列表」,
|
|
8
|
+
* 任何第三方插件都能随时填进列表;仅 custom 条目可经 UI 移除)
|
|
9
|
+
*
|
|
10
|
+
* 条目结构见 protocol.UiPluginCatalogEntry。`id` 是安装落盘目录名
|
|
11
|
+
* (<dataDir>/plugins/<id>);安装固定用 `pi-web-ui install <source>
|
|
12
|
+
* --name <id>` 保证目录名 == 列表 id,前端据此判断已装/未装。
|
|
13
|
+
*
|
|
14
|
+
* 设计上服务端不做网络探测(不拉 manifest):名称/简介/图标由条目本身携带
|
|
15
|
+
* (作者填),id 由来源推导或作者显式指定 —— 保持离线可用、可单测。
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
/** 合法插件 id(与 server/plugins.ts 的 ID_RE 一致,防路径穿越)。 */
|
|
20
|
+
const ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
21
|
+
/** source 校验:owner/repo 或 owner/repo/子路径[#ref]。宽松校验,实际解析
|
|
22
|
+
* 交给 CLI(pi-web-ui install)。只接受远程 GitHub 源,拒绝本地路径。 */
|
|
23
|
+
function isValidSource(source) {
|
|
24
|
+
if (!source || source.length > 300)
|
|
25
|
+
return false;
|
|
26
|
+
if (source.startsWith("/") || /^[A-Za-z]:[\\/]/.test(source))
|
|
27
|
+
return false;
|
|
28
|
+
if (/^https?:\/\//.test(source))
|
|
29
|
+
return true; // 完整 URL(install 也支持)
|
|
30
|
+
const spec = source.split("#")[0].replace(/\/+$/, "");
|
|
31
|
+
const segs = spec.split("/").filter(Boolean);
|
|
32
|
+
if (segs.length < 2)
|
|
33
|
+
return false;
|
|
34
|
+
for (const s of segs)
|
|
35
|
+
if (s === "." || s === "..")
|
|
36
|
+
return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
/** 推导默认 id:与 CLI(bin/pi-web-ui.mjs)的规则对齐 —— 子路径末段 > 仓库名
|
|
40
|
+
* > 来源末段;非法字符替换为 -,两端去 -;空则 "plugin"。显式 raw(含合法
|
|
41
|
+
* id 校验)优先。返回的 id 不保证通过 ID_RE(Cyrillic 等),调用方再校验。 */
|
|
42
|
+
export function deriveCatalogId(raw, source) {
|
|
43
|
+
if (raw && ID_RE.test(raw))
|
|
44
|
+
return raw;
|
|
45
|
+
const spec = source.split("#")[0].replace(/\/+$/, "");
|
|
46
|
+
const segs = spec.split("/").filter(Boolean);
|
|
47
|
+
const last = segs.length >= 2 ? segs[segs.length - 1] : (segs[0] ?? "plugin");
|
|
48
|
+
const cleaned = last.replace(/[^A-Za-z0-9_-]/g, "-").replace(/^-+|-+$/g, "");
|
|
49
|
+
return cleaned || "plugin";
|
|
50
|
+
}
|
|
51
|
+
function readJsonSafe(path, fallback) {
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return fallback;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function atomicWrite(path, data) {
|
|
60
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
61
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
62
|
+
writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n");
|
|
63
|
+
renameSync(tmp, path);
|
|
64
|
+
}
|
|
65
|
+
/** 把任意对象规范化为合法条目;非法则返回 null。 */
|
|
66
|
+
function toEntry(raw, builtin) {
|
|
67
|
+
const source = typeof raw.source === "string" ? raw.source.trim() : "";
|
|
68
|
+
if (!isValidSource(source))
|
|
69
|
+
return null;
|
|
70
|
+
const id = deriveCatalogId(typeof raw.id === "string" ? raw.id.trim() : undefined, source);
|
|
71
|
+
if (!ID_RE.test(id))
|
|
72
|
+
return null;
|
|
73
|
+
const name = typeof raw.name === "string" && raw.name.trim() ? raw.name.trim() : id;
|
|
74
|
+
const description = typeof raw.description === "string" && raw.description.trim() ? raw.description.trim() : undefined;
|
|
75
|
+
const descriptionEn = typeof raw.descriptionEn === "string" && raw.descriptionEn.trim() ? raw.descriptionEn.trim() : undefined;
|
|
76
|
+
const icon = typeof raw.icon === "string" && raw.icon.trim() ? raw.icon.trim() : undefined;
|
|
77
|
+
const homepage = typeof raw.homepage === "string" && raw.homepage.trim() ? raw.homepage.trim() : undefined;
|
|
78
|
+
return {
|
|
79
|
+
id,
|
|
80
|
+
name,
|
|
81
|
+
source,
|
|
82
|
+
builtin,
|
|
83
|
+
...(description ? { description } : {}),
|
|
84
|
+
...(descriptionEn ? { descriptionEn } : {}),
|
|
85
|
+
...(icon ? { icon } : {}),
|
|
86
|
+
...(homepage ? { homepage } : {}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** 读合并后的目录:builtin 在前,custom 在后(同 id 时 custom 覆盖 builtin)。 */
|
|
90
|
+
export function readCatalog(builtinPath, customPath) {
|
|
91
|
+
const out = [];
|
|
92
|
+
const seen = new Set();
|
|
93
|
+
if (existsSync(builtinPath)) {
|
|
94
|
+
const raw = readJsonSafe(builtinPath, []);
|
|
95
|
+
if (Array.isArray(raw)) {
|
|
96
|
+
for (const it of raw) {
|
|
97
|
+
const e = it && typeof it === "object" ? toEntry(it, true) : null;
|
|
98
|
+
if (e && !seen.has(e.id)) {
|
|
99
|
+
seen.add(e.id);
|
|
100
|
+
out.push(e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const rawCustom = readJsonSafe(customPath, {});
|
|
106
|
+
const list = rawCustom && typeof rawCustom === "object" && Array.isArray(rawCustom.entries)
|
|
107
|
+
? rawCustom.entries
|
|
108
|
+
: [];
|
|
109
|
+
for (const it of list) {
|
|
110
|
+
const e = it && typeof it === "object" ? toEntry(it, false) : null;
|
|
111
|
+
if (!e)
|
|
112
|
+
continue;
|
|
113
|
+
const idx = out.findIndex((x) => x.id === e.id);
|
|
114
|
+
if (idx >= 0)
|
|
115
|
+
out[idx] = e;
|
|
116
|
+
else
|
|
117
|
+
out.push(e);
|
|
118
|
+
seen.add(e.id);
|
|
119
|
+
}
|
|
120
|
+
return out;
|
|
121
|
+
}
|
|
122
|
+
/** 把用户填的条目追加进 custom 文件(同 id 覆盖旧条目);返回规范化后的条目。
|
|
123
|
+
* 非法来源/条目抛 Error。 */
|
|
124
|
+
export function addCustomEntry(customPath, input) {
|
|
125
|
+
const source = String(input?.source ?? "").trim();
|
|
126
|
+
if (!isValidSource(source)) {
|
|
127
|
+
throw new Error("来源需为 owner/repo 或 owner/repo/子目录(不支持本地路径)");
|
|
128
|
+
}
|
|
129
|
+
const id = deriveCatalogId(typeof input?.id === "string" ? input.id.trim() : undefined, source);
|
|
130
|
+
if (!ID_RE.test(id))
|
|
131
|
+
throw new Error(`非法 id "${id}"(仅限字母数字-_)`);
|
|
132
|
+
const raw = readJsonSafe(customPath, {});
|
|
133
|
+
const entries = Array.isArray(raw.entries) ? raw.entries : [];
|
|
134
|
+
const next = entries.filter((x) => !(x && typeof x === "object" && x.id === id));
|
|
135
|
+
next.push({
|
|
136
|
+
id,
|
|
137
|
+
source,
|
|
138
|
+
...(typeof input?.name === "string" && input.name.trim() ? { name: input.name.trim() } : {}),
|
|
139
|
+
...(typeof input?.description === "string" && input.description.trim()
|
|
140
|
+
? { description: input.description.trim() }
|
|
141
|
+
: {}),
|
|
142
|
+
...(typeof input?.icon === "string" && input.icon.trim() ? { icon: input.icon.trim() } : {}),
|
|
143
|
+
});
|
|
144
|
+
atomicWrite(customPath, { entries: next });
|
|
145
|
+
return toEntry(next[next.length - 1], false);
|
|
146
|
+
}
|
|
147
|
+
/** 移除用户添加的条目(builtin 由文件自身控制,不可经此删除);返回是否删掉。 */
|
|
148
|
+
export function removeCustomEntry(customPath, id) {
|
|
149
|
+
if (!ID_RE.test(id))
|
|
150
|
+
return false;
|
|
151
|
+
const raw = readJsonSafe(customPath, {});
|
|
152
|
+
if (!Array.isArray(raw.entries))
|
|
153
|
+
return false;
|
|
154
|
+
const next = raw.entries.filter((x) => !(x && typeof x === "object" && x.id === id));
|
|
155
|
+
if (next.length === raw.entries.length)
|
|
156
|
+
return false;
|
|
157
|
+
atomicWrite(customPath, { entries: next });
|
|
158
|
+
return true;
|
|
159
|
+
}
|
package/dist/server/plugins.js
CHANGED
|
@@ -21,6 +21,7 @@ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
|
21
21
|
import { join, resolve, sep } from "node:path";
|
|
22
22
|
import { pathToFileURL } from "node:url";
|
|
23
23
|
import { PluginStorage, PluginSecrets, ensurePluginDeps, WorkspaceFS } from "./plugin-facilities.js";
|
|
24
|
+
import { readCatalog, addCustomEntry, removeCustomEntry } from "./plugin-catalog.js";
|
|
24
25
|
import { createHash } from "node:crypto";
|
|
25
26
|
/** 合法插件 id:字母/数字/下划线/连字符,防路径穿越(同 themes.ts 的做法)。 */
|
|
26
27
|
const ID_RE = /^[A-Za-z0-9_-]+$/;
|
|
@@ -127,6 +128,7 @@ function saveSettingsValues(dir, schema, values) {
|
|
|
127
128
|
}
|
|
128
129
|
export class PluginManager {
|
|
129
130
|
dataDir;
|
|
131
|
+
builtinCatalogPath;
|
|
130
132
|
loaded = new Map();
|
|
131
133
|
/** 已 import 过但无入口/失败的目录——避免重复 import 与重复报错。 */
|
|
132
134
|
attempted = new Set();
|
|
@@ -146,10 +148,15 @@ export class PluginManager {
|
|
|
146
148
|
onBgTasksChanged = undefined;
|
|
147
149
|
/** 服务端重载纪元:每次 reload() +1,前端用作 import 缓存击穿参数。 */
|
|
148
150
|
epochCounter = 0;
|
|
151
|
+
/** 插件市场列表纪元:每次 add/remove +1,前端据此重渲。 */
|
|
152
|
+
catalogEpoch = 0;
|
|
149
153
|
/** 当前全局工作区(host.cwd 的背后存储)——随 notifyCwd 更新。 */
|
|
150
154
|
cwdValue;
|
|
151
|
-
constructor(dataDir, cwd
|
|
155
|
+
constructor(dataDir, cwd,
|
|
156
|
+
/** 随包发布的默认插件列表(<pkgRoot>/plugins/catalog.json)。缺省 = 无内置列表。 */
|
|
157
|
+
builtinCatalogPath) {
|
|
152
158
|
this.dataDir = dataDir;
|
|
159
|
+
this.builtinCatalogPath = builtinCatalogPath;
|
|
153
160
|
this.cwdValue = resolve(cwd);
|
|
154
161
|
}
|
|
155
162
|
/** index.ts 在客户端 set_cwd 成功后调用:更新全局工作区并扇出给
|
|
@@ -259,6 +266,49 @@ export class PluginManager {
|
|
|
259
266
|
get epoch() {
|
|
260
267
|
return this.epochCounter;
|
|
261
268
|
}
|
|
269
|
+
/** 用户自定义插件列表文件(<dataDir>/plugin-catalog.json)。 */
|
|
270
|
+
get customCatalogPath() {
|
|
271
|
+
return join(this.dataDir, "plugin-catalog.json");
|
|
272
|
+
}
|
|
273
|
+
/** 合并后的插件市场列表(builtin + 用户自定义,同 id 用户覆盖)。 */
|
|
274
|
+
catalog() {
|
|
275
|
+
return this.builtinCatalogPath ? readCatalog(this.builtinCatalogPath, this.customCatalogPath) : [];
|
|
276
|
+
}
|
|
277
|
+
/** 插件市场列表纪元(随 plugin_catalog 消息下发)。 */
|
|
278
|
+
get catalogEpochValue() {
|
|
279
|
+
return this.catalogEpoch;
|
|
280
|
+
}
|
|
281
|
+
/** 把插件市场列表推给所有 socket。 */
|
|
282
|
+
async pushCatalog() {
|
|
283
|
+
this.deliverAll({ type: "plugin_catalog", entries: this.catalog(), epoch: this.catalogEpoch });
|
|
284
|
+
}
|
|
285
|
+
/** 往用户自定义列表加一条(同 id 覆盖);返回错误信息或 null(成功)。
|
|
286
|
+
* 成功后 epoch+1 并重推列表。 */
|
|
287
|
+
addCatalogEntry(input) {
|
|
288
|
+
try {
|
|
289
|
+
addCustomEntry(this.customCatalogPath, input);
|
|
290
|
+
this.catalogEpoch += 1;
|
|
291
|
+
void this.pushCatalog();
|
|
292
|
+
return {};
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
return { error: err.message };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** 移除一条用户自定义条目(builtin 不可经此删除);返回错误信息或 null。 */
|
|
299
|
+
removeCatalogEntry(id) {
|
|
300
|
+
try {
|
|
301
|
+
const ok = removeCustomEntry(this.customCatalogPath, id);
|
|
302
|
+
if (!ok)
|
|
303
|
+
return { error: "未找到该条目,或它是内置条目(不可移除)" };
|
|
304
|
+
this.catalogEpoch += 1;
|
|
305
|
+
void this.pushCatalog();
|
|
306
|
+
return {};
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
return { error: err.message };
|
|
310
|
+
}
|
|
311
|
+
}
|
|
262
312
|
addSender(send, cid) {
|
|
263
313
|
const s = { cid, send };
|
|
264
314
|
this.senders.add(s);
|
|
@@ -614,6 +664,12 @@ export class PluginManager {
|
|
|
614
664
|
// 声明式设置 schema + 当前存值(⚙ 面板自动渲染表单用)
|
|
615
665
|
settingsSchema: parseSettingsSchema(m.settings),
|
|
616
666
|
settingsValues: storedSettingsValues(dir, parseSettingsSchema(m.settings)),
|
|
667
|
+
// 可渲染的 fenced-code 语言(manifest "renderers")——前端据此按需加载
|
|
668
|
+
renderers: Array.isArray(m.renderers)
|
|
669
|
+
? m.renderers.filter((r) => typeof r === "string" && r.length > 0).slice(0, 32)
|
|
670
|
+
: undefined,
|
|
671
|
+
// 是否有独立视图 tab(manifest "view",缺省 true);纯 renderer 插件写 false
|
|
672
|
+
view: typeof m.view === "boolean" ? m.view : true,
|
|
617
673
|
// 安装来源(pi-web-ui install 写入的 .pi-source.json)——
|
|
618
674
|
// 设置面板据此显示「更新」按钮;手工拷入的插件没有此文件。
|
|
619
675
|
source: await readFile(join(dir, ".pi-source.json"), "utf8")
|
package/package.json
CHANGED
|
@@ -1,110 +1,113 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
2
|
+
"name": "pi-web-ui",
|
|
3
|
+
"version": "0.65.0",
|
|
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
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "xingshuyin",
|
|
8
|
+
"email": "xingshuyin@users.noreply.github.com"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"pi-web-ui": "bin/pi-web-ui.mjs"
|
|
13
|
+
},
|
|
14
|
+
"main": "bin/pi-web-ui.mjs",
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"dist/",
|
|
18
|
+
"web/dist/",
|
|
19
|
+
"web/public/",
|
|
20
|
+
"themes/",
|
|
21
|
+
"deploy/",
|
|
22
|
+
"extensions/",
|
|
23
|
+
"plugins/catalog.json",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"pi",
|
|
29
|
+
"pi-package",
|
|
30
|
+
"coding-agent",
|
|
31
|
+
"ai",
|
|
32
|
+
"chat",
|
|
33
|
+
"web-ui",
|
|
34
|
+
"terminal",
|
|
35
|
+
"llm"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/xing-shuyin/pi-web-ui.git"
|
|
40
|
+
},
|
|
41
|
+
"pi": {
|
|
42
|
+
"extensions": [
|
|
43
|
+
"./extensions"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=22.19.0"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"prepublishOnly": "npm run build",
|
|
51
|
+
"dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
|
|
52
|
+
"dev:server": "cross-env PI_WEB_PORT=8788 PI_WEB_ALLOW_ORIGINS=http://localhost:5173,http://127.0.0.1:5173 node --watch --import tsx server/index.ts",
|
|
53
|
+
"dev:web": "vite --config web/vite.config.ts",
|
|
54
|
+
"build": "npm run build:web && npm run build:server && npm run build:dsh-runtime && npm run build:mermaid-vendor",
|
|
55
|
+
"build:web": "vite build --config web/vite.config.ts",
|
|
56
|
+
"build:server": "tsc -p tsconfig.server.json",
|
|
57
|
+
"build:dsh-runtime": "node scripts/copy-dsh-runtime.mjs",
|
|
58
|
+
"build:mermaid-vendor": "node scripts/build-mermaid-vendor.mjs",
|
|
59
|
+
"typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:unit": "vitest run",
|
|
62
|
+
"test:smoke": "node tests/run-smoke.mjs",
|
|
63
|
+
"check:protocol": "node scripts/check-protocol-sync.mjs",
|
|
64
|
+
"test:freeze": "node tests/freeze-test.mjs",
|
|
65
|
+
"start": "node dist/server/index.js",
|
|
66
|
+
"format": "prettier --write server/ web/src/ extensions/ scripts/ bin/ tests/*.mjs tests/unit/ *.mjs",
|
|
67
|
+
"format:check": "prettier --check server/ web/src/ extensions/ scripts/ bin/ tests/*.mjs tests/unit/ *.mjs",
|
|
68
|
+
"lint": "oxlint server/ web/src/ extensions/ scripts/",
|
|
69
|
+
"lint:fix": "oxlint --fix server/ web/src/ extensions/ scripts/"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.1-rc.2",
|
|
73
|
+
"@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
|
|
74
|
+
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
75
|
+
"@xterm/addon-fit": "^0.11.0",
|
|
76
|
+
"@xterm/xterm": "^6.0.0",
|
|
77
|
+
"compression": "^1.8.1",
|
|
78
|
+
"express": "^4.21.2",
|
|
79
|
+
"highlight.js": "^11.10.0",
|
|
80
|
+
"node-pty": "^1.1.0",
|
|
81
|
+
"react": "^18.3.1",
|
|
82
|
+
"react-dom": "^18.3.1",
|
|
83
|
+
"react-icons": "^5.7.0",
|
|
84
|
+
"react-markdown": "^9.0.1",
|
|
85
|
+
"rehype-highlight": "^7.0.1",
|
|
86
|
+
"remark-gfm": "^4.0.0",
|
|
87
|
+
"typebox": "^1.3.14",
|
|
88
|
+
"ws": "^8.18.0"
|
|
89
|
+
},
|
|
90
|
+
"devDependencies": {
|
|
91
|
+
"@types/compression": "^1.8.1",
|
|
92
|
+
"@types/express": "^4.17.21",
|
|
93
|
+
"@types/node": "^22.10.0",
|
|
94
|
+
"@types/react": "^18.3.12",
|
|
95
|
+
"@types/react-dom": "^18.3.1",
|
|
96
|
+
"@types/ws": "^8.5.13",
|
|
97
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
98
|
+
"concurrently": "^9.1.0",
|
|
99
|
+
"cross-env": "^10.1.0",
|
|
100
|
+
"esbuild": "^0.25.12",
|
|
101
|
+
"mermaid": "^11.17.2",
|
|
102
|
+
"oxlint": "^1.81.0",
|
|
103
|
+
"playwright-core": "^1.62.1",
|
|
104
|
+
"prettier": "^3.9.6",
|
|
105
|
+
"tsx": "^4.19.2",
|
|
106
|
+
"typescript": "^5.7.2",
|
|
107
|
+
"vite": "^6.0.3",
|
|
108
|
+
"vitest": "^4.1.11"
|
|
109
|
+
},
|
|
110
|
+
"allowScripts": {
|
|
111
|
+
"node-pty@1.1.0": true
|
|
112
|
+
}
|
|
110
113
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "webmail",
|
|
4
|
+
"name": "webmail",
|
|
5
|
+
"icon": "📬",
|
|
6
|
+
"description": "网页邮箱:IMAP 收件 + SMTP 发信 + 新邮件通知,可开放给 AI 直接管理邮件。",
|
|
7
|
+
"descriptionEn": "Web mail: IMAP inbox + SMTP send + new-mail notifications, optionally let the AI manage mail.",
|
|
8
|
+
"source": "xing-shuyin/pi-web-ui/plugins/webmail",
|
|
9
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/webmail"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"id": "db-client",
|
|
13
|
+
"name": "db-client",
|
|
14
|
+
"icon": "🗄️",
|
|
15
|
+
"description": "数据库连接管理:MySQL / PostgreSQL / SQLite / SQL Server / MongoDB / Redis 的库表浏览、SQL 查询与数据编辑。",
|
|
16
|
+
"descriptionEn": "Database client: browse schemas, run SQL and edit data across MySQL / PostgreSQL / SQLite / SQL Server / MongoDB / Redis.",
|
|
17
|
+
"source": "xing-shuyin/pi-web-ui/plugins/db-client",
|
|
18
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/db-client"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"id": "vscode-editor",
|
|
22
|
+
"name": "vscode-editor",
|
|
23
|
+
"icon": "📝",
|
|
24
|
+
"description": "类 VSCode 工作台:多根文件树 + CodeMirror 编辑器 + 底部终端 + Remote-SSH + SFTP 同步。",
|
|
25
|
+
"descriptionEn": "VSCode-like workbench: multi-root file tree, CodeMirror editor, integrated terminal, Remote-SSH and SFTP sync.",
|
|
26
|
+
"source": "xing-shuyin/pi-web-ui/plugins/vscode-editor",
|
|
27
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/vscode-editor"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "mermaid",
|
|
31
|
+
"name": "mermaid",
|
|
32
|
+
"icon": "📊",
|
|
33
|
+
"description": "把对话里的 ```mermaid 围栏渲染成 SVG 图表(fenced-code 渲染插件,本地引擎离线优先)。",
|
|
34
|
+
"descriptionEn": "Renders ```mermaid fences in messages to SVG diagrams (fenced-code renderer plugin, offline-first local engine).",
|
|
35
|
+
"source": "xing-shuyin/pi-web-ui/plugins/mermaid",
|
|
36
|
+
"homepage": "https://github.com/xing-shuyin/pi-web-ui/tree/main/plugins/mermaid"
|
|
37
|
+
}
|
|
38
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{a as l,j as n}from"./markdown-
|
|
1
|
+
import{a as l,j as n}from"./markdown-DRBrS2Nf.js";import{u as O,b as M,T as z,a as W,F as Y,c as H,d as Z,e as q,f as ee,g as ne,h as te,i as se,r as ae}from"./index-CaJbqnT7.js";import{D as re,o as ie}from"./xterm-D1D2FVe3.js";import"./react-C9ovnpIm.js";function le(t){let c=null;return{clean:t.replace(/\r?\n?\[pi-term-exit:(-?\d+)\]\r?\n?/g,(a,h)=>(c=Number(h),`\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:f,cwd:a,title:h,active:u,running:b,exitCode:y,send:j,register:T}){const k=l.useRef(null),N=l.useRef(null),{locale:o}=O(),w=l.useRef(o);w.current=o;const F=f?JSON.stringify(f):"";l.useEffect(()=>{const m=k.current;if(!m)return;const r=new re({theme:M(),fontFamily:'"SF Mono", "JetBrains Mono", ui-monospace, Menlo, Consolas, monospace',fontSize:13,cursorBlink:!0,scrollback:8e3}),g=new ie;r.loadAddon(g),r.open(m),N.current={term:r,fit:g},u&&r.focus();const C=()=>{r.options.theme=M()};window.addEventListener(z,C),r.attachCustomKeyEventHandler(d=>{var A;if(d.type!=="keydown")return!0;const D=(A=d.key)==null?void 0:A.toLowerCase();if((d.ctrlKey||d.metaKey)&&D==="v")return!1;if(d.ctrlKey&&!d.shiftKey&&!d.altKey&&D==="c"&&r.hasSelection()){const E=r.textarea;return E&&(E.value=r.getSelection(),E.select()),!1}return!0});const _=T(t,c,{write:d=>r.write(le(d).clean),dispose:()=>r.dispose()}),$=()=>{try{g.fit(),j({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.cols,rows:r.rows})}catch{}},B=requestAnimationFrame(()=>{try{g.fit()}catch{}j(f?{type:"run_command",terminalId:c,conversationId:t,command:f,cols:r.cols,rows:r.rows}:{type:"terminal_create",terminalId:c,title:h,locale:w.current,conversationId:t,cwd:a,cols:r.cols,rows:r.rows})}),S=r.onData(d=>{j({type:"terminal_input",terminalId:c,conversationId:t,data:d})});let v=null;return typeof ResizeObserver<"u"&&(v=new ResizeObserver(()=>{m.offsetWidth>0&&m.offsetHeight>0&&$()}),v.observe(m)),()=>{cancelAnimationFrame(B),S.dispose(),window.removeEventListener(z,C),v==null||v.disconnect(),_(),r.dispose(),N.current=null}},[t,c,F,j,T]),l.useEffect(()=>{if(!u)return;const m=requestAnimationFrame(()=>{const r=N.current;if(r){try{r.fit.fit(),j({type:"terminal_resize",terminalId:c,conversationId:t,cols:r.term.cols,rows:r.term.rows})}catch{}r.term.focus()}});return()=>cancelAnimationFrame(m)},[u]);const{t:R}=O(),x=l.useRef(void 0);return l.useEffect(()=>{if(b===x.current||(x.current=b,b!==!1))return;const m=N.current;m&&m.term.write(`\r
|
|
4
4
|
\x1B[90m${R("exitBanner",{code:y??""})}\x1B[0m\r
|