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.
Files changed (74) hide show
  1. package/README.md +8 -0
  2. package/dist/server/index.js +40 -3
  3. package/dist/server/plugin-catalog.js +159 -0
  4. package/dist/server/plugins.js +57 -1
  5. package/package.json +111 -108
  6. package/plugins/catalog.json +38 -0
  7. package/web/dist/assets/{TerminalPanel-DL8sTcRC.js → TerminalPanel-B_ACe6Lp.js} +1 -1
  8. package/web/dist/assets/index-Bq4P6EhG.css +10 -0
  9. package/web/dist/assets/index-CaJbqnT7.js +326 -0
  10. package/web/dist/assets/{markdown-BJJLXG-8.js → markdown-DRBrS2Nf.js} +1 -1
  11. package/web/dist/assets/{react-ezqUM1Or.js → react-C9ovnpIm.js} +1 -1
  12. package/web/dist/index.html +4 -4
  13. package/web/dist/assets/abnfDiagram-VCTEODGH-BF9usV5C.js +0 -1
  14. package/web/dist/assets/arc-BHXahvV5.js +0 -1
  15. package/web/dist/assets/architectureDiagram-5GKGNRK7-V2QNH_BW.js +0 -36
  16. package/web/dist/assets/blockDiagram-I7D4REHJ-DNWe8D5y.js +0 -129
  17. package/web/dist/assets/c4Diagram-7LVT6UL2-Cd7YCdDn.js +0 -38
  18. package/web/dist/assets/channel-DDCgqHU5.js +0 -1
  19. package/web/dist/assets/chunk-2Q5K7J3B-Dn4M1g1T.js +0 -1
  20. package/web/dist/assets/chunk-5VM5RSS4-U5uyVkTI.js +0 -15
  21. package/web/dist/assets/chunk-F27PBJKO-lMqDdknq.js +0 -1
  22. package/web/dist/assets/chunk-IMKFNOWR-DlzpFcKz.js +0 -231
  23. package/web/dist/assets/chunk-JWPE2WC7-BdI7zobW.js +0 -1
  24. package/web/dist/assets/chunk-POPQ4Y6H-Dk7oA6BE.js +0 -1
  25. package/web/dist/assets/chunk-SVP7TREG-BhXOKkNm.js +0 -88
  26. package/web/dist/assets/chunk-TICWLB2K-DFW_5Q5s.js +0 -206
  27. package/web/dist/assets/chunk-XXDRQBXY-d-B9YyVA.js +0 -1
  28. package/web/dist/assets/classDiagram-ZZMXUADV-DnlRT21K.js +0 -1
  29. package/web/dist/assets/classDiagram-v2-VYDZK3BY-DnlRT21K.js +0 -1
  30. package/web/dist/assets/cose-bilkent-JH36ORCC-W3IYYiNN.js +0 -1
  31. package/web/dist/assets/cynefin-OW5HDTMX-DVcVxvWj.js +0 -178
  32. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DdUHHDle.js +0 -62
  33. package/web/dist/assets/cytoscape.esm-Bch-eiPH.js +0 -331
  34. package/web/dist/assets/dagre-GXQ25YYZ-DMqzPbm8.js +0 -4
  35. package/web/dist/assets/defaultLocale-DX6XiGOO.js +0 -1
  36. package/web/dist/assets/diagram-S7CK7UJ4-g-T7ZpRp.js +0 -30
  37. package/web/dist/assets/diagram-UQ7AKVKN-Cj1HB5mV.js +0 -41
  38. package/web/dist/assets/diagram-VSXAHHWV-D4Xgm5yq.js +0 -3
  39. package/web/dist/assets/diagram-VX7I27RA-CPaDZ0sg.js +0 -24
  40. package/web/dist/assets/diagram-Z3DM3KII-BwroUmR-.js +0 -24
  41. package/web/dist/assets/ebnfDiagram-PWID7BFC-CymjTfGP.js +0 -1
  42. package/web/dist/assets/erDiagram-RLTQ6QDP-CkCv1f0Q.js +0 -99
  43. package/web/dist/assets/flowDiagram-HODETNUW-IrWWLHqn.js +0 -168
  44. package/web/dist/assets/ganttDiagram-EL5Y4UJY-DhHylBej.js +0 -292
  45. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-C1O_4b0m.js +0 -106
  46. package/web/dist/assets/index-B-D8dBbi.js +0 -326
  47. package/web/dist/assets/index-DWUw-5kk.css +0 -10
  48. package/web/dist/assets/infoDiagram-27XIBGKW-B3SzbIHm.js +0 -2
  49. package/web/dist/assets/init-Gi6I4Gst.js +0 -1
  50. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CM5U90LI.js +0 -70
  51. package/web/dist/assets/journeyDiagram-3NMN7TZE-BIWbaonv.js +0 -139
  52. package/web/dist/assets/kanban-definition-UXKFOSKX-Cg0FRvzj.js +0 -89
  53. package/web/dist/assets/katex-C5jXJg4s.js +0 -257
  54. package/web/dist/assets/layout-BMZQTQDF.js +0 -1
  55. package/web/dist/assets/linear--htAmwBy.js +0 -1
  56. package/web/dist/assets/mermaid.core-Bxpg8jiH.js +0 -312
  57. package/web/dist/assets/mindmap-definition-YA3MSWOX-CNp9C6Gy.js +0 -96
  58. package/web/dist/assets/ordinal-Cboi1Yqb.js +0 -1
  59. package/web/dist/assets/pegDiagram-XKGWAZYB-B4mLGlJp.js +0 -1
  60. package/web/dist/assets/pieDiagram-E7YTZNPT-hP4zuAzM.js +0 -39
  61. package/web/dist/assets/quadrantDiagram-AXDQQJYC-DVN-GEzm.js +0 -7
  62. package/web/dist/assets/railroadDiagram-O6MQD6OU-BjBVdCWk.js +0 -1
  63. package/web/dist/assets/requirementDiagram-BXWQKSXE-BOwNWcER.js +0 -84
  64. package/web/dist/assets/sankeyDiagram-P5KCCOFB-CkK6Y6CK.js +0 -40
  65. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-D49cIkIS.js +0 -162
  66. package/web/dist/assets/sizeCapture-INFHLROL-D9R6YbVj.js +0 -1
  67. package/web/dist/assets/stateDiagram-D77RDMKH-DvwH4KPJ.js +0 -1
  68. package/web/dist/assets/stateDiagram-v2-MP3YSRHH-CHVwI1ZU.js +0 -1
  69. package/web/dist/assets/swimlanes-42K2YHIH-DgpCt172.js +0 -1
  70. package/web/dist/assets/swimlanesDiagram-VR7AAH4N-L7ZmzB2n.js +0 -8
  71. package/web/dist/assets/timeline-definition-24CTP7MA-Cy-6c2rf.js +0 -120
  72. package/web/dist/assets/vennDiagram-4TSXK5OY-D05mFAgh.js +0 -34
  73. package/web/dist/assets/wardleyDiagram-VM6X3IG4--c9A1xky.js +0 -78
  74. 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
 
@@ -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
+ }
@@ -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
- "name": "pi-web-ui",
3
- "version": "0.64.7",
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
- "README.md",
24
- "LICENSE"
25
- ],
26
- "keywords": [
27
- "pi",
28
- "pi-package",
29
- "coding-agent",
30
- "ai",
31
- "chat",
32
- "web-ui",
33
- "terminal",
34
- "llm"
35
- ],
36
- "repository": {
37
- "type": "git",
38
- "url": "git+https://github.com/xing-shuyin/pi-web-ui.git"
39
- },
40
- "pi": {
41
- "extensions": [
42
- "./extensions"
43
- ]
44
- },
45
- "engines": {
46
- "node": ">=22.19.0"
47
- },
48
- "scripts": {
49
- "prepublishOnly": "npm run build",
50
- "dev": "concurrently -k -n server,web -c blue,green \"npm:dev:server\" \"npm:dev:web\"",
51
- "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",
52
- "dev:web": "vite --config web/vite.config.ts",
53
- "build": "npm run build:web && npm run build:server && npm run build:dsh-runtime",
54
- "build:web": "vite build --config web/vite.config.ts",
55
- "build:server": "tsc -p tsconfig.server.json",
56
- "build:dsh-runtime": "node scripts/copy-dsh-runtime.mjs",
57
- "typecheck": "tsc -p tsconfig.server.json --noEmit && tsc -p web/tsconfig.json --noEmit && tsc -p tsconfig.tests.json --noEmit",
58
- "test": "vitest run",
59
- "test:unit": "vitest run",
60
- "test:smoke": "node tests/run-smoke.mjs",
61
- "check:protocol": "node scripts/check-protocol-sync.mjs",
62
- "test:freeze": "node tests/freeze-test.mjs",
63
- "start": "node dist/server/index.js",
64
- "format": "prettier --write server/ web/src/ extensions/ scripts/ bin/ tests/*.mjs tests/unit/ *.mjs",
65
- "format:check": "prettier --check server/ web/src/ extensions/ scripts/ bin/ tests/*.mjs tests/unit/ *.mjs",
66
- "lint": "oxlint server/ web/src/ extensions/ scripts/",
67
- "lint:fix": "oxlint --fix server/ web/src/ extensions/ scripts/"
68
- },
69
- "dependencies": {
70
- "@deepseek-ai/dsh-sdk-jsonrpc-server": "^0.1.1-rc.2",
71
- "@deepseek-ai/dsh-sdk-protocol": "^0.1.1-rc.2",
72
- "@earendil-works/pi-coding-agent": "^0.84.4",
73
- "@xterm/addon-fit": "^0.11.0",
74
- "@xterm/xterm": "^6.0.0",
75
- "compression": "^1.8.1",
76
- "express": "^4.21.2",
77
- "highlight.js": "^11.10.0",
78
- "mermaid": "^11.17.2",
79
- "node-pty": "^1.1.0",
80
- "react": "^18.3.1",
81
- "react-dom": "^18.3.1",
82
- "react-icons": "^5.7.0",
83
- "react-markdown": "^9.0.1",
84
- "rehype-highlight": "^7.0.1",
85
- "remark-gfm": "^4.0.0",
86
- "typebox": "^1.3.14",
87
- "ws": "^8.18.0"
88
- },
89
- "devDependencies": {
90
- "@types/compression": "^1.8.1",
91
- "@types/express": "^4.17.21",
92
- "@types/node": "^22.10.0",
93
- "@types/react": "^18.3.12",
94
- "@types/react-dom": "^18.3.1",
95
- "@types/ws": "^8.5.13",
96
- "@vitejs/plugin-react": "^4.3.4",
97
- "concurrently": "^9.1.0",
98
- "cross-env": "^10.1.0",
99
- "oxlint": "^1.81.0",
100
- "playwright-core": "^1.62.1",
101
- "prettier": "^3.9.6",
102
- "tsx": "^4.19.2",
103
- "typescript": "^5.7.2",
104
- "vite": "^6.0.3",
105
- "vitest": "^4.1.11"
106
- },
107
- "allowScripts": {
108
- "node-pty@1.1.0": true
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-BJJLXG-8.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-B-D8dBbi.js";import{D as re,o as ie}from"./xterm-D1D2FVe3.js";import"./react-ezqUM1Or.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
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