@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/src/web/server.ts
CHANGED
|
@@ -40,14 +40,14 @@ function resolveWebRoot(): string {
|
|
|
40
40
|
}
|
|
41
41
|
const d = _baseDirname;
|
|
42
42
|
const candidates = [
|
|
43
|
-
path.join(d),
|
|
44
|
-
path.join(d
|
|
43
|
+
path.join(d, '..', '..', 'dist', 'web'), // 2026-06-15 优先: src/web → dist/web (build 产物)
|
|
44
|
+
path.join(d), // dist/web (npm 跑时)
|
|
45
45
|
path.join(d, '..', 'web'), // dist/ → web/ 兄弟
|
|
46
46
|
];
|
|
47
47
|
for (const c of candidates) {
|
|
48
48
|
if (fsSync.existsSync(path.join(c, 'index.html'))) return c;
|
|
49
49
|
}
|
|
50
|
-
return candidates[
|
|
50
|
+
return candidates[0];
|
|
51
51
|
}
|
|
52
52
|
const webRoot = resolveWebRoot();
|
|
53
53
|
console.log(`[web] webRoot = ${webRoot}`);
|
|
@@ -238,11 +238,10 @@ async function saveTheme(theme: 'light' | 'dark', agentId: string): Promise<void
|
|
|
238
238
|
// ==================== Task Queue & Workflow System ====================
|
|
239
239
|
|
|
240
240
|
const TASK_QUEUE_PATH = path.join(SHARED_SESSION_PATH, 'task-queue.json');
|
|
241
|
-
const WORKFLOW_STATE_PATH = path.join(SHARED_SESSION_PATH, 'workflow-state.json');
|
|
242
241
|
|
|
243
242
|
interface Task {
|
|
244
243
|
id: string;
|
|
245
|
-
type: 'read' | 'summarize' | 'improve' | 'chat'
|
|
244
|
+
type: 'read' | 'summarize' | 'improve' | 'chat';
|
|
246
245
|
title: string;
|
|
247
246
|
description?: string;
|
|
248
247
|
status: 'pending' | 'running' | 'completed' | 'failed' | 'paused';
|
|
@@ -251,23 +250,10 @@ interface Task {
|
|
|
251
250
|
error?: string;
|
|
252
251
|
createdAt: string;
|
|
253
252
|
updatedAt: string;
|
|
254
|
-
steps?:
|
|
253
|
+
steps?: any[];
|
|
255
254
|
currentStep?: number;
|
|
256
255
|
}
|
|
257
256
|
|
|
258
|
-
interface TaskStep {
|
|
259
|
-
id: string;
|
|
260
|
-
name: string;
|
|
261
|
-
status: 'pending' | 'running' | 'completed' | 'failed';
|
|
262
|
-
result?: string;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
interface WorkflowState {
|
|
266
|
-
channelId: string;
|
|
267
|
-
tasks: Task[];
|
|
268
|
-
lastUpdated: string;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
257
|
async function loadTaskQueue(): Promise<Task[]> {
|
|
272
258
|
try {
|
|
273
259
|
const data = await fs.readFile(TASK_QUEUE_PATH, 'utf-8');
|
|
@@ -281,32 +267,6 @@ async function saveTaskQueue(tasks: Task[]): Promise<void> {
|
|
|
281
267
|
await fs.writeFile(TASK_QUEUE_PATH, JSON.stringify(tasks, null, 2));
|
|
282
268
|
}
|
|
283
269
|
|
|
284
|
-
async function loadWorkflowState(channelId: string): Promise<WorkflowState | null> {
|
|
285
|
-
try {
|
|
286
|
-
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
287
|
-
const states = JSON.parse(data) as WorkflowState[];
|
|
288
|
-
return states.find(s => s.channelId === channelId) || null;
|
|
289
|
-
} catch {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
async function saveWorkflowState(state: WorkflowState): Promise<void> {
|
|
295
|
-
try {
|
|
296
|
-
const data = await fs.readFile(WORKFLOW_STATE_PATH, 'utf-8');
|
|
297
|
-
const states = JSON.parse(data) as WorkflowState[];
|
|
298
|
-
const index = states.findIndex(s => s.channelId === state.channelId);
|
|
299
|
-
if (index >= 0) {
|
|
300
|
-
states[index] = state;
|
|
301
|
-
} else {
|
|
302
|
-
states.push(state);
|
|
303
|
-
}
|
|
304
|
-
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify(states, null, 2));
|
|
305
|
-
} catch {
|
|
306
|
-
await fs.writeFile(WORKFLOW_STATE_PATH, JSON.stringify([state], null, 2));
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
270
|
let isExecutingTask = false;
|
|
311
271
|
let executionTaskId: string | null = null;
|
|
312
272
|
|
|
@@ -355,37 +315,6 @@ async function executeTask(task: Task, channelId: string): Promise<void> {
|
|
|
355
315
|
}
|
|
356
316
|
break;
|
|
357
317
|
|
|
358
|
-
case 'workflow':
|
|
359
|
-
// 执行多步骤工作流
|
|
360
|
-
if (task.steps && task.steps.length > 0) {
|
|
361
|
-
let loopCount = 0;
|
|
362
|
-
for (let i = 0; i < task.steps.length; i++) {
|
|
363
|
-
// 广播循环开始
|
|
364
|
-
loopCount++;
|
|
365
|
-
broadcast({ type: 'workflow_loop', loopCount, content: `开始步骤 ${i + 1}/${task.steps.length}: ${task.steps[i].name}` }, channelId);
|
|
366
|
-
|
|
367
|
-
task.steps[i].status = 'running';
|
|
368
|
-
broadcast({ type: 'task_status', taskId: task.id, status: 'running', currentStep: i, totalSteps: task.steps.length }, channelId);
|
|
369
|
-
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中: ${task.steps[i].name}` }, channelId);
|
|
370
|
-
|
|
371
|
-
// 执行步骤 - 模拟流式输出
|
|
372
|
-
for (let j = 0; j < 3; j++) {
|
|
373
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
374
|
-
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `执行中... (${(j + 1) * 33}%)` }, channelId);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
task.steps[i].status = 'completed';
|
|
378
|
-
task.progress = Math.round(((i + 1) / task.steps.length) * 100);
|
|
379
|
-
|
|
380
|
-
broadcast({ type: 'workflow_step', step: `步骤 ${i + 1}`, content: `✅ 完成: ${task.steps[i].name}` }, channelId);
|
|
381
|
-
broadcast({ type: 'workflow_loop', loopCount, status: 'completed', content: `步骤 ${i + 1} 完成` }, channelId);
|
|
382
|
-
broadcast({ type: 'task_status', taskId: task.id, progress: task.progress }, channelId);
|
|
383
|
-
}
|
|
384
|
-
result = '✅ 工作流执行完成';
|
|
385
|
-
broadcast({ type: 'workflow_loop', loopCount, status: 'finished', content: result }, channelId);
|
|
386
|
-
}
|
|
387
|
-
break;
|
|
388
|
-
|
|
389
318
|
default:
|
|
390
319
|
result = '未知任务类型';
|
|
391
320
|
}
|
|
@@ -792,7 +721,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
792
721
|
const { getMinimax } = await import('../constraints/index.js');
|
|
793
722
|
const llm = getMinimax();
|
|
794
723
|
// v3 新增: 在 prompt 头部标记"这是远端访客", 让 AI 知道对方不是自己 owner
|
|
795
|
-
const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "
|
|
724
|
+
const visitorHint = `[系统上下文] 消息来源: 远端访客 (P2P 连接, publicKey=${senderKey.substring(0, 12)}...). 对方不是你 owner, 是通过 P2P 网络访问你这个 channel 的合作者. 称呼对方时可用 "远端访客" / "朋友" / "合作者", 不要叫 "用户".\n\n`;
|
|
796
725
|
// v3 新增: 也注入 channel 目录给 LLM (B 的 channel 也可以 @-mention 其他)
|
|
797
726
|
let dirHint = '';
|
|
798
727
|
const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
|
|
@@ -813,7 +742,10 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
|
|
|
813
742
|
}
|
|
814
743
|
dirHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session.\n\n';
|
|
815
744
|
}
|
|
816
|
-
|
|
745
|
+
// 2026-06-15: P2P 远端访客路径也用显式 marker 包裹 text
|
|
746
|
+
// 2026-06-15 二次修: 把 text 放在最前 (与主路径 server.ts:1868 对齐),
|
|
747
|
+
// 避免 LLM 被 judgmentHint 末尾的 "..." 误判为整个 input 被截断
|
|
748
|
+
const fullPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${visitorHint}${dirHint}${judgmentHint}\n`;
|
|
817
749
|
let fullResponse = '';
|
|
818
750
|
// v3 新增: 流式 token 节流推给 B — 让 B 看到过程
|
|
819
751
|
let lastFlushAt = 0;
|
|
@@ -1697,6 +1629,32 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1697
1629
|
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
1698
1630
|
broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
|
|
1699
1631
|
console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
|
|
1632
|
+
} else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
|
|
1633
|
+
// 2026-06-15: 步骤状态机事件 — 原样转发 (前端 step-timeline 组件订阅)
|
|
1634
|
+
broadcast({
|
|
1635
|
+
type: event.type,
|
|
1636
|
+
tool: event.tool,
|
|
1637
|
+
content: event.content,
|
|
1638
|
+
success: event.success,
|
|
1639
|
+
output: event.output,
|
|
1640
|
+
error: event.error,
|
|
1641
|
+
args: event.args,
|
|
1642
|
+
}, channelId);
|
|
1643
|
+
// 2026-06-16: 累积 step 到 runState, 供 /api/loop/inspect 读取
|
|
1644
|
+
try {
|
|
1645
|
+
if (event.type === 'step_done' || event.type === 'step_error') {
|
|
1646
|
+
const rs = channelRunState.get(channelId);
|
|
1647
|
+
if (rs) {
|
|
1648
|
+
if (!rs.lastSteps) rs.lastSteps = [];
|
|
1649
|
+
rs.lastSteps.push({
|
|
1650
|
+
name: String(event.tool || event.content || 'step').slice(0, 60),
|
|
1651
|
+
status: event.type === 'step_error' ? 'failed' : (event.success === false ? 'failed' : 'ok'),
|
|
1652
|
+
durationMs: typeof event.durationMs === 'number' ? event.durationMs : undefined,
|
|
1653
|
+
output: event.output ? String(event.output).slice(0, 800) : (event.error ? String(event.error).slice(0, 800) : undefined),
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
} catch { /* non-fatal */ }
|
|
1700
1658
|
} else if (event.type === 'error') {
|
|
1701
1659
|
broadcast({ type: 'error', content: event.content }, channelId);
|
|
1702
1660
|
}
|
|
@@ -1708,7 +1666,7 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1708
1666
|
let contextHint = '';
|
|
1709
1667
|
if (realChannelDid) contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
|
|
1710
1668
|
// v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
|
|
1711
|
-
contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "
|
|
1669
|
+
contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
|
|
1712
1670
|
if (boundWalletAddress) {
|
|
1713
1671
|
contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
|
|
1714
1672
|
}
|
|
@@ -1848,7 +1806,13 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1848
1806
|
|
|
1849
1807
|
if (contextHint) contextHint += '\n';
|
|
1850
1808
|
try {
|
|
1851
|
-
|
|
1809
|
+
// 2026-06-15: 把 user text 单独 marker 包起来, LLM 不会被 8K+ 的 system context 吞掉
|
|
1810
|
+
// (之前 contextHint + text 拼成一整段当 user role, 24 字符的 user input 埋在 8K+ 里看不出)
|
|
1811
|
+
// 修法: contextHint 当 "背景信息", text 当 "本轮用户请求" — 显式 marker 让 LLM 区分
|
|
1812
|
+
// 2026-06-15 二次修: 把 text 放在最前 (LLM 看到 input 第一眼是 user text, 不会被 judgmentHint 末尾
|
|
1813
|
+
// 的 "..." 误判为整个 input 截断)
|
|
1814
|
+
const markedPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
|
|
1815
|
+
fullResponse = await agent.promptStream(markedPrompt, streamCallback, runState.abortController?.signal, channelId);
|
|
1852
1816
|
} catch (err: any) {
|
|
1853
1817
|
// abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
|
|
1854
1818
|
if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
|
|
@@ -1975,12 +1939,17 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
|
|
|
1975
1939
|
running: boolean;
|
|
1976
1940
|
queue: PendingMessage[];
|
|
1977
1941
|
abortController: AbortController | null;
|
|
1942
|
+
// 2026-06-16: loop 检查 — 最近一轮的步骤/摘要/最终回复/token
|
|
1943
|
+
lastSteps?: Array<{ name: string; status: string; durationMs?: number; output?: string }>;
|
|
1944
|
+
lastSummary?: string;
|
|
1945
|
+
lastFinalReply?: string;
|
|
1946
|
+
lastTokens?: { input?: number; output?: number };
|
|
1978
1947
|
}
|
|
1979
1948
|
const channelRunState: Map<string, ChannelRunState> = new Map();
|
|
1980
1949
|
function getOrCreateRunState(channelId: string): ChannelRunState {
|
|
1981
1950
|
let s = channelRunState.get(channelId);
|
|
1982
1951
|
if (!s) {
|
|
1983
|
-
s = { running: false, queue: [], abortController: null };
|
|
1952
|
+
s = { running: false, queue: [], abortController: null, lastSteps: [], lastSummary: '', lastFinalReply: '', lastTokens: {} };
|
|
1984
1953
|
channelRunState.set(channelId, s);
|
|
1985
1954
|
}
|
|
1986
1955
|
return s;
|
|
@@ -2719,14 +2688,27 @@ app.get('/channels', async (_req, res) => {
|
|
|
2719
2688
|
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
2720
2689
|
} else if (event.type === 'status' || event.type === 'tool') {
|
|
2721
2690
|
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
2691
|
+
} else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
|
|
2692
|
+
// 2026-06-15: 步骤状态机事件 — 原样转发
|
|
2693
|
+
broadcast({
|
|
2694
|
+
type: event.type,
|
|
2695
|
+
tool: event.tool,
|
|
2696
|
+
content: event.content,
|
|
2697
|
+
success: event.success,
|
|
2698
|
+
output: event.output,
|
|
2699
|
+
error: event.error,
|
|
2700
|
+
args: event.args,
|
|
2701
|
+
}, channelId);
|
|
2722
2702
|
} else if (event.type === 'error') {
|
|
2723
2703
|
broadcast({ type: 'error', content: event.content }, channelId);
|
|
2724
2704
|
}
|
|
2725
2705
|
};
|
|
2726
2706
|
|
|
2727
2707
|
// 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
|
|
2708
|
+
// 2026-06-15: 同 /message 路径, 用显式 marker 包裹 userMessage, 避免 LLM 把它当背景信息
|
|
2728
2709
|
const regenHint = await buildJudgmentHint(channel, channelId);
|
|
2729
|
-
|
|
2710
|
+
const markedRegen = `${regenHint}\n\n【本轮用户请求】\n${userMessage}\n【请求结束】\n`;
|
|
2711
|
+
fullResponse = await agent.promptStream(markedRegen, streamCallback, undefined, channelId);
|
|
2730
2712
|
|
|
2731
2713
|
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
2732
2714
|
|
|
@@ -2914,46 +2896,6 @@ app.get('/channels', async (_req, res) => {
|
|
|
2914
2896
|
}
|
|
2915
2897
|
});
|
|
2916
2898
|
|
|
2917
|
-
// 创建并执行工作流
|
|
2918
|
-
app.post('/api/workflow', async (req, res) => {
|
|
2919
|
-
try {
|
|
2920
|
-
const { channelId, title, steps } = req.body;
|
|
2921
|
-
if (!channelId || !steps || !Array.isArray(steps)) {
|
|
2922
|
-
return res.status(400).json({ error: 'channelId and steps required' });
|
|
2923
|
-
}
|
|
2924
|
-
|
|
2925
|
-
const tasks = await loadTaskQueue();
|
|
2926
|
-
const task: Task = {
|
|
2927
|
-
id: `wf_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
2928
|
-
type: 'workflow',
|
|
2929
|
-
title: title || '工作流',
|
|
2930
|
-
description: `包含 ${steps.length} 个步骤的工作流`,
|
|
2931
|
-
status: 'pending',
|
|
2932
|
-
progress: 0,
|
|
2933
|
-
createdAt: new Date().toISOString(),
|
|
2934
|
-
updatedAt: new Date().toISOString(),
|
|
2935
|
-
steps: steps.map((s: string, i: number) => ({
|
|
2936
|
-
id: `step_${i}`,
|
|
2937
|
-
name: s,
|
|
2938
|
-
status: 'pending'
|
|
2939
|
-
})),
|
|
2940
|
-
currentStep: 0
|
|
2941
|
-
};
|
|
2942
|
-
|
|
2943
|
-
tasks.push(task);
|
|
2944
|
-
await saveTaskQueue(tasks);
|
|
2945
|
-
|
|
2946
|
-
// 自动开始执行
|
|
2947
|
-
if (!isExecutingTask) {
|
|
2948
|
-
executeTask(task, channelId);
|
|
2949
|
-
}
|
|
2950
|
-
|
|
2951
|
-
res.json({ ok: true, task });
|
|
2952
|
-
} catch (err: any) {
|
|
2953
|
-
res.status(500).json({ error: err.message });
|
|
2954
|
-
}
|
|
2955
|
-
});
|
|
2956
|
-
|
|
2957
2899
|
// ==================== LLM 配置 API ====================
|
|
2958
2900
|
|
|
2959
2901
|
// 获取所有 LLM 配置
|
|
@@ -4178,7 +4120,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
4178
4120
|
}
|
|
4179
4121
|
});
|
|
4180
4122
|
|
|
4181
|
-
//
|
|
4123
|
+
// 用户审阅: 批准 draft
|
|
4182
4124
|
app.post('/api/chat/approve', async (req, res) => {
|
|
4183
4125
|
try {
|
|
4184
4126
|
const { messageId, peerDID, finalText } = req.body || {};
|
|
@@ -4191,7 +4133,7 @@ app.get('/channels', async (_req, res) => {
|
|
|
4191
4133
|
}
|
|
4192
4134
|
});
|
|
4193
4135
|
|
|
4194
|
-
//
|
|
4136
|
+
// 用户审阅: 丢弃 draft
|
|
4195
4137
|
app.post('/api/chat/dismiss', async (req, res) => {
|
|
4196
4138
|
try {
|
|
4197
4139
|
const { messageId, peerDID } = req.body || {};
|
|
@@ -4871,6 +4813,41 @@ app.get('/channels', async (_req, res) => {
|
|
|
4871
4813
|
}
|
|
4872
4814
|
});
|
|
4873
4815
|
|
|
4816
|
+
// ============================================================
|
|
4817
|
+
// system-prompt health (P-Action 2 — Harness Gardening)
|
|
4818
|
+
// 返回每层 lifecycle 状态: ok | stale | overdue-review | missing-frontmatter | dynamic
|
|
4819
|
+
// query: ?activeOnly=1 → 只返回当前 context 激活的层
|
|
4820
|
+
// ============================================================
|
|
4821
|
+
app.get('/api/prompt/health', async (req, res) => {
|
|
4822
|
+
try {
|
|
4823
|
+
const { listLayers } = await import('../llm/system-prompt/registry.js');
|
|
4824
|
+
const { evaluateLayers, markActive } = await import('../llm/system-prompt/health.js');
|
|
4825
|
+
const all = listLayers() as Array<any>;
|
|
4826
|
+
const baseReport = evaluateLayers(all);
|
|
4827
|
+
|
|
4828
|
+
// 如果 query 里有 activeOnly, 跑一次 assembleSystemPrompt 拿激活列表
|
|
4829
|
+
if (String(req.query.activeOnly ?? '') === '1') {
|
|
4830
|
+
const { assembleSystemPrompt } = await import('../llm/system-prompt/registry.js');
|
|
4831
|
+
const channel = String(req.query.channel ?? 'local') as 'local' | 'p2p-visitor' | 'p2p-agent';
|
|
4832
|
+
const role = req.query.role as any;
|
|
4833
|
+
const tool = req.query.tool as any;
|
|
4834
|
+
try {
|
|
4835
|
+
const r = await assembleSystemPrompt({ channel, role, tool });
|
|
4836
|
+
const activeIds = new Set(r.layerIds);
|
|
4837
|
+
res.json(markActive(baseReport, activeIds));
|
|
4838
|
+
} catch (err: any) {
|
|
4839
|
+
console.warn('[prompt-health] assembleSystemPrompt failed (silent, returning base report):', err);
|
|
4840
|
+
res.json(baseReport);
|
|
4841
|
+
}
|
|
4842
|
+
} else {
|
|
4843
|
+
res.json(baseReport);
|
|
4844
|
+
}
|
|
4845
|
+
} catch (err: any) {
|
|
4846
|
+
console.error('[prompt-health] failed:', err);
|
|
4847
|
+
res.status(500).json({ error: err.message });
|
|
4848
|
+
}
|
|
4849
|
+
});
|
|
4850
|
+
|
|
4874
4851
|
// 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
|
|
4875
4852
|
// body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
|
|
4876
4853
|
// query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
|
|
@@ -5353,26 +5330,196 @@ app.get('/channels', async (_req, res) => {
|
|
|
5353
5330
|
res.json(getEventHistory());
|
|
5354
5331
|
});
|
|
5355
5332
|
|
|
5333
|
+
// ============================================================
|
|
5334
|
+
// Permission Mode (P2.2 — UI 暴露开关)
|
|
5335
|
+
// 优先级: 运行时 session 覆盖 > env BOLLOON_PERM_MODE > 'default'
|
|
5336
|
+
// 运行时覆盖存在 ~/.bolloon/sessions/permission-mode.json, 每次 promptStream 入口读取
|
|
5337
|
+
// ============================================================
|
|
5338
|
+
|
|
5339
|
+
const PERM_MODE_FILE = path.join(
|
|
5340
|
+
process.env.HOME || os.homedir() || '/tmp',
|
|
5341
|
+
'.bolloon', 'sessions', 'permission-mode.json'
|
|
5342
|
+
);
|
|
5343
|
+
|
|
5344
|
+
function readPermModeOverride(): { mode: string; ts: string } | null {
|
|
5345
|
+
try {
|
|
5346
|
+
// 同步读, 文件很小
|
|
5347
|
+
const raw = fsSync.readFileSync(PERM_MODE_FILE, 'utf-8');
|
|
5348
|
+
const obj = JSON.parse(raw);
|
|
5349
|
+
if (obj && typeof obj.mode === 'string') {
|
|
5350
|
+
return { mode: obj.mode, ts: obj.ts || new Date().toISOString() };
|
|
5351
|
+
}
|
|
5352
|
+
} catch { /* 不存在 = 无 override */ }
|
|
5353
|
+
return null;
|
|
5354
|
+
}
|
|
5355
|
+
|
|
5356
|
+
function writePermModeOverride(mode: string): void {
|
|
5357
|
+
try {
|
|
5358
|
+
const dir = path.dirname(PERM_MODE_FILE);
|
|
5359
|
+
fsSync.mkdirSync(dir, { recursive: true });
|
|
5360
|
+
fsSync.writeFileSync(PERM_MODE_FILE, JSON.stringify({ mode, ts: new Date().toISOString() }, null, 2), 'utf-8');
|
|
5361
|
+
} catch (err) {
|
|
5362
|
+
console.warn('[server] writePermModeOverride failed:', err);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
|
|
5366
|
+
// 读当前生效 mode (runtime override > env > default)
|
|
5367
|
+
app.get('/api/permission-mode', async (_req: any, res: any) => {
|
|
5368
|
+
const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
|
|
5369
|
+
const override = readPermModeOverride();
|
|
5370
|
+
const envMode = process.env.BOLLOON_PERM_MODE || null;
|
|
5371
|
+
const effective = resolvePermissionMode();
|
|
5372
|
+
res.json({
|
|
5373
|
+
effective,
|
|
5374
|
+
override: override?.mode || null,
|
|
5375
|
+
overrideTs: override?.ts || null,
|
|
5376
|
+
env: envMode,
|
|
5377
|
+
allowed: ALL_PERMISSION_MODES,
|
|
5378
|
+
description: {
|
|
5379
|
+
default: '每次工具调用询问; shell 走 shell-guard',
|
|
5380
|
+
acceptEdits: 'edit_*/write_* 跳过黑名单; shell 仍走 shell-guard',
|
|
5381
|
+
bypassPermissions: '非 shell 全部放行; shell 永远走 shell-guard (硬约束)',
|
|
5382
|
+
},
|
|
5383
|
+
});
|
|
5384
|
+
});
|
|
5385
|
+
|
|
5386
|
+
// 设 runtime override (存盘, 下次 promptStream 入口读取生效)
|
|
5387
|
+
app.post('/api/permission-mode', async (req: any, res: any) => {
|
|
5388
|
+
const { resolvePermissionMode, ALL_PERMISSION_MODES } = await import('../agents/permission-mode.js');
|
|
5389
|
+
const mode = String(req.body?.mode || '');
|
|
5390
|
+
if (!ALL_PERMISSION_MODES.includes(mode as any)) {
|
|
5391
|
+
return res.status(400).json({
|
|
5392
|
+
error: `Invalid mode. Allowed: ${ALL_PERMISSION_MODES.join(', ')}`,
|
|
5393
|
+
allowed: ALL_PERMISSION_MODES,
|
|
5394
|
+
});
|
|
5395
|
+
}
|
|
5396
|
+
const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
|
|
5397
|
+
writePermModeOverride(mode);
|
|
5398
|
+
// 写历史 (append-only JSONL, 跟 bolloon 其他 audit 一致)
|
|
5399
|
+
try {
|
|
5400
|
+
const HISTORY_FILE = path.join(
|
|
5401
|
+
process.env.HOME || os.homedir() || '/tmp',
|
|
5402
|
+
'.bolloon', 'sessions', 'permission-mode-history.jsonl'
|
|
5403
|
+
);
|
|
5404
|
+
fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
5405
|
+
fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
|
|
5406
|
+
ts: new Date().toISOString(),
|
|
5407
|
+
from: oldMode,
|
|
5408
|
+
to: mode,
|
|
5409
|
+
source: 'api',
|
|
5410
|
+
}) + '\n', 'utf-8');
|
|
5411
|
+
} catch { /* 历史写失败不阻塞主流程 */ }
|
|
5412
|
+
console.log(`[server] permission-mode override set to "${mode}" via API (was "${oldMode}")`);
|
|
5413
|
+
res.json({
|
|
5414
|
+
ok: true,
|
|
5415
|
+
mode,
|
|
5416
|
+
previousMode: oldMode,
|
|
5417
|
+
ts: new Date().toISOString(),
|
|
5418
|
+
note: '新 mode 在下一次 promptStream 入口生效 (不打断当前对话)',
|
|
5419
|
+
});
|
|
5420
|
+
});
|
|
5421
|
+
|
|
5422
|
+
// 取消 runtime override, 回到 env 或 default
|
|
5423
|
+
app.delete('/api/permission-mode', async (_req: any, res: any) => {
|
|
5424
|
+
const oldMode = readPermModeOverride()?.mode || process.env.BOLLOON_PERM_MODE || 'default';
|
|
5425
|
+
try {
|
|
5426
|
+
if (fsSync.existsSync(PERM_MODE_FILE)) fsSync.unlinkSync(PERM_MODE_FILE);
|
|
5427
|
+
} catch (err) {
|
|
5428
|
+
console.warn('[server] delete perm-mode override failed:', err);
|
|
5429
|
+
}
|
|
5430
|
+
// 写历史
|
|
5431
|
+
try {
|
|
5432
|
+
const HISTORY_FILE = path.join(
|
|
5433
|
+
process.env.HOME || os.homedir() || '/tmp',
|
|
5434
|
+
'.bolloon', 'sessions', 'permission-mode-history.jsonl'
|
|
5435
|
+
);
|
|
5436
|
+
fsSync.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
|
|
5437
|
+
fsSync.appendFileSync(HISTORY_FILE, JSON.stringify({
|
|
5438
|
+
ts: new Date().toISOString(),
|
|
5439
|
+
from: oldMode,
|
|
5440
|
+
to: 'env-or-default',
|
|
5441
|
+
source: 'api',
|
|
5442
|
+
action: 'delete-override',
|
|
5443
|
+
}) + '\n', 'utf-8');
|
|
5444
|
+
} catch { /* ignore */ }
|
|
5445
|
+
res.json({ ok: true, note: '已删除 runtime override, 回到 env / default' });
|
|
5446
|
+
});
|
|
5447
|
+
|
|
5448
|
+
// 2026-06-16: 循环检查 — GET /api/loop/inspect?channelId=...
|
|
5449
|
+
// 返回最近一轮 ReAct loop 的产出: 步骤 / 工具调用 / 压缩摘要 / 最终回复 / token 用量.
|
|
5450
|
+
// 用于前端 status bar 的「✓ 检查」按钮弹 modal.
|
|
5451
|
+
app.get('/api/loop/inspect', async (req: any, res: any) => {
|
|
5452
|
+
try {
|
|
5453
|
+
const channelId = String(req.query.channelId || '');
|
|
5454
|
+
if (!channelId) return res.status(400).json({ error: 'channelId required' });
|
|
5455
|
+
const s = channelRunState.get(channelId);
|
|
5456
|
+
if (!s) return res.json({ summary: '该 channel 无活跃 loop', steps: [], finalReply: '' });
|
|
5457
|
+
const steps = (s.lastSteps || []).map((st: any) => ({
|
|
5458
|
+
name: st.name || st.tool || 'step',
|
|
5459
|
+
status: st.status || 'completed',
|
|
5460
|
+
durationMs: st.durationMs,
|
|
5461
|
+
output: st.output || st.result || '',
|
|
5462
|
+
}));
|
|
5463
|
+
res.json({
|
|
5464
|
+
summary: s.lastSummary || (s.running ? 'loop 仍在运行中' : 'loop 已结束'),
|
|
5465
|
+
steps,
|
|
5466
|
+
finalReply: s.lastFinalReply || '',
|
|
5467
|
+
tokens: s.lastTokens || {},
|
|
5468
|
+
});
|
|
5469
|
+
} catch (err: any) {
|
|
5470
|
+
res.status(500).json({ error: err.message });
|
|
5471
|
+
}
|
|
5472
|
+
});
|
|
5473
|
+
|
|
5474
|
+
// 历史 (类似 self-improve history, 供前端 timeline)
|
|
5475
|
+
app.get('/api/permission-mode/history', async (_req: any, res: any) => {
|
|
5476
|
+
const HISTORY_FILE = path.join(
|
|
5477
|
+
process.env.HOME || os.homedir() || '/tmp',
|
|
5478
|
+
'.bolloon', 'sessions', 'permission-mode-history.jsonl'
|
|
5479
|
+
);
|
|
5480
|
+
try {
|
|
5481
|
+
if (!fsSync.existsSync(HISTORY_FILE)) return res.json([]);
|
|
5482
|
+
const lines = fsSync.readFileSync(HISTORY_FILE, 'utf-8').split('\n').filter(Boolean);
|
|
5483
|
+
const entries = lines.map((l: string) => {
|
|
5484
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
5485
|
+
}).filter(Boolean);
|
|
5486
|
+
res.json(entries.slice(-50)); // 最近 50 条
|
|
5487
|
+
} catch (err) {
|
|
5488
|
+
res.json([]);
|
|
5489
|
+
}
|
|
5490
|
+
});
|
|
5491
|
+
|
|
5356
5492
|
// 健康检查错误数 ≥ 2 -> 触发自改信号
|
|
5493
|
+
// 2026-06-16: 自迭代默认关 (用户模式), 仅 BOLLOON_DEV_MODE=1 或 selfImprove=true 启动项才装 callback
|
|
5357
5494
|
if (healthMonitor) {
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
.
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5495
|
+
if (selfImproveEnabled) {
|
|
5496
|
+
healthMonitor.startPeriodicCheck(60000, (status: any) => {
|
|
5497
|
+
const errorCount = Object.values(status.checks as Record<string, { status: string }>)
|
|
5498
|
+
.filter((c) => c.status === 'error').length;
|
|
5499
|
+
if (errorCount >= 2) {
|
|
5500
|
+
import('../heartbeat/self-improve-bus.js').then(({ reportSelfImproveEvent }) => {
|
|
5501
|
+
const failedKeys = Object.entries(status.checks as Record<string, { status: string }>)
|
|
5502
|
+
.filter(([_, c]) => c.status === 'error').map(([k]) => k).join(', ');
|
|
5503
|
+
reportSelfImproveEvent({
|
|
5504
|
+
kind: 'silent-timeout',
|
|
5505
|
+
details: `健康检查有 ${errorCount} 项失败: ${failedKeys}`
|
|
5506
|
+
});
|
|
5368
5507
|
});
|
|
5369
|
-
}
|
|
5370
|
-
}
|
|
5371
|
-
}
|
|
5508
|
+
}
|
|
5509
|
+
});
|
|
5510
|
+
} else {
|
|
5511
|
+
// 用户模式: 只跑监控不打自改信号, 心跳仍工作
|
|
5512
|
+
healthMonitor.startPeriodicCheck(60000);
|
|
5513
|
+
console.log('[24h] Health monitor periodic check (no self-improve)');
|
|
5514
|
+
}
|
|
5372
5515
|
}
|
|
5373
5516
|
|
|
5374
|
-
// 安装自改总线 -> SSE 桥
|
|
5375
|
-
|
|
5517
|
+
// 安装自改总线 -> SSE 桥 (开发者模式才装, 用户模式靠 /api/self-improve/trigger 手动触发)
|
|
5518
|
+
if (selfImproveEnabled) {
|
|
5519
|
+
void installSelfImproveHook();
|
|
5520
|
+
} else {
|
|
5521
|
+
console.log('[self-improve] 用户模式, installSelfImproveHook 跳过 (可用 POST /api/self-improve/trigger 手动触发)');
|
|
5522
|
+
}
|
|
5376
5523
|
|
|
5377
5524
|
// 端口冲突时自动找下一个可用端口(最多 10 次),避免 EADDRINUSE 直接崩溃
|
|
5378
5525
|
return new Promise<{ app: express.Express; server: ReturnType<typeof createServer>; port: number }>((resolve, reject) => {
|
|
@@ -5394,11 +5541,24 @@ app.get('/channels', async (_req, res) => {
|
|
|
5394
5541
|
console.log('服务器已监听');
|
|
5395
5542
|
// 安装 chat bus -> SSE 桥 (供前端 inbox UI 实时刷新)
|
|
5396
5543
|
void installChatBusHook();
|
|
5544
|
+
// 2026-06-16: ping 改为 data: {"type":"ping"} — 之前是 SSE 注释格式 (: ping\n\n),
|
|
5545
|
+
// 浏览器 EventSource 不触发 onmessage, 客户端 60s 阈值 (现已 30s) 误判死链.
|
|
5546
|
+
// 改后前端 onmessage 收到 ping 就重置 lastEventTime, 真死链才 30s 后重建.
|
|
5397
5547
|
setInterval(() => {
|
|
5398
5548
|
for (const client of sseClients) {
|
|
5399
|
-
|
|
5549
|
+
try {
|
|
5550
|
+
client.res.write('data: {"type":"ping"}\n\n');
|
|
5551
|
+
} catch (err) {
|
|
5552
|
+
// socket 已断, 跳过 — client 端 onerror 会触发重连
|
|
5553
|
+
console.warn('[SSE ping] write 失败, 跳过该客户端:', (err as Error).message);
|
|
5554
|
+
}
|
|
5400
5555
|
}
|
|
5401
5556
|
}, 30000);
|
|
5557
|
+
// 2026-06-16: 全局捕获 socket error 事件, 避免未处理 EPIPE/ETIMEDOUT 让进程崩
|
|
5558
|
+
currentServer.on('clientError', (err, socket) => {
|
|
5559
|
+
console.warn('[server] clientError:', (err as any).code, err.message);
|
|
5560
|
+
try { socket.end(); } catch {}
|
|
5561
|
+
});
|
|
5402
5562
|
resolve({ app, server: currentServer, port: currentPort });
|
|
5403
5563
|
});
|
|
5404
5564
|
};
|
|
@@ -5568,4 +5728,4 @@ export async function openBrowser(url: string) {
|
|
|
5568
5728
|
console.error('打开浏览器失败:', err.message);
|
|
5569
5729
|
}
|
|
5570
5730
|
});
|
|
5571
|
-
}
|
|
5731
|
+
}
|