pi-web-ui 0.62.0 → 0.63.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 (56) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +504 -497
  3. package/README.zh-CN.md +427 -422
  4. package/bin/pi-web-ui.mjs +1809 -1881
  5. package/deploy/com.xingshuyin.pi-web-ui.plist +48 -48
  6. package/deploy/nginx-subpath.conf +88 -88
  7. package/deploy/pi-web-ui-task.xml +54 -54
  8. package/deploy/pi-web-ui.service +31 -31
  9. package/dist/server/agent-service.js +318 -88
  10. package/dist/server/attachments.js +37 -28
  11. package/dist/server/bg-servers.js +6 -1
  12. package/dist/server/client-state.js +15 -7
  13. package/dist/server/control-socket.js +1 -5
  14. package/dist/server/dsh/dsh-agent-service.js +454 -118
  15. package/dist/server/dsh/dsh-client.js +1 -3
  16. package/dist/server/dsh/dsh-sessions.js +4 -14
  17. package/dist/server/dsh/runtime/cordis.yml +1 -1
  18. package/dist/server/dsh/runtime/goal-rpc.mjs +645 -662
  19. package/dist/server/dsh/runtime/launcher.mjs +8 -18
  20. package/dist/server/dsh/runtime/override.patch.yml +9 -9
  21. package/dist/server/dsh/runtime/runtime-root.mjs +7 -11
  22. package/dist/server/ensure-bash.js +2 -5
  23. package/dist/server/files-service.js +42 -36
  24. package/dist/server/goal-service.js +59 -30
  25. package/dist/server/index.js +17 -10
  26. package/dist/server/mcp-bridge.js +11 -5
  27. package/dist/server/model-admin.js +78 -72
  28. package/dist/server/plugin-facilities.js +2 -4
  29. package/dist/server/plugin-updater.js +1 -1
  30. package/dist/server/plugins.js +26 -8
  31. package/dist/server/process-utils.js +4 -4
  32. package/dist/server/protocol-version.js +1 -1
  33. package/dist/server/scm.js +27 -21
  34. package/dist/server/serialize.js +1 -3
  35. package/dist/server/settings-service.js +63 -21
  36. package/dist/server/slash-commands.js +29 -3
  37. package/dist/server/subagent-templates.js +249 -0
  38. package/dist/server/subagents.js +150 -0
  39. package/dist/server/terminals.js +101 -40
  40. package/dist/server/text-sniff.js +4 -24
  41. package/dist/server/update-check.js +2 -4
  42. package/dist/server/uploads.js +1 -3
  43. package/dist/server/vision-bridge.js +2 -7
  44. package/dist/server/wait-subscription-scan.js +102 -21
  45. package/dist/server/webui-context.js +1 -1
  46. package/extensions/webui.ts +190 -192
  47. package/package.json +109 -103
  48. package/web/dist/assets/TerminalPanel-B9vcI7-_.js +2 -0
  49. package/web/dist/assets/index-B8Wv29p5.css +10 -0
  50. package/web/dist/assets/index-CWUWD0rq.js +323 -0
  51. package/web/dist/favicon.svg +8 -8
  52. package/web/dist/index.html +15 -15
  53. package/web/public/favicon.svg +8 -8
  54. package/web/dist/assets/TerminalPanel-BkCWDsIG.js +0 -2
  55. package/web/dist/assets/index-BRVKed7W.js +0 -321
  56. package/web/dist/assets/index-ORTpR6ZQ.css +0 -10
@@ -38,20 +38,14 @@ if (!runtimeBase) {
38
38
  }
39
39
 
40
40
  const runConfig = process.env.DSH_CORDIS_CONFIG ?? join(HERE, "cordis.yml");
41
- const baseBundlePatch = join(
42
- runtimeBase,
43
- "@deepseek-ai",
44
- "dsh-base",
45
- "cordis.patch.yml",
46
- );
41
+ const baseBundlePatch = join(runtimeBase, "@deepseek-ai", "dsh-base", "cordis.patch.yml");
47
42
  const overridePatch = join(HERE, "override.patch.yml");
48
43
 
49
44
  // 用户 patch 层:<dataDir>/dsh-patches/*.yml(按文件名序,在 override 之后)。
50
45
  // 引擎(DshClientSession)负责在重启运行时前创建目录;launcher 只负责加载。
51
- const userPatchDir =
52
- process.env.PI_WEB_DSH_DATA_DIR
53
- ? join(process.env.PI_WEB_DSH_DATA_DIR, "dsh-patches")
54
- : (process.env.PI_WEB_DSH_PATCH_DIR ?? null);
46
+ const userPatchDir = process.env.PI_WEB_DSH_DATA_DIR
47
+ ? join(process.env.PI_WEB_DSH_DATA_DIR, "dsh-patches")
48
+ : (process.env.PI_WEB_DSH_PATCH_DIR ?? null);
55
49
  const userPatchFiles = [];
56
50
  if (userPatchDir) {
57
51
  try {
@@ -78,7 +72,7 @@ const { boot, installFailLoud, loadOverlayPatches } = await import(
78
72
  // sdk-jsonrpc 走本地扩展插件 goal-rpc.mjs(官方 server 类 + goal RPC 方法);
79
73
  // 官方入口(base 类)通过 env PI_WEB_DSH_JSONRPC_ENTRY 传给 wrapper。
80
74
  const jsonrpcWrapper = join(HERE, "goal-rpc.mjs");
81
- const jsonrpcEntry = process.env.PI_WEB_DSH_JSONRPC_ENTRY
75
+ const _jsonrpcEntry = process.env.PI_WEB_DSH_JSONRPC_ENTRY
82
76
  ? resolve(process.env.PI_WEB_DSH_JSONRPC_ENTRY)
83
77
  : join(
84
78
  resolve(HERE, "..", "..", "..", ".."),
@@ -106,9 +100,7 @@ const userPatchLists = userPatchFiles.map((file) => {
106
100
  try {
107
101
  return loadOverlayPatches(BIN_NAME, file);
108
102
  } catch (err) {
109
- process.stderr.write(
110
- `[${BIN_NAME}] 跳过用户 patch ${file}: ${err?.message ?? String(err)}\n`,
111
- );
103
+ process.stderr.write(`[${BIN_NAME}] 跳过用户 patch ${file}: ${err?.message ?? String(err)}\n`);
112
104
  return [];
113
105
  }
114
106
  });
@@ -156,9 +148,7 @@ const release = () => {
156
148
  try {
157
149
  await ctx.fiber.dispose();
158
150
  } catch (err) {
159
- console.error(
160
- `[${BIN_NAME}] teardown 错误: ${err?.message ?? String(err)}`,
161
- );
151
+ console.error(`[${BIN_NAME}] teardown 错误: ${err?.message ?? String(err)}`);
162
152
  }
163
153
  process.exit(0);
164
154
  })();
@@ -171,4 +161,4 @@ process.stdin.resume();
171
161
  process.stdin.on("end", release);
172
162
  process.stdin.on("close", release);
173
163
  process.on("SIGTERM", release);
174
- process.on("SIGINT", () => process.exit(130));
164
+ process.on("SIGINT", () => process.exit(130));
@@ -6,26 +6,26 @@
6
6
 
7
7
  # 会话持久化根 —— 覆盖 base 的 dshHomePath('sessions'),让 pi-web-ui 指向自己的数据目录。
8
8
  - id: session-persistence-jsonl
9
- name: '@deepseek-ai/dsh-session-persistence-jsonl'
9
+ name: "@deepseek-ai/dsh-session-persistence-jsonl"
10
10
  config:
11
11
  root: !!js process.env.DSH_SESSION_ROOT ?? dshHomePath('sessions')
12
12
 
13
13
  # Agent 默认模型 —— initialize() 每次都带 provider/model,这里只兜底。
14
14
  - id: agent-default-model
15
- name: '@deepseek-ai/dsh-agent-default-model'
15
+ name: "@deepseek-ai/dsh-agent-default-model"
16
16
  config:
17
17
  provider: !!js process.env.DSH_DEFAULT_PROVIDER ?? 'deepseek-official'
18
18
  model: !!js process.env.DSH_DEFAULT_MODEL ?? 'deepseek-v4-flash'
19
19
 
20
20
  # 部署人设(默认空;pi-web-ui 设置面板可注入)。
21
21
  - id: system-prompt
22
- name: '@deepseek-ai/dsh-system-prompt'
22
+ name: "@deepseek-ai/dsh-system-prompt"
23
23
  config:
24
24
  persona: !!js process.env.DSH_PERSONA ?? ''
25
25
 
26
26
  # 沙箱工作区根(workspace-write 权限模式的护栏边界)。
27
27
  - id: sandbox-policy
28
- name: '@deepseek-ai/dsh-sandbox-policy'
28
+ name: "@deepseek-ai/dsh-sandbox-policy"
29
29
  config:
30
30
  mode: !!js process.env.DSH_PERMISSION_MODE ?? 'workspace-write'
31
31
  workspaceRoot: !!js process.env.DSH_CWD ?? process.cwd()
@@ -33,14 +33,14 @@
33
33
  # 无头 JSON-RPC 服务:没有交互用户来应答审批弹窗;工具在 workspace-write
34
34
  # 沙箱内直接执行,用户的 Stop 按钮就是控制手段。
35
35
  - id: approval
36
- name: '@deepseek-ai/dsh-user-approval'
36
+ name: "@deepseek-ai/dsh-user-approval"
37
37
  config:
38
- policy: 'never'
38
+ policy: "never"
39
39
 
40
40
  # 权限预设表 —— 与 approval=never 组合匹配。patch 语义:整行 config 全量替换,
41
41
  # 必须重述 base 的全部预设并补上 (workspace-write, never) 组合,显式 defaultPreset。
42
42
  - id: permission
43
- name: '@deepseek-ai/dsh-permission-presets'
43
+ name: "@deepseek-ai/dsh-permission-presets"
44
44
  config:
45
45
  presets:
46
46
  read-only:
@@ -62,10 +62,10 @@
62
62
  # 模型提问工具:ask_user_question → ctx.userQuestions(base 已挂 user-questions
63
63
  # 服务)→ goal-rpc wrapper 注册的 provider 桥到浏览器对话框。
64
64
  - id: tool-ask-user
65
- name: '@deepseek-ai/dsh-tool-ask-user'
65
+ name: "@deepseek-ai/dsh-tool-ask-user"
66
66
  # Stdio JSON-RPC 服务插件(inject: agents)。name 用 !!js 求值为绝对路径
67
67
  # (指向项目 node_modules 里的 dsh-sdk-jsonrpc-server),因为 cordis loader
68
68
  # 的裸包解析只认 bareModuleBaseUrl(运行时树),混树挂载必须走绝对路径;
69
69
  # 该插件的 peer 依赖从项目树自洽解析。
70
70
  - id: sdk-jsonrpc
71
- name: !!js process.env.PI_WEB_DSH_JSONRPC_ENTRY
71
+ name: !!js process.env.PI_WEB_DSH_JSONRPC_ENTRY
@@ -68,16 +68,12 @@ export async function resolveRuntimeBase() {
68
68
  }
69
69
  try {
70
70
  const { spawnSync } = await import("node:child_process");
71
- const res = spawnSync(
72
- process.platform === "win32" ? "npm" : "npm",
73
- ["root", "-g"],
74
- {
75
- encoding: "utf8",
76
- timeout: 15_000,
77
- windowsHide: true,
78
- ...(process.platform === "win32" ? { shell: true } : {}),
79
- },
80
- );
71
+ const res = spawnSync(process.platform === "win32" ? "npm" : "npm", ["root", "-g"], {
72
+ encoding: "utf8",
73
+ timeout: 15_000,
74
+ windowsHide: true,
75
+ ...(process.platform === "win32" ? { shell: true } : {}),
76
+ });
81
77
  const root = String(res.stdout ?? "").trim();
82
78
  if (root) {
83
79
  const base = runtimeBaseFor(root);
@@ -87,4 +83,4 @@ export async function resolveRuntimeBase() {
87
83
  /* fall through */
88
84
  }
89
85
  return null;
90
- }
86
+ }
@@ -11,7 +11,7 @@
11
11
  * Download is fire-and-forget at server start and never throws: on failure
12
12
  * the terminal simply falls back to $COMSPEC (cmd.exe) as before.
13
13
  */
14
- import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
14
+ import { copyFileSync, existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
15
15
  import { homedir } from "node:os";
16
16
  import { join } from "node:path";
17
17
  /** Official busybox-w32 64-bit Unicode build (Win10 1903+ / Win11). */
@@ -32,10 +32,7 @@ export function windowsBashPath() {
32
32
  export function hasGitBash() {
33
33
  const pf = process.env.ProgramFiles;
34
34
  const pf86 = process.env["ProgramFiles(x86)"];
35
- for (const cand of [
36
- pf ? join(pf, "Git", "bin", "bash.exe") : "",
37
- pf86 ? join(pf86, "Git", "bin", "bash.exe") : "",
38
- ]) {
35
+ for (const cand of [pf ? join(pf, "Git", "bin", "bash.exe") : "", pf86 ? join(pf86, "Git", "bin", "bash.exe") : ""]) {
39
36
  if (cand && existsSync(cand))
40
37
  return true;
41
38
  }
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import { mkdirSync, statSync, writeFileSync, watch } from "node:fs";
9
9
  import { resolve, relative, sep } from "node:path";
10
- import { previewKind, looksLikeText, decodeText, hexDump, countLines, } from "./text-sniff.js";
11
- import { gitDirOf, isNotRepoError, scmStatus, scmHistory, scmFileDiff, scmCommitDetail, } from "./scm.js";
10
+ import { previewKind, looksLikeText, decodeText, hexDump, countLines } from "./text-sniff.js";
11
+ import { gitDirOf, isNotRepoError, scmStatus, scmHistory, scmFileDiff, scmCommitDetail } from "./scm.js";
12
12
  export const IS_WIN32 = process.platform === "win32";
13
13
  /** 预览只读文件前 512KB。 */
14
14
  export const MAX_PREVIEW_BYTES = 512 * 1024;
@@ -36,14 +36,7 @@ const IGNORED_ENTRIES = new Set([
36
36
  // hide what would flood or destabilize the panel (dependency trees, VCS
37
37
  // internals, session data) plus pure junk. Build output (dist/.next/…) and
38
38
  // local env dirs (venv/__pycache__/…) stay visible — "所有文件可查看".
39
- const IGNORED_ENTRIES_WIN = new Set([
40
- "node_modules",
41
- ".git",
42
- ".pi-web",
43
- ".DS_Store",
44
- "Thumbs.db",
45
- "desktop.ini",
46
- ]);
39
+ const IGNORED_ENTRIES_WIN = new Set(["node_modules", ".git", ".pi-web", ".DS_Store", "Thumbs.db", "desktop.ini"]);
47
40
  /** The ignore set for the current platform — keeps win/posix lists separate. */
48
41
  function ignoredEntries() {
49
42
  return IS_WIN32 ? IGNORED_ENTRIES_WIN : IGNORED_ENTRIES;
@@ -122,11 +115,7 @@ async function readDirForUI(abs, rel) {
122
115
  entry.kind = previewKind(d.name);
123
116
  out.push(entry);
124
117
  }
125
- out.sort((a, b) => a.type === b.type
126
- ? a.name.localeCompare(b.name)
127
- : a.type === "dir"
128
- ? -1
129
- : 1);
118
+ out.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : a.type === "dir" ? -1 : 1));
130
119
  const truncated = out.length > MAX;
131
120
  if (truncated)
132
121
  out.length = MAX;
@@ -162,6 +151,7 @@ export class FilesService {
162
151
  type: "notice",
163
152
  level: "warning",
164
153
  text: `路径超出工作区:${relPath ?? ""}`,
154
+ textEn: `Path is outside the workspace: ${relPath ?? ""}`,
165
155
  });
166
156
  return;
167
157
  }
@@ -179,16 +169,13 @@ export class FilesService {
179
169
  type: "notice",
180
170
  level: "warning",
181
171
  text: `目录不可读:${error}`,
172
+ textEn: `Directory is not readable: ${error}`,
182
173
  });
183
174
  }
184
175
  this.host.emit({
185
176
  type: "files",
186
177
  path: rel === "" ? "" : rel,
187
- parent: rel === ""
188
- ? null
189
- : rel.includes("/")
190
- ? rel.slice(0, rel.lastIndexOf("/"))
191
- : "",
178
+ parent: rel === "" ? null : rel.includes("/") ? rel.slice(0, rel.lastIndexOf("/")) : "",
192
179
  entries,
193
180
  truncated,
194
181
  });
@@ -218,9 +205,7 @@ export class FilesService {
218
205
  const results = [];
219
206
  let visited = 0;
220
207
  let truncated = false;
221
- const budgetLeft = () => results.length < MAX_RESULTS &&
222
- visited < MAX_VISITED &&
223
- Date.now() - start < MAX_MS;
208
+ const budgetLeft = () => results.length < MAX_RESULTS && visited < MAX_VISITED && Date.now() - start < MAX_MS;
224
209
  // Breadth-first-ish iterative stack; depth cap is a symlink-cycle guard.
225
210
  const walk = async (abs, rel, depth) => {
226
211
  if (!budgetLeft() || depth > 24) {
@@ -299,8 +284,12 @@ export class FilesService {
299
284
  throw new Error("路径超出工作区");
300
285
  const { staged, worktree } = await scmFileDiff(cwd, arg.path);
301
286
  this.host.emit({
302
- type: "scm_data", reqId, kind, ok: true,
303
- stagedText: staged, worktreeText: worktree,
287
+ type: "scm_data",
288
+ reqId,
289
+ kind,
290
+ ok: true,
291
+ stagedText: staged,
292
+ worktreeText: worktree,
304
293
  });
305
294
  return;
306
295
  }
@@ -315,10 +304,21 @@ export class FilesService {
315
304
  if (isNotRepoError(err)) {
316
305
  // Not a repo — a valid empty answer so the panel shows its hint.
317
306
  this.host.emit({
318
- type: "scm_data", reqId, kind, ok: true, notRepo: true,
319
- branch: "", detached: false, upstream: null,
320
- ahead: 0, behind: 0, upstreamGone: false,
321
- files: [], branches: [], stats: {}, history: [],
307
+ type: "scm_data",
308
+ reqId,
309
+ kind,
310
+ ok: true,
311
+ notRepo: true,
312
+ branch: "",
313
+ detached: false,
314
+ upstream: null,
315
+ ahead: 0,
316
+ behind: 0,
317
+ upstreamGone: false,
318
+ files: [],
319
+ branches: [],
320
+ stats: {},
321
+ history: [],
322
322
  });
323
323
  this.unwatchGit();
324
324
  return;
@@ -482,6 +482,7 @@ export class FilesService {
482
482
  type: "notice",
483
483
  level: "info",
484
484
  text: "此目录不支持实时文件监听(网络盘/受限目录),文件面板已改为每 10 秒自动刷新。",
485
+ textEn: "Live file watching is not supported for this directory (network/restricted); the file panel now refreshes every 10s",
485
486
  });
486
487
  }
487
488
  unwatchDir() {
@@ -513,6 +514,7 @@ export class FilesService {
513
514
  type: "notice",
514
515
  level: "warning",
515
516
  text: `路径超出工作区:${relPath}`,
517
+ textEn: `Path is outside the workspace: ${relPath}`,
516
518
  });
517
519
  return;
518
520
  }
@@ -523,6 +525,7 @@ export class FilesService {
523
525
  type: "notice",
524
526
  level: "warning",
525
527
  text: `不是文件:${relPath}`,
528
+ textEn: `Not a file: ${relPath}`,
526
529
  });
527
530
  return;
528
531
  }
@@ -589,6 +592,7 @@ export class FilesService {
589
592
  type: "notice",
590
593
  level: "error",
591
594
  text: `读取文件失败:${err.message}`,
595
+ textEn: `Failed to read file: ${err.message}`,
592
596
  });
593
597
  }
594
598
  }
@@ -602,6 +606,7 @@ export class FilesService {
602
606
  type: "notice",
603
607
  level: "warning",
604
608
  text: `路径超出工作区:${relPath}`,
609
+ textEn: `Path is outside the workspace: ${relPath}`,
605
610
  });
606
611
  return;
607
612
  }
@@ -610,6 +615,7 @@ export class FilesService {
610
615
  type: "notice",
611
616
  level: "warning",
612
617
  text: "文件内容过大,无法保存(上限 2MB)",
618
+ textEn: "File too large to save (2MB max)",
613
619
  });
614
620
  return;
615
621
  }
@@ -619,6 +625,7 @@ export class FilesService {
619
625
  type: "notice",
620
626
  level: "warning",
621
627
  text: `不是文件:${relPath}`,
628
+ textEn: `Not a file: ${relPath}`,
622
629
  });
623
630
  return;
624
631
  }
@@ -627,6 +634,7 @@ export class FilesService {
627
634
  type: "notice",
628
635
  level: "info",
629
636
  text: `已保存:${wp.rel}`,
637
+ textEn: `Saved: ${wp.rel}`,
630
638
  });
631
639
  // Re-read through the same path as the preview request so the client
632
640
  // gets the canonical content, line count and file size after saving.
@@ -637,6 +645,7 @@ export class FilesService {
637
645
  type: "notice",
638
646
  level: "error",
639
647
  text: `保存文件失败:${err.message}`,
648
+ textEn: `Failed to save file: ${err.message}`,
640
649
  });
641
650
  }
642
651
  }
@@ -665,7 +674,7 @@ export class FilesService {
665
674
  // Basename only — strips any path separators / ".." the name carries;
666
675
  // reject empty results and Windows-illegal characters outright.
667
676
  const base = name.split(/[\\/]/).pop() ?? "";
668
- const safe = (base.replace(/[\/:*?"<>|\x00-\x1f]/g, "_").trim() || "file").slice(0, 200);
677
+ const safe = (base.replace(/[/:*?"<>|\x00-\x1f]/g, "_").trim() || "file").slice(0, 200);
669
678
  const abs = resolve(wp.abs, safe);
670
679
  const rawRel = relative(root, abs);
671
680
  if (rawRel.startsWith("..") || rawRel.includes(`${sep}..`)) {
@@ -687,6 +696,7 @@ export class FilesService {
687
696
  type: "notice",
688
697
  level: "info",
689
698
  text: `已上传:${rawRel.split(sep).join("/")}`,
699
+ textEn: `Uploaded: ${rawRel.split(sep).join("/")}`,
690
700
  });
691
701
  // Emit for the target directory itself so the panel refreshes even
692
702
  // when the active listing/preview isn't that dir (posix watcher only
@@ -733,9 +743,7 @@ export class FilesService {
733
743
  const lastSlash = Math.max(expanded.lastIndexOf("/"), expanded.lastIndexOf("\\"));
734
744
  const dirPart = lastSlash >= 0 ? expanded.slice(0, lastSlash + 1) : "";
735
745
  const prefix = lastSlash >= 0 ? expanded.slice(lastSlash + 1) : expanded;
736
- const dirents = await fs
737
- .readdir(dirPart, { withFileTypes: true })
738
- .catch(() => null);
746
+ const dirents = await fs.readdir(dirPart, { withFileTypes: true }).catch(() => null);
739
747
  if (!dirents) {
740
748
  empty();
741
749
  return;
@@ -747,9 +755,7 @@ export class FilesService {
747
755
  name: d.name,
748
756
  // Windows users type backslashes — normalize the completion to the
749
757
  // wire format ("/") so the picked path round-trips cleanly.
750
- path: IS_WIN32
751
- ? join(dirPart, d.name).split(sep).join("/")
752
- : dirPart + d.name,
758
+ path: IS_WIN32 ? join(dirPart, d.name).split(sep).join("/") : dirPart + d.name,
753
759
  type: (d.isDirectory() ? "dir" : "file"),
754
760
  }))
755
761
  .sort((a, b) => {