pi2web 0.2.6 → 0.2.12
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 +15 -5
- package/dist/activity.js +83 -9
- package/dist/agents.js +244 -43
- package/dist/http/agent-routes.js +37 -3
- package/dist/http/utils.js +5 -1
- package/dist/http/web-assets.js +1 -0
- package/dist/mail-notifier.js +7 -2
- package/dist/metadata-store.js +14 -1
- package/dist/safe-pause.js +100 -0
- package/dist/sdk-backend.js +48 -3
- package/dist/server.js +52 -2
- package/dist/session-performance.js +139 -0
- package/dist/sotto-stt.js +217 -0
- package/dist/voice.js +2 -2
- package/dist/ws/agent-socket.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/packages/protocol/openapi.yaml +22 -1
- package/packages/protocol/websocket.schema.json +4 -3
- package/web/activity-view.js +49 -4
- package/web/app.js +764 -94
- package/web/collab.js +18 -4
- package/web/index.html +5 -2
- package/web/styles.css +8 -2
- package/web/voice-pcm.js +36 -0
package/web/collab.js
CHANGED
|
@@ -58,7 +58,11 @@
|
|
|
58
58
|
const fields = (body.error?.fieldErrors || [])
|
|
59
59
|
.map(entry => `${entry.path}: ${entry.message}`)
|
|
60
60
|
.join(';');
|
|
61
|
-
|
|
61
|
+
const error = Error(
|
|
62
|
+
`${body.error?.message || `HTTP ${response.status}`}${fields ? ` (${fields})` : ''}`
|
|
63
|
+
);
|
|
64
|
+
error.status = response.status;
|
|
65
|
+
throw error;
|
|
62
66
|
}
|
|
63
67
|
return body.data;
|
|
64
68
|
}
|
|
@@ -66,8 +70,15 @@
|
|
|
66
70
|
const requiresAuth = async base => {
|
|
67
71
|
try {
|
|
68
72
|
return (await request(base, '', '/health')).authRequired !== false;
|
|
69
|
-
} catch {
|
|
70
|
-
return true;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (error.status === 401) return true;
|
|
75
|
+
if (error.status === 403)
|
|
76
|
+
throw Error(
|
|
77
|
+
'服务拒绝访问(403):无认证服务默认仅允许本机访问;请检查服务地址、代理和 --no-auth 设置'
|
|
78
|
+
);
|
|
79
|
+
if (error.status === 404) throw Error('服务未提供 /health(404):请检查地址和远端版本');
|
|
80
|
+
if (error instanceof TypeError) throw Error('无法访问服务:请检查地址、网络及浏览器跨域限制(CORS)');
|
|
81
|
+
throw error;
|
|
71
82
|
}
|
|
72
83
|
};
|
|
73
84
|
const post = (url, body = {}) => api(url, {method: 'POST', body: JSON.stringify(body)});
|
|
@@ -1076,5 +1087,8 @@
|
|
|
1076
1087
|
};
|
|
1077
1088
|
window.addEventListener('beforeunload', () => state.ws?.close());
|
|
1078
1089
|
|
|
1079
|
-
connect(state.base, state.token).catch(
|
|
1090
|
+
connect(state.base, state.token).catch(error => {
|
|
1091
|
+
toast(`自动连接失败:${error.message}`);
|
|
1092
|
+
openPair();
|
|
1093
|
+
});
|
|
1080
1094
|
})();
|
package/web/index.html
CHANGED
|
@@ -44,10 +44,13 @@
|
|
|
44
44
|
<button id="sessionName" class="agent-action" title="修改 Session 名称">命名</button>
|
|
45
45
|
<button id="undo" class="agent-action" title="在 Session Tree 中回退输入或 Agent 输出">Undo</button>
|
|
46
46
|
<button id="sessionTree" class="agent-action">Session</button>
|
|
47
|
+
<button id="sessionPerformance" class="agent-action" title="分析当前会话的模型与工具耗时">耗时</button>
|
|
47
48
|
<button id="controls" class="agent-action">模型</button>
|
|
48
49
|
<button id="mcp" class="agent-action">MCP</button>
|
|
49
50
|
<button id="systemPrompt" class="agent-action" title="查看此 Agent 的 System Prompt 与模板">Prompt</button>
|
|
51
|
+
<button id="switchPrompt" class="agent-action" title="仅新会话首次对话前可切换 System Prompt">切换 Prompt</button>
|
|
50
52
|
<button id="compact" class="agent-action">Compact</button>
|
|
53
|
+
<button id="safePause" class="agent-action" title="当前工具执行完后、下一次调用模型前暂停;之后发送消息即可继续">安全暂停</button>
|
|
51
54
|
<button id="stop" class="agent-action">停止</button>
|
|
52
55
|
<button id="abort" class="agent-action danger-action">Abort</button>
|
|
53
56
|
</div>
|
|
@@ -61,7 +64,7 @@
|
|
|
61
64
|
<div id="messages" aria-live="polite"><div class="empty">选择或创建 Agent 开始对话</div></div>
|
|
62
65
|
<form id="prompt">
|
|
63
66
|
<div id="activityBar" hidden></div>
|
|
64
|
-
<div id="queueBar" hidden><span id="queueText"></span><button type="button" id="queueClear" title="清空队列并把消息放回输入框">清空队列</button></div>
|
|
67
|
+
<div id="queueBar" hidden><span id="queueText"></span><div id="queueItems"></div><button type="button" id="queueClear" title="清空队列并把消息放回输入框">清空队列</button></div>
|
|
65
68
|
<div id="mentionPicker" hidden><div class="picker-head"><button type="button" id="mentionUp">↑</button><b id="mentionPath">.</b><button type="button" id="mentionClose">×</button></div><div id="mentionItems"></div></div>
|
|
66
69
|
<div id="commandPicker" hidden><div class="picker-head"><b>命令 · prompt template / skill</b><button type="button" id="commandClose">×</button></div><div id="commandItems"></div></div>
|
|
67
70
|
<div class="composer-row">
|
|
@@ -74,7 +77,7 @@
|
|
|
74
77
|
</article>
|
|
75
78
|
</main>
|
|
76
79
|
<div id="contextMenu" class="context-menu" hidden><button id="menuStartAgent">启动 Agent</button><button id="menuOpenTerminal">在 Terminal 中打开</button><button id="menuSetCwd">设为 Agent 工作路径</button><button id="menuMention">在输入框中引用</button></div>
|
|
77
|
-
<dialog id="configDialog"><form method="dialog" id="configForm"><h2>Remote Pi 配置</h2><section class="config-section"><h3>Backend 连接</h3><div id="backendList"></div><div class="config-actions"><button id="connect" type="button">添加 Backend</button><button id="disconnectAll" type="button" hidden>断开全部</button></div><small id="configConnectionStatus" class="muted"></small></section><section class="config-section"><h3>六键 Session 键盘</h3><div class="config-actions"><button id="keyboardConnect" type="button">连接键盘</button><button id="keyboardDisconnect" type="button" hidden>断开键盘</button></div><small id="keyboardStatus" class="muted">未连接 · 需要 Chrome/Edge 和 HTTPS 或 localhost</small><div id="keyboardSlots" class="keyboard-slots" aria-label="键盘按键绑定"></div></section><section class="config-section"><h3>浏览器通知与语音</h3><div class="config-actions"><button id="notifications" type="button" title="启用所有 Agent 的完成通知">启用通知</button><button id="voicePlayback" type="button" title="启用 Agent 语音摘要播放" hidden>启用语音</button></div><small id="browserNotificationStatus" class="muted"></small></section><section class="config-section"><h3>System Prompt 与 Agent 模板</h3><p class="muted">查看、修改 Pi 的全局 System Prompt(<code>~/.pi/agent/SYSTEM.md</code>、<code>APPEND_SYSTEM.md</code>),并管理新建 Agent 时可选择的模板(<code>~/.pi/pi2web/agent-templates/*.md</code>)。作用于当前 Workspace 所在的 Backend。</p><div class="config-actions"><button id="openSystemPrompts" type="button">管理 System Prompt…</button></div></section><section class="config-section"><h3>邮件通知</h3><p id="mailNotificationAvailability" class="muted"></p><label class="toggle-row"><input id="mailNotificationEnabled" type="checkbox">启动邮件通知</label><label>邮件通知聚合时间(秒)<input id="mailAggregationDelay" type="number" min="0" max="86400" step="1" value="0"><small class="muted">首个任务完成后等待这段时间,期间完成的其他任务合并到同一封邮件;0 表示立即发送。</small></label><label class="toggle-row"><input id="mailIncludeResponse" type="checkbox" checked>包含 Agent 回复内容</label><label class="toggle-row"><input id="mailIncludeSessionDetails" type="checkbox" checked>包含 Session 名称和路径</label><label class="toggle-row"><input id="mailCollabEscalations" type="checkbox" checked>协作会话需要人工裁定或停滞时立即发信</label><small class="muted">如果回复内容和 Session 信息都不包含,邮件只会说明有任务完成。</small></section><div class="dialog-actions"><button id="configClose" type="button">关闭</button><button id="configSave" type="button">保存邮件设置</button></div></form></dialog>
|
|
80
|
+
<dialog id="configDialog"><form method="dialog" id="configForm"><h2>Remote Pi 配置</h2><section class="config-section"><h3>Backend 连接</h3><div id="backendList"></div><div class="config-actions"><button id="connect" type="button">添加 Backend</button><button id="disconnectAll" type="button" hidden>断开全部</button></div><small id="configConnectionStatus" class="muted"></small></section><section class="config-section"><h3>六键 Session 键盘</h3><div class="config-actions"><button id="keyboardConnect" type="button">连接键盘</button><button id="keyboardDisconnect" type="button" hidden>断开键盘</button></div><small id="keyboardStatus" class="muted">未连接 · 需要 Chrome/Edge 和 HTTPS 或 localhost</small><div id="keyboardSlots" class="keyboard-slots" aria-label="键盘按键绑定"></div></section><section class="config-section"><h3>浏览器通知与语音</h3><div class="config-actions"><button id="notifications" type="button" title="启用所有 Agent 的完成通知">启用通知</button><button id="voicePlayback" type="button" title="启用 Agent 语音摘要播放" hidden>启用语音</button></div><small id="browserNotificationStatus" class="muted"></small></section><section class="config-section"><h3>输入</h3><label>发送快捷键 <select id="enterSendMode" aria-label="发送快捷键"><option value="ctrl-enter">Ctrl/⌘+Enter 发送,Enter 换行</option><option value="enter">Enter 发送,Shift+Enter 换行</option></select></label><small class="muted">仅保存在本浏览器,立即生效。输入法选词时的 Enter 不会发送。</small></section><section class="config-section"><h3>Sotto 语音输入</h3><p class="muted">语音输入始终使用提供当前页面的本机 Backend 配置;本机未启用识别时不可用。音频不会发送给远端 Backend,识别出的文字仍可用于远端会话。</p><label class="toggle-row"><input id="sottoEnabled" type="checkbox">启用 Sotto 语音识别(默认关闭)</label><label>Endpoint(pi2web 服务所在机器的本机地址)<input id="sottoEndpoint" type="url" value="http://127.0.0.1:8766" placeholder="http://127.0.0.1:8766"></label><small id="sottoSettingsStatus" class="muted">使用本机 Sotto 已加载的 ASR 模型;未开启时不显示输入框旁的麦克风。</small><div class="config-actions"><button id="saveSotto" type="button">保存语音输入设置</button></div></section><section class="config-section"><h3>System Prompt 与 Agent 模板</h3><p class="muted">查看、修改 Pi 的全局 System Prompt(<code>~/.pi/agent/SYSTEM.md</code>、<code>APPEND_SYSTEM.md</code>),并管理新建 Agent 时可选择的模板(<code>~/.pi/pi2web/agent-templates/*.md</code>)。作用于当前 Workspace 所在的 Backend。</p><div class="config-actions"><button id="openSystemPrompts" type="button">管理 System Prompt…</button></div></section><section class="config-section"><h3>邮件通知</h3><p id="mailNotificationAvailability" class="muted"></p><label class="toggle-row"><input id="mailNotificationEnabled" type="checkbox">启动邮件通知</label><label>邮件通知聚合时间(秒)<input id="mailAggregationDelay" type="number" min="0" max="86400" step="1" value="0"><small class="muted">首个任务完成后等待这段时间,期间完成的其他任务合并到同一封邮件;0 表示立即发送。</small></label><label class="toggle-row"><input id="mailIncludeResponse" type="checkbox" checked>包含 Agent 回复内容</label><label class="toggle-row"><input id="mailIncludeSessionDetails" type="checkbox" checked>包含 Session 名称和路径</label><label class="toggle-row"><input id="mailCollabEscalations" type="checkbox" checked>协作会话需要人工裁定或停滞时立即发信</label><small class="muted">如果回复内容和 Session 信息都不包含,邮件只会说明有任务完成。</small></section><div class="dialog-actions"><button id="configClose" type="button">关闭</button><button id="configSave" type="button">保存邮件设置</button></div></form></dialog>
|
|
78
81
|
<dialog id="pairDialog"><form method="dialog" id="pairForm"><h2>连接 Backend</h2><p class="muted">可以添加多个 backend;连接成功后可选择是否将配对码保存到浏览器本地存储。请仅在可信设备上保存。</p><label>短名称<input id="pairName" placeholder="例如 home / lab,显示在 Session 标签上"></label><label>服务地址<input id="pairBase" required></label><label>配对码<input id="pairToken" type="password" autocomplete="off" placeholder="服务端使用 --no-auth 时留空" autofocus></label><div class="dialog-actions"><button id="pairCancel" type="button">取消</button><button id="pairSubmit" type="submit" value="default">连接</button></div></form></dialog>
|
|
79
82
|
<div id="agentContextMenu" class="context-menu" hidden><button id="menuBindKeyboard">绑定键盘按键…</button><button id="menuKeyboardColor">更换按键颜色…</button><button id="menuUnbindKeyboard">取消按键绑定</button><button id="menuArchiveAgent">Archive</button><button id="menuCloseTerminal" hidden>关闭 Terminal</button></div>
|
|
80
83
|
<dialog id="modal"><form method="dialog"><h2 id="modalTitle"></h2><div id="modalBody"></div><div class="dialog-actions"><button id="modalCancel" type="button">取消</button><button id="modalOk" type="submit" value="default">确定</button></div></form></dialog>
|
package/web/styles.css
CHANGED
|
@@ -72,8 +72,12 @@
|
|
|
72
72
|
@media(max-width:700px){.keyboard-slots{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
|
73
73
|
|
|
74
74
|
/* Queued steering/follow-up messages waiting for the running turn. */
|
|
75
|
-
#queueBar{display:flex;align-items:
|
|
76
|
-
#queueBar
|
|
75
|
+
#queueBar{display:flex;align-items:flex-start;flex-wrap:wrap;gap:8px;margin-bottom:8px;padding:6px 10px;font-size:12px;color:#161c24;background:var(--blue);border-radius:6px}
|
|
76
|
+
#queueBar>span{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
77
|
+
#queueItems{flex-basis:100%;max-height:140px;overflow:auto;order:3}
|
|
78
|
+
.queue-item{display:flex;align-items:center;gap:8px;padding:3px 0}
|
|
79
|
+
.queue-item span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
80
|
+
|
|
77
81
|
#queueBar button{padding:3px 8px;font-size:12px;color:#161c24;background:#ffffffcc;border-color:#ffffff66}
|
|
78
82
|
/* Tool calls a tool made itself (codemode scripts, MCP batches) stay inside their caller's card. */
|
|
79
83
|
.nested-tools{margin-top:10px;padding-left:10px;border-left:2px solid var(--line)}
|
|
@@ -96,7 +100,9 @@
|
|
|
96
100
|
#activityBar[hidden],.agent-activity[hidden]{display:none}
|
|
97
101
|
.activity-chip{display:inline-flex;align-items:center;gap:6px;min-width:0;max-width:100%}
|
|
98
102
|
.activity-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
103
|
+
.activity-speed{flex:0 0 auto;white-space:nowrap;font-variant-numeric:tabular-nums;color:var(--blue)}
|
|
99
104
|
.elapsed{flex:0 0 auto;font-variant-numeric:tabular-nums;color:var(--yellow)}
|
|
100
105
|
.activity-run{margin-left:auto;color:var(--muted);white-space:nowrap}
|
|
101
106
|
.agent-activity{display:flex;align-items:center;gap:6px;margin-top:5px;font-size:11px;color:#c9d1de}
|
|
102
107
|
.agent-activity .activity-chip{flex:1;min-width:0}
|
|
108
|
+
.performance-list{max-height:58vh;overflow:auto}.performance-run{border:1px solid var(--line);border-radius:6px;margin:8px 0;padding:8px}.performance-run summary{cursor:pointer}.performance-row{margin:9px 0;font-size:12px}.performance-label{display:block;white-space:normal;overflow-wrap:anywhere;margin-bottom:3px}.performance-track{position:relative;height:9px;background:#1a1e26;border-radius:4px}.performance-track i{position:absolute;height:100%;border-radius:4px;min-width:2px}.performance-model{background:var(--blue)}.performance-tool{background:var(--yellow)}.performance-compaction,.performance-retry{background:#b48ead}
|
package/web/voice-pcm.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// AudioWorklet: forward microphone frames as mono float32 PCM without recording or re-encoding.
|
|
2
|
+
class VoicePcmProcessor extends AudioWorkletProcessor {
|
|
3
|
+
constructor() {
|
|
4
|
+
super();
|
|
5
|
+
this.samples = new Float32Array(4096);
|
|
6
|
+
this.offset = 0;
|
|
7
|
+
this.finished = false;
|
|
8
|
+
this.port.onmessage = event => {
|
|
9
|
+
if (event.data?.type !== 'flush' || this.finished) return;
|
|
10
|
+
this.finished = true;
|
|
11
|
+
if (this.offset) {
|
|
12
|
+
const tail = this.samples.slice(0, this.offset).buffer;
|
|
13
|
+
this.port.postMessage(tail, [tail]);
|
|
14
|
+
this.offset = 0;
|
|
15
|
+
}
|
|
16
|
+
this.port.postMessage({type: 'flushed'});
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
process(inputs) {
|
|
20
|
+
if (this.finished) return false;
|
|
21
|
+
const channels = inputs[0];
|
|
22
|
+
if (!channels?.length) return true;
|
|
23
|
+
for (let i = 0; i < channels[0].length; i++) {
|
|
24
|
+
let sample = 0;
|
|
25
|
+
for (const channel of channels) sample += channel[i] / channels.length;
|
|
26
|
+
this.samples[this.offset++] = sample;
|
|
27
|
+
if (this.offset === this.samples.length) {
|
|
28
|
+
this.port.postMessage(this.samples.buffer, [this.samples.buffer]);
|
|
29
|
+
this.samples = new Float32Array(4096);
|
|
30
|
+
this.offset = 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
registerProcessor('voice-pcm', VoicePcmProcessor);
|