pi-web-ui 0.86.2 → 0.87.1

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 (37) hide show
  1. package/CHANGELOG.md +84 -23
  2. package/README.md +1 -1
  3. package/bin/pi-web-ui.mjs +312 -40
  4. package/dist/server/agent-service.js +377 -25
  5. package/dist/server/client-state.js +8 -0
  6. package/dist/server/composer-drafts.js +138 -0
  7. package/dist/server/dsh/dsh-agent-service.js +535 -54
  8. package/dist/server/dsh/dsh-client.js +28 -0
  9. package/dist/server/dsh/dsh-sessions.js +28 -0
  10. package/dist/server/dsh/dsh-usage.js +82 -0
  11. package/dist/server/dsh/preset-clones.js +260 -0
  12. package/dist/server/dsh/runtime/custom-prompt.mjs +33 -0
  13. package/dist/server/dsh/runtime/goal-rpc.mjs +406 -22
  14. package/dist/server/dsh/runtime/launcher.mjs +17 -0
  15. package/dist/server/dsh/runtime/override.patch.yml +19 -1
  16. package/dist/server/files-service.js +259 -1
  17. package/dist/server/index.js +244 -5
  18. package/dist/server/mcp-bridge.js +126 -22
  19. package/dist/server/mcp-hot-reload.js +117 -0
  20. package/dist/server/model-admin.js +93 -3
  21. package/dist/server/plugin-dom.js +83 -0
  22. package/dist/server/plugin-facilities.js +15 -1
  23. package/dist/server/plugin-installer.js +6 -0
  24. package/dist/server/plugins.js +781 -74
  25. package/dist/server/protocol-version.js +1 -1
  26. package/dist/server/provider-oauth-flow.js +157 -0
  27. package/dist/server/update-check.js +22 -0
  28. package/package.json +1 -1
  29. package/plugins/catalog.json +9 -0
  30. package/themes/dark-teal.css +65 -22
  31. package/web/dist/assets/index-8xCnPMZP.js +374 -0
  32. package/web/dist/assets/index-F86qWlJy.css +41 -0
  33. package/web/dist/index.html +3 -2
  34. package/web/dist/assets/TerminalPanel-BytY8dx7.js +0 -6
  35. package/web/dist/assets/TerminalPanel-DOrYoP_4.css +0 -32
  36. package/web/dist/assets/index-CKoVyDkP.css +0 -10
  37. package/web/dist/assets/index-Dmwji4Cr.js +0 -361
@@ -0,0 +1,138 @@
1
+ /**
2
+ * composer-drafts.ts — 未发送输入框草稿的单中心文件存储(全局 <dataDir>/composer-drafts.json)。
3
+ *
4
+ * 设计(issue #166 单中心文件方案):不进 session JSONL、不按会话建 sidecar,
5
+ * 全会话的草稿集中在一个文件里,按 **sessionId**(uuidv7,重启稳定)键入。
6
+ * conversationId 每次重启都变,不能做 key(见 agent-service 的 compaction 注释)。
7
+ *
8
+ * - 空白新会话也能存:sessionId 在 SessionManager.create() 时内存里就有了,
9
+ * 不依赖转录文件落盘(SDK 的 _persist 门控要等首轮 assistant 才写盘)。
10
+ * - 每会话只留最新一条(last-write-wins,按 ts,比 marker-store 的扫描模式更轻)。
11
+ * - 与 per-client 的 client-state.json 不同:按 sessionId 全局存,跨标签页可见。
12
+ * - 文件 I/O 一律 best-effort:持久化故障绝不能弄崩 server(同 subagent-templates)。
13
+ */
14
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
15
+ import { basename, dirname } from "node:path";
16
+ /** 单条草稿上限字符数:超出截断再存(快照全量下发时携带,太大浪费流量)。 */
17
+ export const DRAFT_TEXT_MAX = 20_000;
18
+ /** 草稿保留期:超过这么久没更新,下次加载时清扫(兜底;正常路径靠 clear/prune)。 */
19
+ export const DRAFT_TTL_MS = 30 * 24 * 3600 * 1000;
20
+ /** 转录文件名 `<timestamp>_<sessionId>.jsonl` → sessionId。
21
+ * 时间戳里的 `:`/`.` 已被 SDK 换成 `-`(无下划线),取首个 `_` 之后即 sessionId
22
+ * (uuidv7 本身也不含下划线)。对不上形状返回 undefined(调用方跳过剪枝)。 */
23
+ export function sessionIdFromTranscriptPath(p) {
24
+ const base = basename(p);
25
+ if (!base.endsWith(".jsonl"))
26
+ return undefined;
27
+ const noExt = base.slice(0, -".jsonl".length);
28
+ const i = noExt.indexOf("_");
29
+ if (i < 0 || i + 1 >= noExt.length)
30
+ return undefined;
31
+ return noExt.slice(i + 1);
32
+ }
33
+ /** 归一化待存文本:超长截断;纯空白 → undefined(调用方按「删除」处理,不存空条目)。 */
34
+ export function normalizeDraftText(text) {
35
+ const t = (text ?? "").slice(0, DRAFT_TEXT_MAX);
36
+ return t.trim() ? t : undefined;
37
+ }
38
+ function isValidDraft(d) {
39
+ if (!d || typeof d !== "object")
40
+ return false;
41
+ const o = d;
42
+ return typeof o.text === "string" && typeof o.ts === "number" && typeof o.updatedAt === "number";
43
+ }
44
+ export class ComposerDraftsStore {
45
+ filePath;
46
+ cache = null;
47
+ constructor(filePath) {
48
+ this.filePath = filePath;
49
+ }
50
+ load() {
51
+ if (this.cache)
52
+ return this.cache;
53
+ let raw = {};
54
+ try {
55
+ raw = JSON.parse(readFileSync(this.filePath, "utf8"));
56
+ }
57
+ catch {
58
+ raw = {};
59
+ }
60
+ const now = Date.now();
61
+ const kept = {};
62
+ let pruned = false;
63
+ for (const [k, v] of Object.entries(raw)) {
64
+ if (!isValidDraft(v)) {
65
+ pruned = true;
66
+ continue;
67
+ }
68
+ if (now - v.updatedAt > DRAFT_TTL_MS) {
69
+ pruned = true;
70
+ continue;
71
+ }
72
+ kept[k] = v;
73
+ }
74
+ this.cache = kept;
75
+ // 读到过期/脏条目才回写:正常加载不碰磁盘。
76
+ if (pruned)
77
+ this.persist();
78
+ return this.cache;
79
+ }
80
+ persist() {
81
+ if (!this.cache)
82
+ return;
83
+ try {
84
+ mkdirSync(dirname(this.filePath), { recursive: true });
85
+ const tmp = `${this.filePath}.tmp-${process.pid}`;
86
+ writeFileSync(tmp, JSON.stringify(this.cache, null, 2) + "\n");
87
+ renameSync(tmp, this.filePath);
88
+ }
89
+ catch {
90
+ // best-effort:草稿丢了可以重打,server 绝不能因此崩。
91
+ }
92
+ }
93
+ /** 读一条草稿({text, ts};没有返回 undefined)。 */
94
+ get(sessionId) {
95
+ if (!sessionId)
96
+ return undefined;
97
+ const d = this.load()[sessionId];
98
+ return d ? { text: d.text, ts: d.ts } : undefined;
99
+ }
100
+ /** 存一条草稿:空文本 = 删除;同 key 上 ts 更大的才覆盖(last-write-wins,
101
+ * `>=` 让同 ts 的重发幂等)。只有实际变化才写盘。 */
102
+ save(sessionId, text, ts) {
103
+ if (!sessionId)
104
+ return;
105
+ const map = this.load();
106
+ const norm = normalizeDraftText(text);
107
+ if (norm === undefined) {
108
+ if (map[sessionId] !== undefined) {
109
+ delete map[sessionId];
110
+ this.persist();
111
+ }
112
+ return;
113
+ }
114
+ const prev = map[sessionId];
115
+ if (prev && prev.ts > ts)
116
+ return;
117
+ if (prev && prev.ts === ts && prev.text === norm)
118
+ return;
119
+ map[sessionId] = { text: norm, ts, updatedAt: Date.now() };
120
+ this.persist();
121
+ }
122
+ /** 发送成功 / 会话删除后清掉(有 key 才写盘)。 */
123
+ clear(sessionId) {
124
+ if (!sessionId)
125
+ return;
126
+ const map = this.load();
127
+ if (map[sessionId] !== undefined) {
128
+ delete map[sessionId];
129
+ this.persist();
130
+ }
131
+ }
132
+ /** delete_session 用:按转录文件路径反解 sessionId 并清掉(形状对不上就跳过)。 */
133
+ pruneSessionFile(sessionPath) {
134
+ const id = sessionIdFromTranscriptPath(sessionPath);
135
+ if (id)
136
+ this.clear(id);
137
+ }
138
+ }