@bolloon/bolloon-agent 0.1.35 → 0.1.36
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/.playwright-mcp/page-2026-06-16T07-51-45-706Z.yml +130 -0
- package/.playwright-mcp/page-2026-06-16T07-56-44-116Z.yml +131 -0
- package/Bolloon.md +45 -4
- package/Dive-into/CITATION.cff +17 -0
- package/Dive-into/LICENSE +25 -0
- package/Dive-into/README.md +598 -0
- package/Dive-into/README_zh.md +599 -0
- package/Dive-into/assets/context.png +0 -0
- package/Dive-into/assets/extensibility.png +0 -0
- package/Dive-into/assets/iteration.png +0 -0
- package/Dive-into/assets/layered_architecture.png +0 -0
- package/Dive-into/assets/main_structure.png +0 -0
- package/Dive-into/assets/permission.png +0 -0
- package/Dive-into/assets/session_compact.png +0 -0
- package/Dive-into/assets/subagent.png +0 -0
- package/Dive-into/paper/Dive_into_Claude_Code.pdf +0 -0
- package/README.md +1 -1
- package/dist/agents/p2p-chat-tools.js +6 -6
- package/dist/agents/permission-mode.js +115 -0
- package/dist/agents/pi-sdk.js +435 -25
- package/dist/agents/pre-tool-validator.js +194 -0
- package/dist/agents/workflow-pivot-loop.js +113 -12
- package/dist/bollharness/src/scripts/checks/check_doc_freshness.js +1 -1
- package/dist/bollharness/src/scripts/checks/check_doc_links.js +1 -1
- package/dist/bollharness/src/scripts/context_router.js +1 -1
- package/dist/bollharness/src/scripts/deploy-guard.js +1 -1
- package/dist/bollharness/src/scripts/guard_router.js +1 -1
- package/dist/bollharness/src/scripts/hooks/_hook_output.js +1 -1
- package/dist/bollharness/src/scripts/hooks/risk-tracker.js +1 -1
- package/dist/bollharness-integration/context-router.js +2 -2
- package/dist/bollharness-integration/guard-checker.js +1 -1
- package/dist/bootstrap/bootstrap.js +3 -0
- package/dist/bootstrap/context-collector.js +17 -1
- package/dist/bootstrap/context-hierarchy.js +218 -0
- package/dist/bootstrap/lifecycle-hooks.js +86 -24
- package/dist/bootstrap/project-context.js +10 -3
- package/dist/context-compaction/auto-compact.js +144 -0
- package/dist/context-compaction/budget-gate.js +28 -0
- package/dist/context-compaction/budget-reduce.js +35 -0
- package/dist/context-compaction/context-collapse.js +66 -0
- package/dist/context-compaction/index.js +21 -0
- package/dist/context-compaction/microcompact.js +51 -0
- package/dist/context-compaction/pipeline.js +123 -0
- package/dist/context-compaction/snip.js +45 -0
- package/dist/context-compaction/token-estimator.js +35 -0
- package/dist/context-compaction/types.js +19 -0
- package/dist/heartbeat/HealthMonitor.js +3 -2
- package/dist/index.js +38 -2
- package/dist/llm/llm-judgment-client.js +32 -30
- package/dist/llm/pi-ai.js +103 -16
- package/dist/llm/system-prompt/health.js +129 -0
- package/dist/llm/system-prompt/registry.js +246 -0
- package/dist/llm/system-prompt/strip-hibsml.js +51 -0
- package/dist/llm/tool-manifest/ask_user_input.js +35 -0
- package/dist/llm/tool-manifest/bash.js +23 -0
- package/dist/llm/tool-manifest/create_file.js +24 -0
- package/dist/llm/tool-manifest/fetch_sports_data.js +26 -0
- package/dist/llm/tool-manifest/image_search.js +24 -0
- package/dist/llm/tool-manifest/index.js +69 -0
- package/dist/llm/tool-manifest/mcp.js +43 -0
- package/dist/llm/tool-manifest/message_compose.js +31 -0
- package/dist/llm/tool-manifest/places.js +83 -0
- package/dist/llm/tool-manifest/present_files.js +21 -0
- package/dist/llm/tool-manifest/recipe.js +40 -0
- package/dist/llm/tool-manifest/recommend_apps.js +23 -0
- package/dist/llm/tool-manifest/str_replace.js +27 -0
- package/dist/llm/tool-manifest/types.js +7 -0
- package/dist/llm/tool-manifest/view.js +24 -0
- package/dist/llm/tool-manifest/weather.js +27 -0
- package/dist/llm/tool-manifest/web.js +51 -0
- package/dist/network/p2p-direct.js +23 -0
- package/dist/network/source-intent-broadcaster.js +203 -0
- package/dist/network/source-intent.js +100 -0
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +48 -0
- package/dist/pi-ecosystem-judgment/injection-gate.js +54 -12
- package/dist/pi-ecosystem-judgment/value-injection.js +8 -2
- package/dist/security/context-router-tool.js +1 -1
- package/dist/security/input-scanner.js +223 -0
- package/dist/web/client.js +3677 -4161
- package/dist/web/components/p2p/P2PModal.js +188 -0
- package/dist/web/components/p2p/index.js +264 -226
- package/dist/web/components/p2p/p2p-modal.js +657 -0
- package/dist/web/components/p2p/p2p-tools.js +248 -0
- package/dist/web/index.html +58 -67
- package/dist/web/server.js +299 -117
- package/dist/web/style.css +531 -249
- package/dist/web/ui/message-renderer.js +463 -0
- package/dist/web/ui/step-timeline.js +375 -0
- package/lefthook.yml +4 -0
- package/package.json +1 -1
- package/scripts/auto-evolve-loop.ts +105 -0
- package/scripts/build-web.ts +35 -1
- package/scripts/validate-system-prompt.ts +142 -0
- package/src/agents/p2p-chat-tools.ts +6 -6
- package/src/agents/permission-mode.ts +127 -0
- package/src/agents/pi-sdk.ts +461 -28
- package/src/agents/pre-tool-validator.ts +213 -0
- package/src/agents/workflow-pivot-loop.ts +110 -19
- package/src/bollharness/CLAUDE.md +1 -1
- package/src/bollharness/README.md +2 -2
- package/src/bollharness/README.zh-CN.md +2 -2
- package/src/bollharness/reference/boll-reference/scripts/hooks/find-boll-root.sh +2 -2
- package/src/bollharness/scripts/context-fragments/truth-source-hierarchy.md +2 -2
- package/src/bollharness/scripts/context-fragments/version-sources.md +1 -1
- package/src/bollharness/scripts/hooks/find-project-root.sh +4 -4
- package/src/bollharness/src/scripts/checks/check_doc_freshness.ts +1 -1
- package/src/bollharness/src/scripts/checks/check_doc_links.ts +1 -1
- package/src/bollharness/src/scripts/context_router.ts +1 -1
- package/src/bollharness/src/scripts/deploy-guard.ts +1 -1
- package/src/bollharness/src/scripts/guard_router.ts +1 -1
- package/src/bollharness/src/scripts/hooks/_hook_output.ts +1 -1
- package/src/bollharness/src/scripts/hooks/risk-tracker.ts +1 -1
- package/src/bollharness/templates/scaffold/.gitignore.append +1 -1
- package/src/bollharness/templates/scaffold/CLAUDE.md +2 -2
- package/src/bollharness-integration/context-router.ts +2 -2
- package/src/bollharness-integration/guard-checker.ts +1 -1
- package/src/bootstrap/bootstrap.ts +3 -0
- package/src/bootstrap/context-collector.ts +30 -1
- package/src/bootstrap/context-hierarchy.ts +283 -0
- package/src/bootstrap/lifecycle-hooks.ts +139 -26
- package/src/bootstrap/project-context.ts +11 -3
- package/src/context-compaction/auto-compact.ts +153 -0
- package/src/context-compaction/budget-gate.ts +32 -0
- package/src/context-compaction/budget-reduce.ts +37 -0
- package/src/context-compaction/context-collapse.ts +72 -0
- package/src/context-compaction/index.ts +24 -0
- package/src/context-compaction/microcompact.ts +54 -0
- package/src/context-compaction/pipeline.ts +133 -0
- package/src/context-compaction/snip.ts +51 -0
- package/src/context-compaction/token-estimator.ts +36 -0
- package/src/context-compaction/types.ts +99 -0
- package/src/heartbeat/HealthMonitor.ts +3 -2
- package/src/index.ts +36 -2
- package/src/llm/llm-judgment-client.ts +36 -35
- package/src/llm/pi-ai.ts +111 -16
- package/src/llm/system-prompt/health.ts +159 -0
- package/src/llm/system-prompt/layers/channel/local.md +14 -0
- package/src/llm/system-prompt/layers/channel/p2p-agent.md +18 -0
- package/src/llm/system-prompt/layers/channel/p2p-visitor.md +19 -0
- package/src/llm/system-prompt/layers/core/artifacts_storage.md +89 -0
- package/src/llm/system-prompt/layers/core/evenhandedness.md +21 -0
- package/src/llm/system-prompt/layers/core/hibs_reminders.md +15 -0
- package/src/llm/system-prompt/layers/core/identity.md +37 -0
- package/src/llm/system-prompt/layers/core/knowledge.md +17 -0
- package/src/llm/system-prompt/layers/core/memory_system.md +12 -0
- package/src/llm/system-prompt/layers/core/network_filesystem.md +28 -0
- package/src/llm/system-prompt/layers/core/refusal.md +37 -0
- package/src/llm/system-prompt/layers/core/tone.md +31 -0
- package/src/llm/system-prompt/layers/core/tools.thin.md +13 -0
- package/src/llm/system-prompt/layers/core/wellbeing.md +41 -0
- package/src/llm/system-prompt/layers/role/architect.md +20 -0
- package/src/llm/system-prompt/layers/role/expert.md +19 -0
- package/src/llm/system-prompt/layers/role/implementer.md +15 -0
- package/src/llm/system-prompt/layers/role/security.md +15 -0
- package/src/llm/system-prompt/layers/tool/artifacts.md +72 -0
- package/src/llm/system-prompt/layers/tool/bash.md +25 -0
- package/src/llm/system-prompt/layers/tool/hibs_api.md +171 -0
- package/src/llm/system-prompt/layers/tool/image_search.md +70 -0
- package/src/llm/system-prompt/layers/tool/manifest.md +89 -0
- package/src/llm/system-prompt/layers/tool/mcp_apps.md +53 -0
- package/src/llm/system-prompt/layers/tool/web_search.md +83 -0
- package/src/llm/system-prompt/registry.ts +325 -0
- package/src/llm/system-prompt/strip-hibsml.ts +52 -0
- package/src/llm/tool-manifest/ask_user_input.ts +37 -0
- package/src/llm/tool-manifest/bash.ts +25 -0
- package/src/llm/tool-manifest/create_file.ts +26 -0
- package/src/llm/tool-manifest/fetch_sports_data.ts +28 -0
- package/src/llm/tool-manifest/image_search.ts +26 -0
- package/src/llm/tool-manifest/index.ts +88 -0
- package/src/llm/tool-manifest/mcp.ts +46 -0
- package/src/llm/tool-manifest/message_compose.ts +33 -0
- package/src/llm/tool-manifest/places.ts +86 -0
- package/src/llm/tool-manifest/present_files.ts +23 -0
- package/src/llm/tool-manifest/recipe.ts +42 -0
- package/src/llm/tool-manifest/recommend_apps.ts +25 -0
- package/src/llm/tool-manifest/str_replace.ts +29 -0
- package/src/llm/tool-manifest/types.ts +52 -0
- package/src/llm/tool-manifest/view.ts +26 -0
- package/src/llm/tool-manifest/weather.ts +29 -0
- package/src/llm/tool-manifest/web.ts +54 -0
- package/src/network/p2p-direct.ts +22 -0
- package/src/network/source-intent-broadcaster.ts +242 -0
- package/src/network/source-intent.ts +167 -0
- package/src/security/context-router-tool.ts +1 -1
- package/src/security/input-scanner.ts +287 -0
- package/src/web/client.js +271 -2817
- package/src/web/client.ts +4326 -0
- package/src/web/index.html +58 -67
- package/src/web/server.ts +301 -141
- package/src/web/style.css +531 -249
- package/src/web/ui/message-renderer.ts +540 -0
- package/src/web/ui/step-timeline.ts +394 -0
- package/test-results/.last-run.json +6 -0
- package/test-results/src-test-web-loop-status-b-5734c-atus-bar-/346/230/276/347/244/272-spinner-/344/270/215/346/230/276/347/244/272/345/276/252/347/216/257/350/256/241/346/225/260/error-context.md +285 -0
package/dist/web/server.js
CHANGED
|
@@ -31,15 +31,15 @@ function resolveWebRoot() {
|
|
|
31
31
|
}
|
|
32
32
|
const d = _baseDirname;
|
|
33
33
|
const candidates = [
|
|
34
|
-
path.join(d), // dist/web
|
|
35
|
-
path.join(d
|
|
34
|
+
path.join(d, '..', '..', 'dist', 'web'), // 2026-06-15 优先: src/web → dist/web (build 产物)
|
|
35
|
+
path.join(d), // dist/web (npm 跑时)
|
|
36
36
|
path.join(d, '..', 'web'), // dist/ → web/ 兄弟
|
|
37
37
|
];
|
|
38
38
|
for (const c of candidates) {
|
|
39
39
|
if (fsSync.existsSync(path.join(c, 'index.html')))
|
|
40
40
|
return c;
|
|
41
41
|
}
|
|
42
|
-
return candidates[
|
|
42
|
+
return candidates[0];
|
|
43
43
|
}
|
|
44
44
|
const webRoot = resolveWebRoot();
|
|
45
45
|
console.log(`[web] webRoot = ${webRoot}`);
|
|
@@ -140,7 +140,6 @@ async function saveTheme(theme, agentId) {
|
|
|
140
140
|
}
|
|
141
141
|
// ==================== Task Queue & Workflow System ====================
|
|
142
142
|
const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
|
|
143
|
-
const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
|
|
144
143
|
async function loadTaskQueue() {
|
|
145
144
|
try {
|
|
146
145
|
const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
|
|
@@ -153,33 +152,6 @@ async function loadTaskQueue() {
|
|
|
153
152
|
async function saveTaskQueue(tasks) {
|
|
154
153
|
await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
|
|
155
154
|
}
|
|
156
|
-
async function loadWorkflowState(channelId) {
|
|
157
|
-
try {
|
|
158
|
-
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
159
|
-
const states = JSON.parse(data);
|
|
160
|
-
return states.find(s => s.channelId === channelId) || null;
|
|
161
|
-
}
|
|
162
|
-
catch {
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
async function saveWorkflowState(state) {
|
|
167
|
-
try {
|
|
168
|
-
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
169
|
-
const states = JSON.parse(data);
|
|
170
|
-
const index = states.findIndex(s => s.channelId === state.channelId);
|
|
171
|
-
if (index >= 0) {
|
|
172
|
-
states[index] = state;
|
|
173
|
-
}
|
|
174
|
-
else {
|
|
175
|
-
states.push(state);
|
|
176
|
-
}
|
|
177
|
-
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
155
|
let isExecutingTask = false;
|
|
184
156
|
let executionTaskId = null;
|
|
185
157
|
async function executeTask(task, channelId) {
|
|
@@ -221,32 +193,6 @@ async function executeTask(task, channelId) {
|
|
|
221
193
|
result = `📝 文档总结:\n\n${summary.summary}`;
|
|
222
194
|
}
|
|
223
195
|
break;
|
|
224
|
-
case 'workflow':
|
|
225
|
-
// 执行多步骤工作流
|
|
226
|
-
if (task.steps && task.steps.length > 0) {
|
|
227
|
-
let loopCount = 0;
|
|
228
|
-
for (let i = 0; i < task.steps.length; i++) {
|
|
229
|
-
// 广播循环开始
|
|
230
|
-
loopCount++;
|
|
231
|
-
broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
|
|
232
|
-
task.steps[i].status = 'running';
|
|
233
|
-
broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
|
|
234
|
-
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
|
|
235
|
-
// 执行步骤 - 模拟流式输出
|
|
236
|
-
for (let j = 0; j < 3; j++) {
|
|
237
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
238
|
-
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
|
|
239
|
-
}
|
|
240
|
-
task.steps[i].status = 'completed';
|
|
241
|
-
task.progress = Math.round(((i + 1) / task.steps.length) * 100);
|
|
242
|
-
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
|
|
243
|
-
broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
|
|
244
|
-
broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
|
|
245
|
-
}
|
|
246
|
-
result = '✅ 工作流执行完成';
|
|
247
|
-
broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
|
|
248
|
-
}
|
|
249
|
-
break;
|
|
250
196
|
default:
|
|
251
197
|
result = '未知任务类型';
|
|
252
198
|
}
|
|
@@ -632,7 +578,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
632
578
|
const { getMinimax } = await import('../constraints/index.js');
|
|
633
579
|
const llm = getMinimax();
|
|
634
580
|
// v3 新增: 在 prompt 头部标记"这是远端访客", 让 AI 知道对方不是自己 owner
|
|
635
|
-
const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "
|
|
581
|
+
const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "用户".\n\n`;
|
|
636
582
|
// v3 新增: 也注入 channel 目录给 LLM (B 的 channel 也可以 @-mention 其他)
|
|
637
583
|
let dirHint = '';
|
|
638
584
|
const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
|
|
@@ -654,7 +600,10 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
654
600
|
}
|
|
655
601
|
dirHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session.\n\n';
|
|
656
602
|
}
|
|
657
|
-
|
|
603
|
+
// 2026-06-15: P2P 远端访客路径也用显式 marker 包裹 text
|
|
604
|
+
// 2026-06-15 二次修: 把 text 放在最前 (与主路径 server.ts:1868 对齐),
|
|
605
|
+
// 避免 LLM 被 judgmentHint 末尾的 "..." 误判为整个 input 被截断
|
|
606
|
+
const fullPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${visitorHint}${dirHint}${judgmentHint}\n`;
|
|
658
607
|
let fullResponse = '';
|
|
659
608
|
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
660
609
|
let lastFlushAt = 0;
|
|
@@ -1475,6 +1424,35 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1475
1424
|
broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
|
|
1476
1425
|
console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
|
|
1477
1426
|
}
|
|
1427
|
+
else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
|
|
1428
|
+
// 2026-06-15: 步骤状态机事件 — 原样转发 (前端 step-timeline 组件订阅)
|
|
1429
|
+
broadcast({
|
|
1430
|
+
type: event.type,
|
|
1431
|
+
tool: event.tool,
|
|
1432
|
+
content: event.content,
|
|
1433
|
+
success: event.success,
|
|
1434
|
+
output: event.output,
|
|
1435
|
+
error: event.error,
|
|
1436
|
+
args: event.args,
|
|
1437
|
+
}, channelId);
|
|
1438
|
+
// 2026-06-16: 累积 step 到 runState, 供 /api/loop/inspect 读取
|
|
1439
|
+
try {
|
|
1440
|
+
if (event.type === 'step_done' || event.type === 'step_error') {
|
|
1441
|
+
const rs = channelRunState.get(channelId);
|
|
1442
|
+
if (rs) {
|
|
1443
|
+
if (!rs.lastSteps)
|
|
1444
|
+
rs.lastSteps = [];
|
|
1445
|
+
rs.lastSteps.push({
|
|
1446
|
+
name: String(event.tool || event.content || 'step').slice(0, 60),
|
|
1447
|
+
status: event.type === 'step_error' ? 'failed' : (event.success === false ? 'failed' : 'ok'),
|
|
1448
|
+
durationMs: typeof event.durationMs === 'number' ? event.durationMs : undefined,
|
|
1449
|
+
output: event.output ? String(event.output).slice(0, 800) : (event.error ? String(event.error).slice(0, 800) : undefined),
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
catch { /* non-fatal */ }
|
|
1455
|
+
}
|
|
1478
1456
|
else if (event.type === 'error') {
|
|
1479
1457
|
broadcast({ type: 'error', content: event.content }, channelId);
|
|
1480
1458
|
}
|
|
@@ -1485,7 +1463,7 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1485
1463
|
if (realChannelDid)
|
|
1486
1464
|
contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
|
|
1487
1465
|
// v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
|
|
1488
|
-
contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "
|
|
1466
|
+
contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
|
|
1489
1467
|
if (boundWalletAddress) {
|
|
1490
1468
|
contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
|
|
1491
1469
|
}
|
|
@@ -1632,7 +1610,13 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1632
1610
|
if (contextHint)
|
|
1633
1611
|
contextHint += '\n';
|
|
1634
1612
|
try {
|
|
1635
|
-
|
|
1613
|
+
// 2026-06-15: 把 user text 单独 marker 包起来, LLM 不会被 8K+ 的 system context 吞掉
|
|
1614
|
+
// (之前 contextHint + text 拼成一整段当 user role, 24 字符的 user input 埋在 8K+ 里看不出)
|
|
1615
|
+
// 修法: contextHint 当 "背景信息", text 当 "本轮用户请求" — 显式 marker 让 LLM 区分
|
|
1616
|
+
// 2026-06-15 二次修: 把 text 放在最前 (LLM 看到 input 第一眼是 user text, 不会被 judgmentHint 末尾
|
|
1617
|
+
// 的 "..." 误判为整个 input 截断)
|
|
1618
|
+
const markedPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
|
|
1619
|
+
fullResponse = await agent.promptStream(markedPrompt, streamCallback, runState.abortController?.signal, channelId);
|
|
1636
1620
|
}
|
|
1637
1621
|
catch (err) {
|
|
1638
1622
|
// abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
|
|
@@ -1747,7 +1731,7 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1747
1731
|
function getOrCreateRunState(channelId) {
|
|
1748
1732
|
let s = channelRunState.get(channelId);
|
|
1749
1733
|
if (!s) {
|
|
1750
|
-
s = { running: false, queue: [], abortController: null };
|
|
1734
|
+
s = { running: false, queue: [], abortController: null, lastSteps: [], lastSummary: '', lastFinalReply: '', lastTokens: {} };
|
|
1751
1735
|
channelRunState.set(channelId, s);
|
|
1752
1736
|
}
|
|
1753
1737
|
return s;
|
|
@@ -2463,13 +2447,27 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2463
2447
|
else if (event.type === 'status' || event.type === 'tool') {
|
|
2464
2448
|
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
2465
2449
|
}
|
|
2450
|
+
else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
|
|
2451
|
+
// 2026-06-15: 步骤状态机事件 — 原样转发
|
|
2452
|
+
broadcast({
|
|
2453
|
+
type: event.type,
|
|
2454
|
+
tool: event.tool,
|
|
2455
|
+
content: event.content,
|
|
2456
|
+
success: event.success,
|
|
2457
|
+
output: event.output,
|
|
2458
|
+
error: event.error,
|
|
2459
|
+
args: event.args,
|
|
2460
|
+
}, channelId);
|
|
2461
|
+
}
|
|
2466
2462
|
else if (event.type === 'error') {
|
|
2467
2463
|
broadcast({ type: 'error', content: event.content }, channelId);
|
|
2468
2464
|
}
|
|
2469
2465
|
};
|
|
2470
2466
|
// 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
|
|
2467
|
+
// 2026-06-15: 同 /message 路径, 用显式 marker 包裹 userMessage, 避免 LLM 把它当背景信息
|
|
2471
2468
|
const regenHint = await buildJudgmentHint(channel, channelId);
|
|
2472
|
-
|
|
2469
|
+
const markedRegen = `${regenHint}\n\n【本轮用户请求】\n${userMessage}\n【请求结束】\n`;
|
|
2470
|
+
fullResponse = await agent.promptStream(markedRegen, streamCallback, undefined, channelId);
|
|
2473
2471
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
2474
2472
|
// 更新 session
|
|
2475
2473
|
const existingSession = await loadSession(channelId, currentSessionId);
|
|
@@ -2639,42 +2637,6 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2639
2637
|
res.status(500).json({ error: err.message });
|
|
2640
2638
|
}
|
|
2641
2639
|
});
|
|
2642
|
-
// 创建并执行工作流
|
|
2643
|
-
app.post('/api/workflow', async (req, res) => {
|
|
2644
|
-
try {
|
|
2645
|
-
const { channelId, title, steps } = req.body;
|
|
2646
|
-
if (!channelId || !steps || !Array.isArray(steps)) {
|
|
2647
|
-
return res.status(400).json({ error: 'channelId and steps required' });
|
|
2648
|
-
}
|
|
2649
|
-
const tasks = await loadTaskQueue();
|
|
2650
|
-
const task = {
|
|
2651
|
-
id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
2652
|
-
type: 'workflow',
|
|
2653
|
-
title: title || '工作流',
|
|
2654
|
-
description: `包含 ${steps.length} 个步骤的工作流`,
|
|
2655
|
-
status: 'pending',
|
|
2656
|
-
progress: 0,
|
|
2657
|
-
createdAt: new Date().toISOString(),
|
|
2658
|
-
updatedAt: new Date().toISOString(),
|
|
2659
|
-
steps: steps.map((s, i) => ({
|
|
2660
|
-
id: `step_${i}`,
|
|
2661
|
-
name: s,
|
|
2662
|
-
status: 'pending'
|
|
2663
|
-
})),
|
|
2664
|
-
currentStep: 0
|
|
2665
|
-
};
|
|
2666
|
-
tasks.push(task);
|
|
2667
|
-
await saveTaskQueue(tasks);
|
|
2668
|
-
// 自动开始执行
|
|
2669
|
-
if (!isExecutingTask) {
|
|
2670
|
-
executeTask(task, channelId);
|
|
2671
|
-
}
|
|
2672
|
-
res.json({ ok: true, task });
|
|
2673
|
-
}
|
|
2674
|
-
catch (err) {
|
|
2675
|
-
res.status(500).json({ error: err.message });
|
|
2676
|
-
}
|
|
2677
|
-
});
|
|
2678
2640
|
// ==================== LLM 配置 API ====================
|
|
2679
2641
|
// 获取所有 LLM 配置
|
|
2680
2642
|
app.get('/api/llm-config', async (req, res) => {
|
|
@@ -3827,7 +3789,7 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
3827
3789
|
res.status(500).json({ error: err.message });
|
|
3828
3790
|
}
|
|
3829
3791
|
});
|
|
3830
|
-
//
|
|
3792
|
+
// 用户审阅: 批准 draft
|
|
3831
3793
|
app.post('/api/chat/approve', async (req, res) => {
|
|
3832
3794
|
try {
|
|
3833
3795
|
const { messageId, peerDID, finalText } = req.body || {};
|
|
@@ -3841,7 +3803,7 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
3841
3803
|
res.status(500).json({ error: err.message });
|
|
3842
3804
|
}
|
|
3843
3805
|
});
|
|
3844
|
-
//
|
|
3806
|
+
// 用户审阅: 丢弃 draft
|
|
3845
3807
|
app.post('/api/chat/dismiss', async (req, res) => {
|
|
3846
3808
|
try {
|
|
3847
3809
|
const { messageId, peerDID } = req.body || {};
|
|
@@ -4454,6 +4416,42 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
4454
4416
|
res.status(500).json({ error: err.message });
|
|
4455
4417
|
}
|
|
4456
4418
|
});
|
|
4419
|
+
// ============================================================
|
|
4420
|
+
// system-prompt health (P-Action 2 — Harness Gardening)
|
|
4421
|
+
// 返回每层 lifecycle 状态: ok | stale | overdue-review | missing-frontmatter | dynamic
|
|
4422
|
+
// query: ?activeOnly=1 → 只返回当前 context 激活的层
|
|
4423
|
+
// ============================================================
|
|
4424
|
+
app.get('/api/prompt/health', async (req, res) => {
|
|
4425
|
+
try {
|
|
4426
|
+
const { listLayers } = await import('../llm/system-prompt/registry.js');
|
|
4427
|
+
const { evaluateLayers, markActive } = await import('../llm/system-prompt/health.js');
|
|
4428
|
+
const all = listLayers();
|
|
4429
|
+
const baseReport = evaluateLayers(all);
|
|
4430
|
+
// 如果 query 里有 activeOnly, 跑一次 assembleSystemPrompt 拿激活列表
|
|
4431
|
+
if (String(req.query.activeOnly ?? '') === '1') {
|
|
4432
|
+
const { assembleSystemPrompt } = await import('../llm/system-prompt/registry.js');
|
|
4433
|
+
const channel = String(req.query.channel ?? 'local');
|
|
4434
|
+
const role = req.query.role;
|
|
4435
|
+
const tool = req.query.tool;
|
|
4436
|
+
try {
|
|
4437
|
+
const r = await assembleSystemPrompt({ channel, role, tool });
|
|
4438
|
+
const activeIds = new Set(r.layerIds);
|
|
4439
|
+
res.json(markActive(baseReport, activeIds));
|
|
4440
|
+
}
|
|
4441
|
+
catch (err) {
|
|
4442
|
+
console.warn('[prompt-health] assembleSystemPrompt failed (silent, returning base report):', err);
|
|
4443
|
+
res.json(baseReport);
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
else {
|
|
4447
|
+
res.json(baseReport);
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
catch (err) {
|
|
4451
|
+
console.error('[prompt-health] failed:', err);
|
|
4452
|
+
res.status(500).json({ error: err.message });
|
|
4453
|
+
}
|
|
4454
|
+
});
|
|
4457
4455
|
// 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
|
|
4458
4456
|
// body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
|
|
4459
4457
|
// query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
|
|
@@ -4919,25 +4917,192 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
4919
4917
|
const { getEventHistory } = await import('../heartbeat/self-improve-bus.js');
|
|
4920
4918
|
res.json(getEventHistory());
|
|
4921
4919
|
});
|
|
4920
|
+
// ============================================================
|
|
4921
|
+
// Permission Mode (P2.2 — UI 暴露开关)
|
|
4922
|
+
// 优先级: 运行时 session 覆盖 > env BOLLOON_PERM_MODE > 'default'
|
|
4923
|
+
// 运行时覆盖存在 ~/.bolloon/sessions/permission-mode.json, 每次 promptStream 入口读取
|
|
4924
|
+
// ============================================================
|
|
4925
|
+
const PERM_MODE_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode.json');
|
|
4926
|
+
function readPermModeOverride() {
|
|
4927
|
+
try {
|
|
4928
|
+
// 同步读, 文件很小
|
|
4929
|
+
const raw = fsSync.readFileSync(PERM_MODE_FILE, 'utf-8');
|
|
4930
|
+
const obj = JSON.parse(raw);
|
|
4931
|
+
if (obj && typeof obj.mode === 'string') {
|
|
4932
|
+
return { mode: obj.mode, ts: obj.ts || new Date().toISOString() };
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
catch { /* 不存在 = 无 override */ }
|
|
4936
|
+
return null;
|
|
4937
|
+
}
|
|
4938
|
+
function writePermModeOverride(mode) {
|
|
4939
|
+
try {
|
|
4940
|
+
const dir = path.dirname(PERM_MODE_FILE);
|
|
4941
|
+
fsSync.mkdirSync(dir, { recursive: true });
|
|
4942
|
+
fsSync.writeFileSync(PERM_MODE_FILE, JSON.stringify({ mode, ts: new Date().toISOString() }, null, 2), 'utf-8');
|
|
4943
|
+
}
|
|
4944
|
+
catch (err) {
|
|
4945
|
+
console.warn('[server] writePermModeOverride failed:', err);
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
// 读当前生效 mode (runtime override > env > default)
|
|
4949
|
+
app.get('/api/permission-mode', async (_req, res) => {
|
|
4950
|
+
const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
|
|
4951
|
+
const override = readPermModeOverride();
|
|
4952
|
+
const envMode = process.env.BOLLOON_PERM_MODE || null;
|
|
4953
|
+
const effective = resolvePermissionMode();
|
|
4954
|
+
res.json({
|
|
4955
|
+
effective,
|
|
4956
|
+
override: override?.mode || null,
|
|
4957
|
+
overrideTs: override?.ts || null,
|
|
4958
|
+
env: envMode,
|
|
4959
|
+
allowed: ALL_PERMISSION_MODES,
|
|
4960
|
+
description: {
|
|
4961
|
+
default: '每次工具调用询问; shell 走 shell-guard',
|
|
4962
|
+
acceptEdits: 'edit_*/write_* 跳过黑名单; shell 仍走 shell-guard',
|
|
4963
|
+
bypassPermissions: '非 shell 全部放行; shell 永远走 shell-guard (硬约束)',
|
|
4964
|
+
},
|
|
4965
|
+
});
|
|
4966
|
+
});
|
|
4967
|
+
// 设 runtime override (存盘, 下次 promptStream 入口读取生效)
|
|
4968
|
+
app.post('/api/permission-mode', async (req, res) => {
|
|
4969
|
+
const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
|
|
4970
|
+
const mode = String(req.body?.mode || '');
|
|
4971
|
+
if (!ALL_PERMISSION_MODES.includes(mode)) {
|
|
4972
|
+
return res.status(400).json({
|
|
4973
|
+
error: `Invalid mode. Allowed: ${ALL_PERMISSION_MODES.join(', ')}`,
|
|
4974
|
+
allowed: ALL_PERMISSION_MODES,
|
|
4975
|
+
});
|
|
4976
|
+
}
|
|
4977
|
+
const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
|
|
4978
|
+
writePermModeOverride(mode);
|
|
4979
|
+
// 写历史 (append-only JSONL, 跟 bolloon 其他 audit 一致)
|
|
4980
|
+
try {
|
|
4981
|
+
const HISTORY_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode-history.jsonl');
|
|
4982
|
+
fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
4983
|
+
fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
|
|
4984
|
+
ts: new Date().toISOString(),
|
|
4985
|
+
from: oldMode,
|
|
4986
|
+
to: mode,
|
|
4987
|
+
source: 'api',
|
|
4988
|
+
}) + '\n', 'utf-8');
|
|
4989
|
+
}
|
|
4990
|
+
catch { /* 历史写失败不阻塞主流程 */ }
|
|
4991
|
+
console.log(`[server] permission-mode override set to "${mode}" via API (was "${oldMode}")`);
|
|
4992
|
+
res.json({
|
|
4993
|
+
ok: true,
|
|
4994
|
+
mode,
|
|
4995
|
+
previousMode: oldMode,
|
|
4996
|
+
ts: new Date().toISOString(),
|
|
4997
|
+
note: '新 mode 在下一次 promptStream 入口生效 (不打断当前对话)',
|
|
4998
|
+
});
|
|
4999
|
+
});
|
|
5000
|
+
// 取消 runtime override, 回到 env 或 default
|
|
5001
|
+
app.delete('/api/permission-mode', async (_req, res) => {
|
|
5002
|
+
const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
|
|
5003
|
+
try {
|
|
5004
|
+
if (fsSync.existsSync(PERM_MODE_FILE))
|
|
5005
|
+
fsSync.unlinkSync(PERM_MODE_FILE);
|
|
5006
|
+
}
|
|
5007
|
+
catch (err) {
|
|
5008
|
+
console.warn('[server] delete perm-mode override failed:', err);
|
|
5009
|
+
}
|
|
5010
|
+
// 写历史
|
|
5011
|
+
try {
|
|
5012
|
+
const HISTORY_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode-history.jsonl');
|
|
5013
|
+
fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
5014
|
+
fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
|
|
5015
|
+
ts: new Date().toISOString(),
|
|
5016
|
+
from: oldMode,
|
|
5017
|
+
to: 'env-or-default',
|
|
5018
|
+
source: 'api',
|
|
5019
|
+
action: 'delete-override',
|
|
5020
|
+
}) + '\n', 'utf-8');
|
|
5021
|
+
}
|
|
5022
|
+
catch { /* ignore */ }
|
|
5023
|
+
res.json({ ok: true, note: '已删除 runtime override, 回到 env / default' });
|
|
5024
|
+
});
|
|
5025
|
+
// 2026-06-16: 循环检查 — GET /api/loop/inspect?channelId=...
|
|
5026
|
+
// 返回最近一轮 ReAct loop 的产出: 步骤 / 工具调用 / 压缩摘要 / 最终回复 / token 用量.
|
|
5027
|
+
// 用于前端 status bar 的「✓ 检查」按钮弹 modal.
|
|
5028
|
+
app.get('/api/loop/inspect', async (req, res) => {
|
|
5029
|
+
try {
|
|
5030
|
+
const channelId = String(req.query.channelId || '');
|
|
5031
|
+
if (!channelId)
|
|
5032
|
+
return res.status(400).json({ error: 'channelId required' });
|
|
5033
|
+
const s = channelRunState.get(channelId);
|
|
5034
|
+
if (!s)
|
|
5035
|
+
return res.json({ summary: '该 channel 无活跃 loop', steps: [], finalReply: '' });
|
|
5036
|
+
const steps = (s.lastSteps || []).map((st) => ({
|
|
5037
|
+
name: st.name || st.tool || 'step',
|
|
5038
|
+
status: st.status || 'completed',
|
|
5039
|
+
durationMs: st.durationMs,
|
|
5040
|
+
output: st.output || st.result || '',
|
|
5041
|
+
}));
|
|
5042
|
+
res.json({
|
|
5043
|
+
summary: s.lastSummary || (s.running ? 'loop 仍在运行中' : 'loop 已结束'),
|
|
5044
|
+
steps,
|
|
5045
|
+
finalReply: s.lastFinalReply || '',
|
|
5046
|
+
tokens: s.lastTokens || {},
|
|
5047
|
+
});
|
|
5048
|
+
}
|
|
5049
|
+
catch (err) {
|
|
5050
|
+
res.status(500).json({ error: err.message });
|
|
5051
|
+
}
|
|
5052
|
+
});
|
|
5053
|
+
// 历史 (类似 self-improve history, 供前端 timeline)
|
|
5054
|
+
app.get('/api/permission-mode/history', async (_req, res) => {
|
|
5055
|
+
const HISTORY_FILE = path.join(process.env.HOME || os.homedir() || '/tmp', '.bolloon', 'sessions', 'permission-mode-history.jsonl');
|
|
5056
|
+
try {
|
|
5057
|
+
if (!fsSync.existsSync(HISTORY_FILE))
|
|
5058
|
+
return res.json([]);
|
|
5059
|
+
const lines = fsSync.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean);
|
|
5060
|
+
const entries = lines.map((l) => {
|
|
5061
|
+
try {
|
|
5062
|
+
return JSON.parse(l);
|
|
5063
|
+
}
|
|
5064
|
+
catch {
|
|
5065
|
+
return null;
|
|
5066
|
+
}
|
|
5067
|
+
}).filter(Boolean);
|
|
5068
|
+
res.json(entries.slice(-50)); // 最近 50 条
|
|
5069
|
+
}
|
|
5070
|
+
catch (err) {
|
|
5071
|
+
res.json([]);
|
|
5072
|
+
}
|
|
5073
|
+
});
|
|
4922
5074
|
// 健康检查错误数 ≥ 2 -> 触发自改信号
|
|
5075
|
+
// 2026-06-16: 自迭代默认关 (用户模式), 仅 BOLLOON_DEV_MODE=1 或 selfImprove=true 启动项才装 callback
|
|
4923
5076
|
if (healthMonitor) {
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
5077
|
+
if (selfImproveEnabled) {
|
|
5078
|
+
healthMonitor.startPeriodicCheck(60000, (status) => {
|
|
5079
|
+
const errorCount = Object.values(status.checks)
|
|
5080
|
+
.filter((c) => c.status === 'error').length;
|
|
5081
|
+
if (errorCount >= 2) {
|
|
5082
|
+
import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
|
|
5083
|
+
const failedKeys = Object.entries(status.checks)
|
|
5084
|
+
.filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
|
|
5085
|
+
reportSelfImproveEvent({
|
|
5086
|
+
kind: 'silent-timeout',
|
|
5087
|
+
details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
|
|
5088
|
+
});
|
|
4934
5089
|
});
|
|
4935
|
-
}
|
|
4936
|
-
}
|
|
4937
|
-
}
|
|
5090
|
+
}
|
|
5091
|
+
});
|
|
5092
|
+
}
|
|
5093
|
+
else {
|
|
5094
|
+
// 用户模式: 只跑监控不打自改信号, 心跳仍工作
|
|
5095
|
+
healthMonitor.startPeriodicCheck(60000);
|
|
5096
|
+
console.log('[24h] Health monitor periodic check (no self-improve)');
|
|
5097
|
+
}
|
|
5098
|
+
}
|
|
5099
|
+
// 安装自改总线 -> SSE 桥 (开发者模式才装, 用户模式靠 /api/self-improve/trigger 手动触发)
|
|
5100
|
+
if (selfImproveEnabled) {
|
|
5101
|
+
void installSelfImproveHook();
|
|
5102
|
+
}
|
|
5103
|
+
else {
|
|
5104
|
+
console.log('[self-improve] 用户模式, installSelfImproveHook 跳过 (可用 POST /api/self-improve/trigger 手动触发)');
|
|
4938
5105
|
}
|
|
4939
|
-
// 安装自改总线 -> SSE 桥
|
|
4940
|
-
void installSelfImproveHook();
|
|
4941
5106
|
// 端口冲突时自动找下一个可用端口(最多 10 次),避免 EADDRINUSE 直接崩溃
|
|
4942
5107
|
return new Promise((resolve, reject) => {
|
|
4943
5108
|
const maxAttempts = 10;
|
|
@@ -4957,11 +5122,28 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
4957
5122
|
console.log('服务器已监听');
|
|
4958
5123
|
// 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
|
|
4959
5124
|
void installChatBusHook();
|
|
5125
|
+
// 2026-06-16: ping 改为 data: {"type":"ping"} — 之前是 SSE 注释格式 (: ping\n\n),
|
|
5126
|
+
// 浏览器 EventSource 不触发 onmessage, 客户端 60s 阈值 (现已 30s) 误判死链.
|
|
5127
|
+
// 改后前端 onmessage 收到 ping 就重置 lastEventTime, 真死链才 30s 后重建.
|
|
4960
5128
|
setInterval(() => {
|
|
4961
5129
|
for (const client of sseClients) {
|
|
4962
|
-
|
|
5130
|
+
try {
|
|
5131
|
+
client.res.write('data: {"type":"ping"}\n\n');
|
|
5132
|
+
}
|
|
5133
|
+
catch (err) {
|
|
5134
|
+
// socket 已断, 跳过 — client 端 onerror 会触发重连
|
|
5135
|
+
console.warn('[SSE ping] write 失败, 跳过该客户端:', err.message);
|
|
5136
|
+
}
|
|
4963
5137
|
}
|
|
4964
5138
|
}, 30000);
|
|
5139
|
+
// 2026-06-16: 全局捕获 socket error 事件, 避免未处理 EPIPE/ETIMEDOUT 让进程崩
|
|
5140
|
+
currentServer.on('clientError', (err, socket) => {
|
|
5141
|
+
console.warn('[server] clientError:', err.code, err.message);
|
|
5142
|
+
try {
|
|
5143
|
+
socket.end();
|
|
5144
|
+
}
|
|
5145
|
+
catch { }
|
|
5146
|
+
});
|
|
4965
5147
|
resolve({ app, server: currentServer, port: currentPort });
|
|
4966
5148
|
});
|
|
4967
5149
|
};
|