pi2web 0.2.0 → 0.2.6

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 (55) hide show
  1. package/README.md +209 -18
  2. package/dist/activity.js +178 -0
  3. package/dist/agent-feed.js +133 -0
  4. package/dist/agent-templates.js +144 -0
  5. package/dist/agents.js +575 -97
  6. package/dist/auth.js +85 -20
  7. package/dist/collab/baseline.js +84 -0
  8. package/dist/collab/dispatcher.js +221 -0
  9. package/dist/collab/hub.js +2888 -0
  10. package/dist/collab/pi-extension.js +494 -0
  11. package/dist/collab/review-flow.js +459 -0
  12. package/dist/collab/routes.js +285 -0
  13. package/dist/collab/schemas.js +281 -0
  14. package/dist/collab/scoring-flow.js +336 -0
  15. package/dist/collab/similarity.js +15 -0
  16. package/dist/collab/store.js +918 -0
  17. package/dist/collab/types.js +137 -0
  18. package/dist/collab/validate.js +185 -0
  19. package/dist/drafts.js +50 -0
  20. package/dist/http/agent-routes.js +196 -0
  21. package/dist/http/login-limiter.js +53 -0
  22. package/dist/http/mail-settings-routes.js +53 -0
  23. package/dist/http/system-prompt-routes.js +52 -0
  24. package/dist/http/utils.js +43 -0
  25. package/dist/http/web-assets.js +63 -0
  26. package/dist/http/workspace-routes.js +58 -0
  27. package/dist/mail-notifier.js +95 -21
  28. package/dist/main.js +141 -17
  29. package/dist/mcp.js +132 -0
  30. package/dist/metadata-store.js +199 -56
  31. package/dist/paths.js +10 -0
  32. package/dist/sdk-backend.js +288 -49
  33. package/dist/server.js +558 -358
  34. package/dist/session-namer.js +163 -0
  35. package/dist/terminals.js +94 -27
  36. package/dist/timeline.js +185 -0
  37. package/dist/voice.js +196 -40
  38. package/dist/workspaces.js +91 -26
  39. package/dist/ws/agent-socket.js +256 -0
  40. package/dist/ws/terminal-socket.js +46 -0
  41. package/npm-shrinkwrap.json +3685 -0
  42. package/package.json +18 -6
  43. package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__init__.cpython-312.pyc +0 -0
  44. package/packages/edge-tts-server/src/remote_pi_edge_tts/__pycache__/__main__.cpython-312.pyc +0 -0
  45. package/packages/protocol/openapi.yaml +8 -2
  46. package/packages/protocol/websocket.schema.json +94 -10
  47. package/web/activity-view.js +121 -0
  48. package/web/app.js +3770 -404
  49. package/web/backend-state.js +269 -0
  50. package/web/collab.html +95 -0
  51. package/web/collab.js +1080 -0
  52. package/web/hid-keyboard.js +453 -0
  53. package/web/index.html +14 -9
  54. package/web/styles.css +73 -0
  55. package/ANDROID_APP_FEATURES.md +0 -256
@@ -0,0 +1,269 @@
1
+ (() => {
2
+ 'use strict';
3
+
4
+ const CONNECTIONS_KEY = 'rpConnections';
5
+ const LEGACY_BACKEND_ID = 'bk-main';
6
+
7
+ function createBackend(entry) {
8
+ return {
9
+ id: entry.id,
10
+ name: String(entry.name || ''),
11
+ base: String(entry.base || '').replace(/\/$/, ''),
12
+ token: String(entry.token || ''),
13
+ saved: Boolean(entry.saved ?? entry.token),
14
+ status: 'off',
15
+ error: '',
16
+ serverInfo: '',
17
+ statusInfo: null,
18
+ authRequired: true,
19
+ voiceEnabled: false,
20
+ voiceSttEnabled: false,
21
+ agents: [],
22
+ terminals: [],
23
+ workspaces: [],
24
+ ws: null,
25
+ reconnectTimer: null,
26
+ reconnectAttempt: 0,
27
+ manuallyClosed: false,
28
+ reconnecting: false
29
+ };
30
+ }
31
+
32
+ function createBackendId() {
33
+ return `bk-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
34
+ }
35
+
36
+ function resourceKey(backendId, remoteId) {
37
+ return `${backendId}:${remoteId}`;
38
+ }
39
+
40
+ const normalizePath = value =>
41
+ String(value || '')
42
+ .split('\\')
43
+ .join('/')
44
+ .replace(/\/+/g, '/')
45
+ .replace(/\/$/, '') || '/';
46
+ const isInsidePath = (file, dir) => file === dir || file.startsWith(`${dir}/`);
47
+
48
+ /**
49
+ * Picks the Workspace that actually owns a cwd. Callers must pass the Workspaces of one
50
+ * Backend only: identical paths on two machines are different directories. The deepest
51
+ * matching root wins so nested Workspaces resolve to the closest one.
52
+ */
53
+ function matchWorkspace(workspaces, cwd) {
54
+ const target = normalizePath(cwd);
55
+ let best = null,
56
+ bestLength = -1;
57
+ for (const workspace of workspaces || []) {
58
+ const root = normalizePath(workspace.rootPath);
59
+ if (!isInsidePath(target, root)) continue;
60
+ if (root.length > bestLength) {
61
+ best = workspace;
62
+ bestLength = root.length;
63
+ }
64
+ }
65
+ return best;
66
+ }
67
+
68
+ function splitResourceKey(key) {
69
+ const value = String(key || '');
70
+ const separator = value.indexOf(':');
71
+ if (separator < 0) return {backendId: '', remoteId: value};
72
+ return {backendId: value.slice(0, separator), remoteId: value.slice(separator + 1)};
73
+ }
74
+
75
+ function migrateLegacySelections(storage) {
76
+ const selectedAgentId = String(storage.rpAgentId || '');
77
+ if (selectedAgentId && !selectedAgentId.includes(':')) {
78
+ const compositeId = resourceKey(LEGACY_BACKEND_ID, selectedAgentId);
79
+ storage.rpAgentId = compositeId;
80
+ }
81
+ for (const key of ['rpTerminalId', 'rpWorkspaceId']) {
82
+ const value = String(storage[key] || '');
83
+ if (value && !value.includes(':')) storage[key] = resourceKey(LEGACY_BACKEND_ID, value);
84
+ }
85
+ try {
86
+ const bindings = JSON.parse(storage.rpSessionKeyboardBindings || '[]');
87
+ if (!Array.isArray(bindings)) return;
88
+ storage.rpSessionKeyboardBindings = JSON.stringify(
89
+ bindings.map(binding => {
90
+ const agentId = String(binding?.agentId || '');
91
+ return {
92
+ ...binding,
93
+ agentId: agentId.includes(':') ? agentId : resourceKey(LEGACY_BACKEND_ID, agentId)
94
+ };
95
+ })
96
+ );
97
+ } catch {}
98
+ }
99
+
100
+ const normalizeBase = value => String(value || '').replace(/\/$/, '');
101
+
102
+ function storedConnections(storage) {
103
+ const stored = storage.getItem(CONNECTIONS_KEY);
104
+ if (stored === null) return null;
105
+ try {
106
+ const entries = JSON.parse(stored);
107
+ if (!Array.isArray(entries)) return null;
108
+ return entries
109
+ .filter(entry => entry && typeof entry.id === 'string' && typeof entry.base === 'string')
110
+ .map(entry => ({
111
+ id: entry.id,
112
+ name: String(entry.name || ''),
113
+ base: normalizeBase(entry.base),
114
+ token: String(entry.token || '')
115
+ }));
116
+ } catch {
117
+ return null;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * The console and the collaboration page share one saved connection list, so neither page
123
+ * may read a private format. An explicitly stored list stays authoritative, including when
124
+ * it is empty; the single-backend format is migrated only when no list exists yet.
125
+ */
126
+ function savedConnections(storage, origin) {
127
+ const stored = storedConnections(storage);
128
+ if (stored) return stored;
129
+ // A user who declined to save the pairing code still has a base, selections and keyboard bindings.
130
+ // Migrating without a code keeps their Backend id, so those keep matching after pairing again.
131
+ if (!storage.rpToken && !storage.rpBase) return [];
132
+ migrateLegacySelections(storage);
133
+ return [
134
+ {
135
+ id: LEGACY_BACKEND_ID,
136
+ name: '本机',
137
+ base: normalizeBase(storage.rpBase || origin),
138
+ token: String(storage.rpToken || '')
139
+ }
140
+ ];
141
+ }
142
+
143
+ /** Connection a page should offer first: the one for `base`, else any with a saved pairing code. */
144
+ function selectConnection(storage, origin, base = '') {
145
+ const items = savedConnections(storage, origin);
146
+ const wanted = normalizeBase(base);
147
+ return (
148
+ (wanted && items.find(item => item.base === wanted)) ||
149
+ items.find(item => item.token) ||
150
+ items[0] ||
151
+ null
152
+ );
153
+ }
154
+
155
+ /** Upserts a connection without touching the other entries. The code is stored only on request. */
156
+ function rememberConnection(storage, {base, token = '', name = '', remember = false} = {}) {
157
+ const value = normalizeBase(base);
158
+ if (!value) return null;
159
+ const items = savedConnections(storage, value);
160
+ const existing = items.find(item => item.base === value);
161
+ const entry = existing || {id: createBackendId(), name: '', base: value, token: ''};
162
+ if (name) entry.name = String(name);
163
+ if (!entry.name) entry.name = value;
164
+ entry.token = remember ? String(token || '') : '';
165
+ const next = existing ? items : [...items, entry];
166
+ storage[CONNECTIONS_KEY] = JSON.stringify(
167
+ next.map(item => ({id: item.id, name: item.name, base: item.base, token: item.token}))
168
+ );
169
+ // The list is authoritative from here on, so the migrated single-backend copy of the code must not
170
+ // survive: keeping it would leave a pairing code behind that the user just asked to forget.
171
+ delete storage.rpToken;
172
+ return entry;
173
+ }
174
+
175
+ function loadStoredBackends(storage, origin) {
176
+ return savedConnections(storage, origin).map(entry =>
177
+ createBackend({...entry, saved: Boolean(entry.token)})
178
+ );
179
+ }
180
+
181
+ class BackendRegistry {
182
+ constructor(storage, origin) {
183
+ this.storage = storage;
184
+ this.items = loadStoredBackends(storage, origin);
185
+ }
186
+
187
+ find(id) {
188
+ return this.items.find(backend => backend.id === id);
189
+ }
190
+
191
+ findByBase(base) {
192
+ return this.items.find(backend => backend.base === base);
193
+ }
194
+
195
+ connected() {
196
+ return this.items.filter(backend => backend.status === 'connected');
197
+ }
198
+
199
+ primary() {
200
+ return this.connected()[0] || null;
201
+ }
202
+
203
+ add(entry) {
204
+ const backend = createBackend(entry);
205
+ this.items.push(backend);
206
+ return backend;
207
+ }
208
+
209
+ remove(backend) {
210
+ const index = this.items.indexOf(backend);
211
+ if (index >= 0) this.items.splice(index, 1);
212
+ }
213
+
214
+ persist() {
215
+ this.storage[CONNECTIONS_KEY] = JSON.stringify(
216
+ this.items.map(backend => ({
217
+ id: backend.id,
218
+ name: backend.name,
219
+ base: backend.base,
220
+ token: backend.saved ? backend.token : ''
221
+ }))
222
+ );
223
+ // The list is authoritative now, so the migrated single-backend copy of the code must not survive.
224
+ delete this.storage.rpToken;
225
+ }
226
+
227
+ deactivate(backend, status = 'off', error = '') {
228
+ backend.status = status;
229
+ backend.error = error;
230
+ backend.reconnecting = false;
231
+ backend.agents = [];
232
+ backend.terminals = [];
233
+ backend.workspaces = [];
234
+ }
235
+
236
+ resources(property, filterBackendId = '') {
237
+ return this.connected()
238
+ .filter(backend => !filterBackendId || backend.id === filterBackendId)
239
+ .flatMap(backend => backend[property]);
240
+ }
241
+
242
+ stamp(backend, item, idProperty) {
243
+ const remoteId = item[idProperty];
244
+ return {
245
+ ...item,
246
+ backendId: backend.id,
247
+ backendName: backend.name,
248
+ remoteId,
249
+ [idProperty]: resourceKey(backend.id, remoteId)
250
+ };
251
+ }
252
+ }
253
+
254
+ window.RemotePiBackendState = {
255
+ BackendRegistry,
256
+ CONNECTIONS_KEY,
257
+ LEGACY_BACKEND_ID,
258
+ loadStoredBackends,
259
+ savedConnections,
260
+ selectConnection,
261
+ rememberConnection,
262
+ createBackendId,
263
+ resourceKey,
264
+ splitResourceKey,
265
+ normalizePath,
266
+ isInsidePath,
267
+ matchWorkspace
268
+ };
269
+ })();
@@ -0,0 +1,95 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="color-scheme" content="dark">
7
+ <meta name="theme-color" content="#2b303b">
8
+ <title>Remote Pi · 协作看板</title>
9
+ <link rel="icon" href="/icon.svg" type="image/svg+xml">
10
+ <link rel="stylesheet" href="/styles.css">
11
+ <style>
12
+ main{grid-template-columns:360px minmax(0,1fr)}
13
+ article{overflow:auto;padding:16px}
14
+ .card{margin:0 0 14px;padding:12px;background:var(--panel);border:1px solid var(--line);border-radius:8px}
15
+ .card h3{margin:0 0 10px;font-size:14px;display:flex;align-items:center;gap:8px}
16
+ .card h3 .grow{flex:1}
17
+ .row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
18
+ .row>label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--muted)}
19
+ .row input,.row select,.row textarea{min-width:150px}
20
+ .field-help{display:block;margin-top:4px;color:var(--muted);font-size:11px;line-height:1.45;max-width:520px}
21
+ .participant-config{padding:8px;border:1px solid var(--line);border-radius:6px;background:#252a33}
22
+ table{width:100%;border-collapse:collapse;font-size:12px}
23
+ th,td{padding:6px 8px;border-bottom:1px solid var(--line);text-align:left;vertical-align:top}
24
+ th{color:var(--muted);font-weight:600}
25
+ .pill{display:inline-block;padding:2px 8px;border-radius:999px;font-size:11px;background:#3b4252;color:#dfe5ef}
26
+ .pill.blue{background:var(--blue);color:#16202a}.pill.green{background:var(--green);color:#16210f}
27
+ .pill.yellow{background:var(--yellow);color:#221c0c}.pill.red{background:var(--red);color:#fff}
28
+ .session-item{display:block;width:100%;margin:6px 0;padding:10px;text-align:left;background:var(--input);border:1px solid transparent;border-radius:7px}
29
+ .session-item.selected{border-color:var(--blue)}
30
+ .session-item b{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
31
+ .session-item small{display:block;margin-top:4px;color:var(--muted)}
32
+ .events{max-height:320px;overflow:auto;font:12px ui-monospace,monospace}
33
+ .events div{padding:3px 0;border-bottom:1px solid #363c48}
34
+ details.raw pre{max-height:280px;overflow:auto;background:#1d2129;padding:8px;border-radius:6px;font:12px ui-monospace,monospace}
35
+ .flow-chart{display:flex;align-items:center;gap:7px;overflow-x:auto;padding:6px 2px 10px}
36
+ .flow-step{flex:0 0 auto;min-width:105px;padding:9px 10px;text-align:center;border:1px solid var(--line);border-radius:7px;background:#252a33;color:var(--muted)}
37
+ .flow-step b{display:block;color:inherit}.flow-step small{display:block;margin-top:3px}
38
+ .flow-step.active{border-color:var(--blue);color:var(--blue);box-shadow:0 0 0 2px #88c0d022}
39
+ .flow-step.done{border-color:#607557;color:var(--green);background:#283329}.flow-step.optional{border-style:dashed}
40
+ .flow-arrow{flex:0 0 auto;color:var(--muted);font-size:18px}.flow-loop{margin-top:2px;color:var(--muted);font-size:12px}
41
+ .recheck-actions{padding:10px;border:1px solid #6f6449;border-radius:7px;background:#332f24}
42
+ .round-section{margin:8px 0;padding:8px 10px;border:1px solid var(--line);border-radius:7px;background:#252a33}
43
+ .round-section>summary{cursor:pointer;display:flex;align-items:center;gap:7px;flex-wrap:wrap}
44
+ .round-section>summary b{margin-right:2px}
45
+ .round-section .round-stats{margin-left:auto;display:flex;gap:5px;flex-wrap:wrap}
46
+ .round-section table{margin:6px 0 10px}
47
+ @media(max-width:700px){main{display:block}aside{height:auto;border-bottom:1px solid var(--line)}article{display:block;height:auto}}
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <header>
52
+ <div class="brand"><img src="/icon.svg" alt="" width="32" height="32"><h1>协作看板</h1></div>
53
+ <a href="/" style="color:var(--blue)">← 返回 Agent 控制台</a>
54
+ <button id="connect" type="button">输入配对码</button>
55
+ <button id="refresh" type="button" title="刷新">↻</button>
56
+ <span id="status" class="muted">未配对</span>
57
+ <span id="escalationBadge" class="muted"></span>
58
+ </header>
59
+ <main>
60
+ <aside>
61
+ <section>
62
+ <div class="section-title"><h2>协作会话</h2><span><select id="statusFilter" aria-label="状态"><option value="active">进行中</option><option value="">全部</option><option value="finished">已结束</option><option value="aborted">已中止</option></select></span></div>
63
+ <div id="sessions"><p class="muted">尚未连接</p></div>
64
+ </section>
65
+ <section>
66
+ <div class="section-title"><h2>新建会话</h2></div>
67
+ <form id="createForm" class="card">
68
+ <div class="row">
69
+ <label>类型<select id="newKind"><option value="review">review 评审</option><option value="scoring">scoring 评分</option></select></label>
70
+ <label>Workspace<select id="newWorkspace"></select></label>
71
+ </div>
72
+ <div class="row" style="margin-top:8px">
73
+ <label style="flex:1">会话标题<input id="newTitle" required placeholder="支付回调评审"><small class="field-help">用于左侧列表、通知邮件和 Agent 任务标题,方便识别会话;它不决定实际评审范围。</small></label>
74
+ <label>工作目录<input id="newCwd" value="." size="8"><small class="field-help">相对于所选 Workspace,例如 <code>.</code> 或 <code>packages/api</code>。</small></label>
75
+ </div>
76
+ <div class="row" style="margin-top:8px">
77
+ <label>评审 / 评分对象<select id="newSubjectType"><option value="commit_range">Git 提交范围</option><option value="diff">当前未提交改动</option><option value="paths">指定文件或目录</option><option value="free">自定义范围</option></select></label>
78
+ <label style="flex:1"><span id="newSubjectValueLabel">Git 范围</span><input id="newSubjectValue" required value="HEAD~1..HEAD" placeholder="HEAD~1..HEAD"><small id="newSubjectHelp" class="field-help">填写 Git revision range。中枢会把它放进每个 Agent 的任务包,并记录为评审基线范围。</small></label>
79
+ </div>
80
+ <label class="row" style="margin-top:8px;flex-direction:column;align-items:stretch">任务说明 / 评审重点(会原样进入每个 Agent 的任务包)<textarea id="newSubjectNotes" rows="4" placeholder="要开发的功能:……;验收标准:……;评审重点:并发与幂等"></textarea></label>
81
+ <p class="field-help">Review 会话默认启用共识流程:所有 Agent 完成盲审后统一进行问题投票、重复问题合并投票与分轮讨论;未收敛项交给人工裁定。</p>
82
+ <div class="row" style="margin-top:8px"><label>争议辩论轮数<input id="newConsensusRounds" type="number" min="1" max="10" step="1" value="3" style="width:90px"><small class="field-help">完成 3 轮“赞成方回应 → 反对方重投”后仍不一致才转人工;Agent 可中途主动上报。</small></label></div>
83
+ <label class="toggle-row" style="margin-top:8px;display:flex;align-items:center;gap:8px"><input id="newImplementationFirst" type="checkbox" style="width:auto">先开发后评审:会话先进入 implementing,开发 Agent 完工(POST /ready 或托管 Agent 空闲)后自动召集评审</label>
84
+ <div class="row" style="margin-top:8px"><button type="submit">创建会话</button></div>
85
+ </form>
86
+ </section>
87
+ </aside>
88
+ <article id="detail"><p class="muted">选择左侧的协作会话,或先创建一个。</p></article>
89
+ </main>
90
+ <dialog id="pairDialog"><form method="dialog" id="pairForm"><h2>连接 Remote Pi</h2><label>服务地址<input id="pairBase" required></label><label>配对码<input id="pairToken" type="password" autocomplete="off" placeholder="服务端使用 --no-auth 时留空" autofocus></label><label class="toggle-row" style="display:flex;align-items:center;gap:8px"><input id="pairRemember" type="checkbox" style="width:auto">在此浏览器保存配对码(仅限可信设备)</label><div class="dialog-actions"><button id="pairCancel" type="button">取消</button><button type="submit">连接</button></div></form></dialog>
91
+ <div id="toast" hidden></div>
92
+ <script src="/backend-state.js"></script>
93
+ <script src="/collab.js"></script>
94
+ </body>
95
+ </html>