@bolloon/bolloon-agent 0.5.5 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/execution-entry.js +10 -0
- package/dist/agents/goal-flywheel/wiring/seams.js +5 -1
- package/dist/agents/parse-tool-call.js +6 -1
- package/dist/agents/pi-sdk-session-factory.js +36 -3
- package/dist/agents/pi-sdk-tools.js +18 -8
- package/dist/agents/pi-sdk.js +749 -1390
- package/dist/agents/run-context.js +20 -0
- package/dist/agents/runner-resolver.js +3 -1
- package/dist/agents/tool-capability/index.js +51 -0
- package/dist/agents/workflow-pivot-loop.js +31 -10
- package/dist/agents/write-verify.js +60 -0
- package/dist/bollharness-integration/integration.js +8 -0
- package/dist/bollharness-integration/skill-adapter.js +32 -3
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js +5 -1
- package/dist/electron-build/agents/goal-flywheel/wiring/seams.js.map +1 -1
- package/dist/electron-build/agents/parse-tool-call.js +6 -1
- package/dist/electron-build/agents/parse-tool-call.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-session-factory.js +36 -3
- package/dist/electron-build/agents/pi-sdk-session-factory.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-tools.js +21 -11
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +750 -1389
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-context.js +24 -0
- package/dist/electron-build/agents/run-context.js.map +1 -0
- package/dist/electron-build/agents/runner-resolver.js +3 -1
- package/dist/electron-build/agents/runner-resolver.js.map +1 -1
- package/dist/electron-build/agents/tool-capability/index.js +90 -0
- package/dist/electron-build/agents/tool-capability/index.js.map +1 -0
- package/dist/electron-build/agents/workflow-pivot-loop.js +31 -10
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/agents/write-verify.js +99 -0
- package/dist/electron-build/agents/write-verify.js.map +1 -0
- package/dist/electron-build/bollharness-integration/integration.js +8 -0
- package/dist/electron-build/bollharness-integration/integration.js.map +1 -1
- package/dist/electron-build/bollharness-integration/skill-adapter.js +32 -3
- package/dist/electron-build/bollharness-integration/skill-adapter.js.map +1 -1
- package/dist/electron-build/kernel/channel-actor.js +339 -0
- package/dist/electron-build/kernel/channel-actor.js.map +1 -0
- package/dist/electron-build/kernel/control.js +102 -0
- package/dist/electron-build/kernel/control.js.map +1 -0
- package/dist/electron-build/kernel/model-runtime.js +508 -0
- package/dist/electron-build/kernel/model-runtime.js.map +1 -0
- package/dist/electron-build/kernel/run-lifecycle.js +146 -0
- package/dist/electron-build/kernel/run-lifecycle.js.map +1 -0
- package/dist/electron-build/kernel/transport.js +119 -0
- package/dist/electron-build/kernel/transport.js.map +1 -0
- package/dist/electron-build/llm/native-adapter.js +122 -0
- package/dist/electron-build/llm/native-adapter.js.map +1 -0
- package/dist/index.js +43 -19
- package/dist/ios/bolloon-web.json +3 -3
- package/dist/ios/client.js +13 -7
- package/dist/ios/mobile-core.js +7762 -6289
- package/dist/ios/mobile.js +64 -0
- package/dist/ios/routes-mobile-tasks.js +2 -1
- package/dist/ios/routes-tasks.js +16 -1
- package/dist/ios/routes-x402-info.js +28 -1
- package/dist/ios/server-storage.js +26 -5
- package/dist/ios/server.js +840 -862
- package/dist/ios/sw.js +1 -1
- package/dist/ios/ui/message-renderer.js +473 -29
- package/dist/ios/ui/step-timeline.js +13 -12
- package/dist/kernel/channel-actor.js +321 -0
- package/dist/kernel/control.js +95 -0
- package/dist/kernel/gate-scan.js +1350 -0
- package/dist/kernel/model-runtime.js +494 -0
- package/dist/kernel/plan-channel-actor.js +305 -0
- package/dist/kernel/plan-communication.js +163 -0
- package/dist/kernel/plan-constraint.js +152 -0
- package/dist/kernel/plan-deletion.js +32 -0
- package/dist/kernel/plan-harness.js +78 -0
- package/dist/kernel/plan-modelruntime.js +60 -0
- package/dist/kernel/plan-runcontext.js +163 -0
- package/dist/kernel/plan.js +149 -0
- package/dist/kernel/roster.js +244 -0
- package/dist/kernel/run-lifecycle.js +138 -0
- package/dist/kernel/transport.js +111 -0
- package/dist/llm/native-adapter.js +117 -0
- package/dist/web/bolloon-web.json +3 -3
- package/dist/web/client.js +12 -7
- package/dist/web/routes-tasks.js +16 -1
- package/dist/web/server.js +838 -862
- package/dist/web/sw.js +1 -1
- package/dist/web/ui/message-renderer.js +473 -29
- package/dist/web/ui/step-timeline.js +13 -12
- package/package.json +1 -1
- package/scripts/build-web.ts +74 -1
- package/dist/constraint-runtime/_archive_helper.d.ts +0 -1
- package/dist/constraint-runtime/_archive_helper.js +0 -9
- package/dist/constraint-runtime/assistant/index.d.ts +0 -11
- package/dist/constraint-runtime/assistant/index.js +0 -12
- package/dist/constraint-runtime/bootstrap/index.d.ts +0 -11
- package/dist/constraint-runtime/bootstrap/index.js +0 -12
- package/dist/constraint-runtime/bridge/index.d.ts +0 -11
- package/dist/constraint-runtime/bridge/index.js +0 -12
- package/dist/constraint-runtime/buddy/index.d.ts +0 -11
- package/dist/constraint-runtime/buddy/index.js +0 -12
- package/dist/constraint-runtime/cli/index.d.ts +0 -11
- package/dist/constraint-runtime/cli/index.js +0 -12
- package/dist/constraint-runtime/components/index.d.ts +0 -11
- package/dist/constraint-runtime/components/index.js +0 -12
- package/dist/constraint-runtime/constants/index.d.ts +0 -11
- package/dist/constraint-runtime/constants/index.js +0 -12
- package/dist/constraint-runtime/coordinator/index.d.ts +0 -11
- package/dist/constraint-runtime/coordinator/index.js +0 -12
- package/dist/constraint-runtime/cost_hook.d.ts +0 -2
- package/dist/constraint-runtime/cost_hook.js +0 -4
- package/dist/constraint-runtime/entrypoints/index.d.ts +0 -11
- package/dist/constraint-runtime/entrypoints/index.js +0 -12
- package/dist/constraint-runtime/execution_registry.d.ts +0 -20
- package/dist/constraint-runtime/execution_registry.js +0 -44
- package/dist/constraint-runtime/hooks/index.d.ts +0 -11
- package/dist/constraint-runtime/hooks/index.js +0 -12
- package/dist/constraint-runtime/ink.d.ts +0 -1
- package/dist/constraint-runtime/ink.js +0 -4
- package/dist/constraint-runtime/keybindings/index.d.ts +0 -11
- package/dist/constraint-runtime/keybindings/index.js +0 -12
- package/dist/constraint-runtime/memdir/index.d.ts +0 -11
- package/dist/constraint-runtime/memdir/index.js +0 -12
- package/dist/constraint-runtime/migrations/index.d.ts +0 -11
- package/dist/constraint-runtime/migrations/index.js +0 -12
- package/dist/constraint-runtime/moreright/index.d.ts +0 -11
- package/dist/constraint-runtime/moreright/index.js +0 -12
- package/dist/constraint-runtime/native_ts/index.d.ts +0 -11
- package/dist/constraint-runtime/native_ts/index.js +0 -12
- package/dist/constraint-runtime/output_styles/index.d.ts +0 -11
- package/dist/constraint-runtime/output_styles/index.js +0 -12
- package/dist/constraint-runtime/plugins/index.d.ts +0 -11
- package/dist/constraint-runtime/plugins/index.js +0 -12
- package/dist/constraint-runtime/port_manifest.d.ts +0 -7
- package/dist/constraint-runtime/port_manifest.js +0 -11
- package/dist/constraint-runtime/query.d.ts +0 -6
- package/dist/constraint-runtime/query.js +0 -1
- package/dist/constraint-runtime/remote/index.d.ts +0 -11
- package/dist/constraint-runtime/remote/index.js +0 -12
- package/dist/constraint-runtime/schemas/index.d.ts +0 -11
- package/dist/constraint-runtime/schemas/index.js +0 -12
- package/dist/constraint-runtime/screens/index.d.ts +0 -11
- package/dist/constraint-runtime/screens/index.js +0 -12
- package/dist/constraint-runtime/server/index.d.ts +0 -11
- package/dist/constraint-runtime/server/index.js +0 -12
- package/dist/constraint-runtime/services/index.d.ts +0 -11
- package/dist/constraint-runtime/services/index.js +0 -12
- package/dist/constraint-runtime/state/index.d.ts +0 -11
- package/dist/constraint-runtime/state/index.js +0 -12
- package/dist/constraint-runtime/system_init.d.ts +0 -1
- package/dist/constraint-runtime/system_init.js +0 -20
- package/dist/constraint-runtime/types/index.d.ts +0 -11
- package/dist/constraint-runtime/types/index.js +0 -12
- package/dist/constraint-runtime/upstream_proxy/index.d.ts +0 -11
- package/dist/constraint-runtime/upstream_proxy/index.js +0 -12
- package/dist/constraint-runtime/utils/index.d.ts +0 -11
- package/dist/constraint-runtime/utils/index.js +0 -12
- package/dist/constraint-runtime/vim/index.d.ts +0 -11
- package/dist/constraint-runtime/vim/index.js +0 -12
- package/dist/constraint-runtime/voice/index.d.ts +0 -11
- package/dist/constraint-runtime/voice/index.js +0 -12
package/dist/ios/server.js
CHANGED
|
@@ -87,6 +87,9 @@ console.log = (...args) => {
|
|
|
87
87
|
import { createHyperswarmCommunicator, createTopic, KeyManager, AgentAuthManager, } from '@diap/sdk';
|
|
88
88
|
import { initMinimax } from '../constraints/index.js';
|
|
89
89
|
import { createAgentSession } from '../agents/pi-sdk.js';
|
|
90
|
+
// **K5 步骤④ (入口投递)**: 把一次入口执行投进会话 Actor 的 mailbox ⇒ 同一会话身份的输入排队执行
|
|
91
|
+
import { deliverThroughActor, getChannelQueue, abortActorsOfChannel } from '../kernel/channel-actor.js';
|
|
92
|
+
import { requireRunExecution } from '../agents/execution-entry.js'; // K8 唯一执行入口的取用助手 (脱挂坑见该文件头) // K8: 待办执行经内核邮箱串行
|
|
90
93
|
import { irohTransport } from '../network/iroh-transport.js';
|
|
91
94
|
import { createAgentDelegateApp } from './agent-delegate-server.js';
|
|
92
95
|
import { createIrohDelegateTransport } from './iroh-delegate-transport.js';
|
|
@@ -94,6 +97,14 @@ import { verifyMessage, isAddress, getAddress } from 'viem';
|
|
|
94
97
|
// 2026-07-05: peer 目录管理 + manifest 协议
|
|
95
98
|
import * as peerFs from '../network/peer-fs.js';
|
|
96
99
|
import { loadLocalResources, writeRemoteResources } from '../network/peer-resource-bridge.js';
|
|
100
|
+
/**
|
|
101
|
+
* 2026-10-02 (K8): 通道拿**唯一执行入口**的统一姿势 —— `runExecution` 在 `AgentSession` 上是**可选**的
|
|
102
|
+
* (接口允许实现自定义), 所以直接调会报 possibly-undefined。这里**响亮失败**而不是 `!` 断言:
|
|
103
|
+
* 拿不到入口就说明"通道又要直呼 prompt 了", 必须当场炸掉, 不许静默回落 (否则收口只剩纸面)。
|
|
104
|
+
*/
|
|
105
|
+
// 2026-10-02: 实现搬到 `src/agents/execution-entry.ts` (为了能被行为门测到 —— 那个"脱挂"坑源码看不出)。
|
|
106
|
+
// ⚠️ 曾经这里写的是 `return agent.runExecution;` (等于脱挂方法) ⇒ 调用时 this===undefined
|
|
107
|
+
// ⇒ 实例里 supervisor / 心跳 / 任务三条路径都报 `... reading 'applyExecutionRequest'`。
|
|
97
108
|
// 前端资源路径: 兼容 src 运行 + dist 运行 + npm 全局安装
|
|
98
109
|
// - src 跑 (tsx): __dirname = .../src/web → .../dist/web
|
|
99
110
|
// - dist 跑 (npm): __dirname = .../dist/web → 自身就是 web 根
|
|
@@ -585,7 +596,8 @@ async function triggerRemoteFollowup(channelId, remoteReply, fromPublicKey, roun
|
|
|
585
596
|
broadcast({ type: 'followup-step', ...event, channelId }, channelId);
|
|
586
597
|
}
|
|
587
598
|
};
|
|
588
|
-
|
|
599
|
+
// 2026-10-02 (K8): 经唯一入口 `runExecution` —— 参数与派发**完全一致** ⇒ 按构造零行为改变
|
|
600
|
+
const fullResponse = await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: markedPrompt, onStream: streamCallback, channelId }));
|
|
589
601
|
if (!fullResponse.trim())
|
|
590
602
|
return;
|
|
591
603
|
// 广播续看结果给 UI
|
|
@@ -913,7 +925,8 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
913
925
|
}
|
|
914
926
|
};
|
|
915
927
|
const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
|
|
916
|
-
|
|
928
|
+
// 2026-10-02 (K8): 同上 (零差量)
|
|
929
|
+
fullResponse = await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: fullPrompt, onStream: streamCallback, channelId }));
|
|
917
930
|
// 2026-07-06: 防御性兜底
|
|
918
931
|
if (!fullResponse.trim()) {
|
|
919
932
|
fullResponse = '⚠️ AI 未返回内容, 请重试';
|
|
@@ -1065,7 +1078,8 @@ async function handleV3P2PMessage(parsed, conn, comm) {
|
|
|
1065
1078
|
// 临时用 channelId = "__collab__" 拿个一次性 agent
|
|
1066
1079
|
const collabChannelId = `__collab_${senderKey.substring(0, 8)}__`;
|
|
1067
1080
|
const agent = await getAgentForChannel(collabChannelId, '', `collab-${senderKey.substring(0, 8)}`, undefined);
|
|
1068
|
-
|
|
1081
|
+
// 2026-10-02 (K8): 经唯一入口 (零差量: channelId 就是取这个 agent 用的 collabChannelId)
|
|
1082
|
+
const resultText = await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: fullPrompt, channelId: collabChannelId }));
|
|
1069
1083
|
const reply = JSON.stringify({
|
|
1070
1084
|
v: 3,
|
|
1071
1085
|
op: 'agent.collab.reply',
|
|
@@ -1679,7 +1693,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1679
1693
|
// 跨预算继续: 新 Run 必须挂在同一个 Goal 下 (setGoalId), 并带上上一个 Run 的非幂等守卫
|
|
1680
1694
|
agent.setGoalId?.(goal.goalId);
|
|
1681
1695
|
agent.setContinuationGuards?.(r.guards || []);
|
|
1682
|
-
|
|
1696
|
+
// 2026-10-02 (K8): 经唯一入口 (零差量: agent 本来就是按 goal.channelId 解析的)
|
|
1697
|
+
const reply = await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: r.instruction, channelId: goal.channelId }));
|
|
1683
1698
|
// 注意: prompt 收尾会清空 currentRunId → 必须读 lastRunId (否则会拿上一条 run 做决策)
|
|
1684
1699
|
const runId = agent.getLastRunId?.() || agent.getRunId?.();
|
|
1685
1700
|
return { runId, status: 'done', reply: typeof reply === 'string' ? reply.slice(0, 500) : undefined };
|
|
@@ -1791,6 +1806,9 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
1791
1806
|
console.warn('[mcp] 初始化失败 (非致命):', e?.message?.slice(0, 120));
|
|
1792
1807
|
}
|
|
1793
1808
|
})();
|
|
1809
|
+
// 2026-07-28: 用户 DID 身份端点 — 静默生成/加载, 持久化到 ~/.bolloon/identity/user.json
|
|
1810
|
+
let userIdentityCache = null;
|
|
1811
|
+
const IDENTITY_DIR = `${process.env.HOME || '/tmp'}/.bolloon/identity`;
|
|
1794
1812
|
// 2026-08-08: DID 目录启动接入 — 回填既有磁盘数据 + 启动 OrbitDB 自动复制.
|
|
1795
1813
|
// fire-and-forget: 不阻塞启动; 失败静默 (catalog 是增强层, 原磁盘路径永远可用).
|
|
1796
1814
|
(async () => {
|
|
@@ -2489,7 +2507,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2489
2507
|
new Promise((res) => setTimeout(() => res(null), 8000)),
|
|
2490
2508
|
]);
|
|
2491
2509
|
if (agent && typeof agent.promptStream === 'function') {
|
|
2492
|
-
|
|
2510
|
+
// 2026-10-02 (K8): 经唯一入口 (零差量: channelId=local.id 与旧调用同一个)
|
|
2511
|
+
llm = (p) => deliverThroughActor(agent, () => requireRunExecution(agent)({ input: p, onStream: () => { }, channelId: local.id }));
|
|
2493
2512
|
}
|
|
2494
2513
|
}
|
|
2495
2514
|
}
|
|
@@ -2541,7 +2560,8 @@ export async function createWebServer(port = 3000, options = {}) {
|
|
|
2541
2560
|
throw new Error('无可用 agent, 跳过定时任务');
|
|
2542
2561
|
}
|
|
2543
2562
|
// 前缀 [cron] 标记后台执行来源 (供审计/日志区分人类消息与定时任务)
|
|
2544
|
-
|
|
2563
|
+
// 2026-10-02 (K8): 经唯一入口 (零差量: channelId=target.id)
|
|
2564
|
+
await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: `[cron] ${job.name}: ${job.prompt}`, onStream: () => { }, channelId: target.id }));
|
|
2545
2565
|
},
|
|
2546
2566
|
});
|
|
2547
2567
|
global.cronScheduler = cronHandle;
|
|
@@ -2593,7 +2613,8 @@ ${goalDesc}
|
|
|
2593
2613
|
现在是否要主动联系其中某个智能体? 只输出一个 JSON 对象, 不要任何其他文字:
|
|
2594
2614
|
{"initiate": true 或 false, "goalAchieved": true 或 false, "targetPeerPublicKey": "对方 pk", "targetChannelId": "对方渠道 id", "message": "你要说的话"}
|
|
2595
2615
|
若不想发起, 输出 {"initiate": false}。`;
|
|
2596
|
-
|
|
2616
|
+
// 2026-10-02 (K8): 经唯一入口 (零差量: channelId=local.id)
|
|
2617
|
+
const raw = await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: prompt, onStream: () => { }, channelId: local.id }));
|
|
2597
2618
|
const m = raw.match(/\{[\s\S]*\}/);
|
|
2598
2619
|
if (!m)
|
|
2599
2620
|
return { initiate: false };
|
|
@@ -3406,7 +3427,16 @@ fetchState();
|
|
|
3406
3427
|
if (!woke) {
|
|
3407
3428
|
// 没在等外部事件也要如实回答 (可能它其实在等人/在跑), 但允许显式加急
|
|
3408
3429
|
if (req.body?.force) {
|
|
3409
|
-
|
|
3430
|
+
// **K4**: channel 不直接写 Goal —— 提交控制请求, 由内核控制面执行
|
|
3431
|
+
const { submitRunControl } = await import('../kernel/control.js');
|
|
3432
|
+
const outcome = await submitRunControl({
|
|
3433
|
+
kind: 'wake-goal', origin: 'web', goalId, reason: 'force 加急唤醒',
|
|
3434
|
+
payload: { wakeReason: 'active', autoContinue: true, wakeAt: undefined, needsExternal: undefined },
|
|
3435
|
+
}, { setContinuation });
|
|
3436
|
+
if (!outcome.ok) {
|
|
3437
|
+
res.status(500).json({ error: `控制面拒绝: ${outcome.detail}` });
|
|
3438
|
+
return;
|
|
3439
|
+
}
|
|
3410
3440
|
res.json({ ok: true, goalId, woke: false, forced: true, note: `原状态 ${g.status} (不在等外部事件), 已按 force 加急` });
|
|
3411
3441
|
return;
|
|
3412
3442
|
}
|
|
@@ -3467,12 +3497,17 @@ fetchState();
|
|
|
3467
3497
|
let boundary = view.runBoundary;
|
|
3468
3498
|
if (boundary.action === 'stop_running_run' && !boundary.stopped) {
|
|
3469
3499
|
const { setRunStatus } = await import('../agents/run-store.js');
|
|
3500
|
+
const { submitRunControl } = await import('../kernel/control.js');
|
|
3470
3501
|
boundary = await seam.applyRunBoundary({
|
|
3471
3502
|
plan: view.plan,
|
|
3472
3503
|
now,
|
|
3473
3504
|
stop: async (i) => {
|
|
3474
|
-
|
|
3475
|
-
|
|
3505
|
+
// **K4**: 提交控制请求 (端口返回值里的"拒绝"会被控制面归一化成 ok=false + detail)
|
|
3506
|
+
const outcome = await submitRunControl({
|
|
3507
|
+
kind: 'set-run-status', origin: 'web', runId: i.runId, reason: `变更注入 (${i.goalId})`,
|
|
3508
|
+
payload: { status: i.runStatus, error: `变更注入 (${i.goalId}): ${String(i.reason).slice(0, 120)}` },
|
|
3509
|
+
}, { setRunStatus });
|
|
3510
|
+
return { ok: outcome.ok, reason: outcome.ok ? '' : String(outcome.detail ?? '') };
|
|
3476
3511
|
},
|
|
3477
3512
|
});
|
|
3478
3513
|
}
|
|
@@ -3606,7 +3641,19 @@ fetchState();
|
|
|
3606
3641
|
res.status(409).json({ error: `只有 needs_human 的运行需要批准 (当前 ${run.status})` });
|
|
3607
3642
|
return;
|
|
3608
3643
|
}
|
|
3609
|
-
|
|
3644
|
+
// **K4**: channel 不直接写 Run —— 提交控制请求, 由内核控制面执行 (这里只做 ports wiring)
|
|
3645
|
+
const { submitRunControl } = await import('../kernel/control.js');
|
|
3646
|
+
const outcome = await submitRunControl({
|
|
3647
|
+
kind: 'record-recovery',
|
|
3648
|
+
origin: 'web',
|
|
3649
|
+
runId,
|
|
3650
|
+
reason: '人工批准后继续',
|
|
3651
|
+
payload: { errorClass: run.errorClass || 'unknown', message: '人工批准后继续', action: 'resume' },
|
|
3652
|
+
}, { recordRecovery });
|
|
3653
|
+
if (!outcome.ok) {
|
|
3654
|
+
res.status(500).json({ error: `控制面拒绝: ${outcome.detail}` });
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3610
3657
|
if (!run.channelId) {
|
|
3611
3658
|
res.status(400).json({ error: '该 run 没有 channelId, 请在 CLI 用 /approve' });
|
|
3612
3659
|
return;
|
|
@@ -3630,9 +3677,11 @@ fetchState();
|
|
|
3630
3677
|
const runId = String(req.params.runId);
|
|
3631
3678
|
try {
|
|
3632
3679
|
const { setRunStatus } = await import('../agents/run-store.js');
|
|
3633
|
-
const
|
|
3634
|
-
|
|
3635
|
-
|
|
3680
|
+
const { submitRunControl } = await import('../kernel/control.js');
|
|
3681
|
+
const outcome = await submitRunControl({ kind: 'set-run-status', origin: 'web', runId, reason: `外部请求 (${suffix})`, payload: { status: to, error: `外部请求 (${suffix})` } }, { setRunStatus });
|
|
3682
|
+
// 语义不变: 端口/store 拒绝 ⇒ 409 (原因由控制面归一化进 detail)
|
|
3683
|
+
if (!outcome.ok) {
|
|
3684
|
+
res.status(409).json({ error: String(outcome.detail ?? '状态迁移被拒绝').replace(/^端口拒绝: /, '') });
|
|
3636
3685
|
return;
|
|
3637
3686
|
}
|
|
3638
3687
|
res.json({ ok: true, runId, status: to, note: '运行中的 agent 会在下一轮循环检查时停下 (轮内不打断)' });
|
|
@@ -3911,7 +3960,8 @@ fetchState();
|
|
|
3911
3960
|
req.docContent ? `\n资料内容:\n${req.docContent}` : '',
|
|
3912
3961
|
`\n(来自 agent ${req.fromAgentId || 'unknown'}),请直接给出可交付结果。`,
|
|
3913
3962
|
].join('');
|
|
3914
|
-
|
|
3963
|
+
// 2026-10-02 (K8): 经唯一入口 (零差量: agent 就是按 ch.id 取的)
|
|
3964
|
+
const out = await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: task, channelId: ch.id }));
|
|
3915
3965
|
const text = String(out || '').trim();
|
|
3916
3966
|
if (!text || text.startsWith('❌') || text.startsWith('[AI 服务调用失败]')) {
|
|
3917
3967
|
return { ok: false, summary: text.slice(0, 800) || '(空结果)', error: 'execution-failed' };
|
|
@@ -4034,6 +4084,8 @@ fetchState();
|
|
|
4034
4084
|
const r = await createGroup(String(req.body?.name || '').trim(), {
|
|
4035
4085
|
from: String(req.body?.from || '').trim() || undefined,
|
|
4036
4086
|
hello: req.body?.hello ? String(req.body.hello) : undefined,
|
|
4087
|
+
// 微信式群聊: 拿到邀请链接的人都能发言 ⇒ 显式声明 (2026-10-01 起不再是隐式默认)
|
|
4088
|
+
acl: 'open',
|
|
4037
4089
|
});
|
|
4038
4090
|
if (!r.ok)
|
|
4039
4091
|
return res.status(400).json({ error: r.error });
|
|
@@ -4280,8 +4332,11 @@ fetchState();
|
|
|
4280
4332
|
res.json([]); // tool-manifest 已废弃, 工具列表由 pi-sdk.ts this.tools 维护
|
|
4281
4333
|
});
|
|
4282
4334
|
// 2026-07-28: 用户 DID 身份端点 — 静默生成/加载, 持久化到 ~/.bolloon/identity/user.json
|
|
4283
|
-
|
|
4284
|
-
|
|
4335
|
+
// 2026-10-02: **声明位置有硬约束** —— 必须早于下面 did-catalog 的启动 IIFE。
|
|
4336
|
+
// 踩过一次: `loadOrCreateUserIdentity()` 在 did-catalog 的 IIFE (1876 附近) 里被 `await` 调用,
|
|
4337
|
+
// 而 `let` 声明在 4200+ 行之外 ⇒ 微任务先跑到那次调用 ⇒ **TDZ** ("Cannot access 'userIdentityCache'
|
|
4338
|
+
// before initialization") ⇒ 开机时 **OrbitDB 复制根本没起来**, 只在日志里留了一条"非致命"。
|
|
4339
|
+
// 修法: 把声明挪到**首次使用之前** (同作用域, 语义不变); 位置由下面的门锁住。
|
|
4285
4340
|
async function loadOrCreateUserIdentity() {
|
|
4286
4341
|
if (userIdentityCache)
|
|
4287
4342
|
return userIdentityCache;
|
|
@@ -4870,925 +4925,844 @@ fetchState();
|
|
|
4870
4925
|
: channel?.autoInvokeTools !== false;
|
|
4871
4926
|
// 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
|
|
4872
4927
|
const channelForJudgment = channel;
|
|
4873
|
-
//
|
|
4928
|
+
// K8 正刀 (2026-10-02): **每条消息都投内核邮箱** —— 取代原来的 check-then-set + 通道自带的 queue。
|
|
4929
|
+
// 为什么 (三条, 按重要性):
|
|
4930
|
+
// (a) 同通道串行交给内核邮箱 (与 K5 同一条语义), 通道不再兼任调度器;
|
|
4931
|
+
// (b) 消灭 `finishChannelRun` **同步**置 `running = false` 后**异步** handoff 造成的**静默丢消息**窗口
|
|
4932
|
+
// (先行门 `src/test/k8-mailbox-scheduling.test.ts` ③ 已把该机制等价复刻并判红);
|
|
4933
|
+
// (c) 过去排队消息只能走 `runMessageFromQueue` 的**简化版** (无 judgment hint / persona / slash / 附件上下文),
|
|
4934
|
+
// 现在每条消息都走**同一条**完整路径 —— 这是行为改善, **不是零差量** (已在 log 如实记)。
|
|
4874
4935
|
const runState = getOrCreateRunState(channelId);
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4936
|
+
const runChannelMessage = async () => {
|
|
4937
|
+
runState.running = true; // 观测口径 (串行权威已归邮箱)
|
|
4938
|
+
// K8 收尾 (2026-10-02): **中止位归内核 actor** —— `beginCancellation()` 每次执行开一个新 controller
|
|
4939
|
+
// (与邮箱串行一致), web 不再自己持 abortController。取到 agent 后立刻开户 (见下面 getAgentForChannel 之后)。
|
|
4940
|
+
let abortSignal;
|
|
4941
|
+
// 2026-07-04: pivot loop safety net — 防止 LLM hang (minimax M3
|
|
4942
|
+
// 偶尔反复 think 不输出 <final gen>, pivot 连 5 次无进展时会 hang 在
|
|
4943
|
+
// quality 评估). setTimeout 让 LLM 客户端收 signal 主动 break.
|
|
4944
|
+
// 2026-07-06: 30s 太短 — minimax M3 单次 LLM 约 10s, pivot 默认 moderate profile 30 iter
|
|
4945
|
+
// 留 ~300s. 之前 30s 自动 abort 时, 一次"你好"调用就被截断, 用户看到 ❌ 循环异常 + 空内容.
|
|
4946
|
+
// 改为 5 分钟 = 5x 实际耗时上限.
|
|
4947
|
+
const PIVOT_FORCE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
4948
|
+
const forceTimeout = setTimeout(() => {
|
|
4949
|
+
console.warn(`[server] /message pivot 强制 timeout (${PIVOT_FORCE_TIMEOUT_MS}ms), aborting`);
|
|
4950
|
+
// K8 收尾: 中止经 actor; 拿不到 actor 就**如实记**"没能中止" (不静默)
|
|
4951
|
+
const act = agent?.actor;
|
|
4952
|
+
if (act?.abort)
|
|
4953
|
+
act.abort('pivot 强制 timeout');
|
|
4954
|
+
else
|
|
4955
|
+
console.warn('[K8] 强制 timeout 时没有 actor ⇒ 本次没能中止 (如实记)');
|
|
4956
|
+
}, PIVOT_FORCE_TIMEOUT_MS);
|
|
4885
4957
|
broadcastQueueUpdate(channelId);
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
// 2026-07-01 (v0.2.5): hoist sessionKey 到 try 外, 让 finally 块的 saveCurrentSession 能用
|
|
4904
|
-
const sessionKey = `${channelId}:${currentSessionId}`;
|
|
4905
|
-
let agent = null;
|
|
4906
|
-
try {
|
|
4907
|
-
agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
4908
|
-
let fullResponse = '';
|
|
4909
|
-
// P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
|
|
4910
|
-
let usedJudgmentIds = [];
|
|
4911
|
-
const streamCallback = (event) => {
|
|
4912
|
-
// P0.5: 捕获注入门回传
|
|
4913
|
-
if (event.type === 'used_judgments' && Array.isArray(event.usedIds)) {
|
|
4914
|
-
usedJudgmentIds = event.usedIds;
|
|
4915
|
-
// 同步推给前端 (用于 finalizeTimelineAsMessage 时给 addMessage 传 usedIds)
|
|
4916
|
-
broadcast({ type: 'used_judgments', usedIds: usedJudgmentIds }, channelId);
|
|
4917
|
-
return;
|
|
4918
|
-
}
|
|
4919
|
-
// 阶段事件 (注入门 / D 触发)
|
|
4920
|
-
if (event.type === 'phase') {
|
|
4921
|
-
broadcast({
|
|
4922
|
-
type: 'phase',
|
|
4923
|
-
phase: event.phase,
|
|
4924
|
-
detail: event.detail,
|
|
4925
|
-
usedCount: event.usedCount,
|
|
4926
|
-
}, channelId);
|
|
4927
|
-
return;
|
|
4928
|
-
}
|
|
4929
|
-
// 同时发送给流式显示和工作流显示
|
|
4930
|
-
if (event.type === 'token' || event.type === 'thinking') {
|
|
4931
|
-
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
4932
|
-
// 同时作为 workflow_step 显示(用于动态 loop 循环)
|
|
4933
|
-
if (event.content) {
|
|
4934
|
-
broadcast({ type: 'workflow_step', step: 'AI 思考', content: event.content.substring(0, 100) }, channelId);
|
|
4958
|
+
// 2026-07-01 (v0.2.5): hoist sessionKey 到 try 外, 让 finally 块的 saveCurrentSession 能用
|
|
4959
|
+
const sessionKey = `${channelId}:${currentSessionId}`;
|
|
4960
|
+
let agent = null;
|
|
4961
|
+
try {
|
|
4962
|
+
agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
|
|
4963
|
+
// K8 收尾: 这一次执行的取消位由 actor 开 (web 只持有它返回的 signal)
|
|
4964
|
+
abortSignal = agent.actor ? agent.actor.beginCancellation() : undefined;
|
|
4965
|
+
let fullResponse = '';
|
|
4966
|
+
// P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
|
|
4967
|
+
let usedJudgmentIds = [];
|
|
4968
|
+
const streamCallback = (event) => {
|
|
4969
|
+
// P0.5: 捕获注入门回传
|
|
4970
|
+
if (event.type === 'used_judgments' && Array.isArray(event.usedIds)) {
|
|
4971
|
+
usedJudgmentIds = event.usedIds;
|
|
4972
|
+
// 同步推给前端 (用于 finalizeTimelineAsMessage 时给 addMessage 传 usedIds)
|
|
4973
|
+
broadcast({ type: 'used_judgments', usedIds: usedJudgmentIds }, channelId);
|
|
4974
|
+
return;
|
|
4935
4975
|
}
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
}
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
// — 用户报告"进程看不到": 本地智能体执行过程 (即使没调工具, 只有 status/tool 事件)
|
|
4953
|
-
// 也要在 P2P 对话框显示, 让用户看到"进程"
|
|
4954
|
-
try {
|
|
4955
|
-
const rs = channelRunState.get(channelId);
|
|
4956
|
-
if (rs?.remoteFollowup?.remoteChannelId) {
|
|
4957
|
-
broadcast({
|
|
4958
|
-
type: 'remote-chat-step',
|
|
4959
|
-
channelId: rs.remoteFollowup.remoteChannelId,
|
|
4960
|
-
stepType: 'step_start',
|
|
4961
|
-
tool: event.tool || '工作流',
|
|
4962
|
-
content: event.content,
|
|
4963
|
-
localStep: true,
|
|
4964
|
-
});
|
|
4976
|
+
// 阶段事件 (注入门 / D 触发)
|
|
4977
|
+
if (event.type === 'phase') {
|
|
4978
|
+
broadcast({
|
|
4979
|
+
type: 'phase',
|
|
4980
|
+
phase: event.phase,
|
|
4981
|
+
detail: event.detail,
|
|
4982
|
+
usedCount: event.usedCount,
|
|
4983
|
+
}, channelId);
|
|
4984
|
+
return;
|
|
4985
|
+
}
|
|
4986
|
+
// 同时发送给流式显示和工作流显示
|
|
4987
|
+
if (event.type === 'token' || event.type === 'thinking') {
|
|
4988
|
+
broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
|
|
4989
|
+
// 同时作为 workflow_step 显示(用于动态 loop 循环)
|
|
4990
|
+
if (event.content) {
|
|
4991
|
+
broadcast({ type: 'workflow_step', step: 'AI 思考', content: event.content.substring(0, 100) }, channelId);
|
|
4965
4992
|
}
|
|
4966
4993
|
}
|
|
4967
|
-
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
4995
|
-
|
|
4996
|
-
});
|
|
4994
|
+
else if (event.type === 'reply-preview') {
|
|
4995
|
+
// 2026-07-06: pivot 每 iter 把完整 reply 推给前端 — 前端可以更新临时气泡
|
|
4996
|
+
// 等 loop 退完 type=ai 终文事件覆盖. 不依赖整个 promptWithPivotLoop 返回.
|
|
4997
|
+
// 这样 pivot 中途网络慢 / 5min-long 任务, 用户看到中间内容不是空白 + '任务处理超时'.
|
|
4998
|
+
broadcast({
|
|
4999
|
+
type: 'reply-preview',
|
|
5000
|
+
content: event.content,
|
|
5001
|
+
iteration: event.iteration,
|
|
5002
|
+
}, channelId);
|
|
5003
|
+
}
|
|
5004
|
+
else if (event.type === 'status' || event.type === 'tool') {
|
|
5005
|
+
broadcast({ type: 'status', tool: event.tool, content: event.content }, channelId);
|
|
5006
|
+
broadcast({ type: 'workflow_step', step: event.tool || '系统', content: event.content }, channelId);
|
|
5007
|
+
console.log(`[SSE 广播] workflow_step: step=${event.tool}, content="${event.content?.substring(0, 80)}..."`);
|
|
5008
|
+
// 2026-08-02: 本地 @ 远端时, 工作流步骤也推给 P2P 对话框 (rcm-log)
|
|
5009
|
+
// — 用户报告"进程看不到": 本地智能体执行过程 (即使没调工具, 只有 status/tool 事件)
|
|
5010
|
+
// 也要在 P2P 对话框显示, 让用户看到"进程"
|
|
5011
|
+
try {
|
|
5012
|
+
const rs = channelRunState.get(channelId);
|
|
5013
|
+
if (rs?.remoteFollowup?.remoteChannelId) {
|
|
5014
|
+
broadcast({
|
|
5015
|
+
type: 'remote-chat-step',
|
|
5016
|
+
channelId: rs.remoteFollowup.remoteChannelId,
|
|
5017
|
+
stepType: 'step_start',
|
|
5018
|
+
tool: event.tool || '工作流',
|
|
5019
|
+
content: event.content,
|
|
5020
|
+
localStep: true,
|
|
5021
|
+
});
|
|
5022
|
+
}
|
|
4997
5023
|
}
|
|
5024
|
+
catch { /* 非致命 */ }
|
|
4998
5025
|
}
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5026
|
+
else if (event.type === 'step_start' || event.type === 'step_done' || event.type === 'step_error') {
|
|
5027
|
+
// 2026-06-15: 步骤状态机事件 — 原样转发 (前端 step-timeline 组件订阅)
|
|
5028
|
+
broadcast({
|
|
5029
|
+
type: event.type,
|
|
5030
|
+
tool: event.tool,
|
|
5031
|
+
content: event.content,
|
|
5032
|
+
success: event.success,
|
|
5033
|
+
output: event.output,
|
|
5034
|
+
error: event.error,
|
|
5035
|
+
args: event.args,
|
|
5036
|
+
}, channelId);
|
|
5037
|
+
// 2026-08-02: 对称显示 — 本地智能体 @ 远端时, 本地工具调用过程也推给 P2P 对话框
|
|
5038
|
+
// (rcm-log): remote-chat-step 事件带 remoteChannelId, 前端按对话框匹配显示
|
|
5039
|
+
try {
|
|
5003
5040
|
const rs = channelRunState.get(channelId);
|
|
5004
|
-
if (rs) {
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5041
|
+
if (rs?.remoteFollowup?.remoteChannelId) {
|
|
5042
|
+
broadcast({
|
|
5043
|
+
type: 'remote-chat-step',
|
|
5044
|
+
channelId: rs.remoteFollowup.remoteChannelId,
|
|
5045
|
+
stepType: event.type,
|
|
5046
|
+
tool: event.tool,
|
|
5047
|
+
content: event.content,
|
|
5048
|
+
success: event.success,
|
|
5049
|
+
output: event.output,
|
|
5050
|
+
error: event.error,
|
|
5051
|
+
args: event.args,
|
|
5052
|
+
localStep: true, // 标记: 这是本地智能体的工具过程 (区别于对方转发的)
|
|
5012
5053
|
});
|
|
5013
5054
|
}
|
|
5014
5055
|
}
|
|
5056
|
+
catch { /* 非致命 */ }
|
|
5057
|
+
// 2026-06-16: 累积 step 到 runState, 供 /api/loop/inspect 读取
|
|
5058
|
+
try {
|
|
5059
|
+
if (event.type === 'step_done' || event.type === 'step_error') {
|
|
5060
|
+
const rs = channelRunState.get(channelId);
|
|
5061
|
+
if (rs) {
|
|
5062
|
+
if (!rs.lastSteps)
|
|
5063
|
+
rs.lastSteps = [];
|
|
5064
|
+
rs.lastSteps.push({
|
|
5065
|
+
name: String(event.tool || event.content || 'step').slice(0, 60),
|
|
5066
|
+
status: event.type === 'step_error' ? 'failed' : (event.success === false ? 'failed' : 'ok'),
|
|
5067
|
+
durationMs: typeof event.durationMs === 'number' ? event.durationMs : undefined,
|
|
5068
|
+
output: event.output ? String(event.output).slice(0, 800) : (event.error ? String(event.error).slice(0, 800) : undefined),
|
|
5069
|
+
});
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
catch { /* non-fatal */ }
|
|
5074
|
+
}
|
|
5075
|
+
else if (event.type === 'error') {
|
|
5076
|
+
broadcast({ type: 'error', content: event.content }, channelId);
|
|
5077
|
+
}
|
|
5078
|
+
};
|
|
5079
|
+
console.log(`[消息处理] 开始处理用户消息, channelId: ${channelId}, sessionId: ${currentSessionId}`);
|
|
5080
|
+
// 2026-08-02 fix: 预激活远端协作续看 — 用户消息含 @远端 时立即激活 remoteFollowup.
|
|
5081
|
+
// 之前只在 routeMentionsInReply (AI 回复后) 激活 → 首次 @ 时本地智能体的工具调用
|
|
5082
|
+
// (step 事件) 发生在激活前, P2P 对话框看不到本地工具过程 (用户报告"进程看不到").
|
|
5083
|
+
// 现在收到消息即激活 → promptStream 期间的工具 step 也能 broadcast remote-chat-step.
|
|
5084
|
+
try {
|
|
5085
|
+
const mentionMatch = /@([一-龥A-Za-z0-9_\-]{1,30})/.exec(text);
|
|
5086
|
+
if (mentionMatch && remoteChannelCache.size > 0) {
|
|
5087
|
+
const targetName = mentionMatch[1];
|
|
5088
|
+
let hitRemoteId = null;
|
|
5089
|
+
for (const list of remoteChannelCache.values()) {
|
|
5090
|
+
const rc = list.find((c) => c.name === targetName);
|
|
5091
|
+
if (rc) {
|
|
5092
|
+
hitRemoteId = String(rc.id);
|
|
5093
|
+
break;
|
|
5094
|
+
}
|
|
5095
|
+
}
|
|
5096
|
+
if (hitRemoteId) {
|
|
5097
|
+
const rs = channelRunState.get(channelId);
|
|
5098
|
+
if (rs && !rs.remoteFollowup) {
|
|
5099
|
+
rs.remoteFollowup = { rounds: 0, maxRounds: 3, remoteChannelId: hitRemoteId };
|
|
5100
|
+
console.log(`[v3-followup] ${channelId} 预激活远端协作续看 → ${hitRemoteId} (消息含 @${targetName})`);
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5015
5103
|
}
|
|
5016
|
-
catch { /* non-fatal */ }
|
|
5017
5104
|
}
|
|
5018
|
-
|
|
5019
|
-
|
|
5105
|
+
catch { /* 预激活失败不阻塞 */ }
|
|
5106
|
+
// 将真实 DID 作为上下文前缀,让 AI 使用真实的 DID 而不是自己编造的
|
|
5107
|
+
let contextHint = '';
|
|
5108
|
+
// 2026-08-14: 本地消息带 gateway 链接 → 自动加入, 结果注入 agent 上下文 (5s 上限, 不阻塞 LLM)
|
|
5109
|
+
try {
|
|
5110
|
+
const { maybeAutoJoinGateway } = await import('../agents/gateway-network.js');
|
|
5111
|
+
const gwNote = await Promise.race([
|
|
5112
|
+
maybeAutoJoinGateway(text),
|
|
5113
|
+
new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
|
|
5114
|
+
]);
|
|
5115
|
+
if (gwNote)
|
|
5116
|
+
contextHint += `[系统上下文] ${gwNote}\n`;
|
|
5117
|
+
}
|
|
5118
|
+
catch { /* 自动加入失败不阻塞 */ }
|
|
5119
|
+
// 2026-08-02: slash 命令提示 (在 /message 开头解析, 这里注入)
|
|
5120
|
+
if (slashCommandHint)
|
|
5121
|
+
contextHint += slashCommandHint;
|
|
5122
|
+
if (realChannelDid)
|
|
5123
|
+
contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
|
|
5124
|
+
// v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
|
|
5125
|
+
contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
|
|
5126
|
+
if (boundWalletAddress) {
|
|
5127
|
+
contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
|
|
5128
|
+
}
|
|
5129
|
+
if (autoToolsEnabled) {
|
|
5130
|
+
contextHint += `[系统上下文] 自动工具调用已开启: 你可以使用受信任的本地工具 (shell / 文件 / skill) 而无需逐次询问用户。\n`;
|
|
5020
5131
|
}
|
|
5021
|
-
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
const
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
const rc = list.find((c) => c.name === targetName);
|
|
5034
|
-
if (rc) {
|
|
5035
|
-
hitRemoteId = String(rc.id);
|
|
5036
|
-
break;
|
|
5132
|
+
else {
|
|
5133
|
+
contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
|
|
5134
|
+
}
|
|
5135
|
+
// 2026-08-02 fix: 本地路径注入远端 channel 目录 (dirHint) — 之前只有远端路径 (agent.chat.send)
|
|
5136
|
+
// 有 dirHint, 本地智能体对话时看不到远端 channel 列表 → 无法 @ 远程智能体交流.
|
|
5137
|
+
// 现在注入: 可用渠道列表 (本地 + 远端), 让 LLM 知道 @ 谁、发什么.
|
|
5138
|
+
try {
|
|
5139
|
+
const localChs = await loadChannels();
|
|
5140
|
+
const remoteForDir = [];
|
|
5141
|
+
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
5142
|
+
for (const ch of list) {
|
|
5143
|
+
remoteForDir.push({ ...ch, _ownerPublicKey: peerPk });
|
|
5037
5144
|
}
|
|
5038
5145
|
}
|
|
5039
|
-
if (
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5146
|
+
if (localChs.length > 0 || remoteForDir.length > 0) {
|
|
5147
|
+
contextHint += '[系统上下文] 可用渠道 (你可以在回复中写 "@渠道名 消息内容" 给它们发消息, 消息会持久化到目标 channel 的 session):\n';
|
|
5148
|
+
for (const c of localChs) {
|
|
5149
|
+
if (c.id === channelId)
|
|
5150
|
+
continue; // 跳过自己
|
|
5151
|
+
contextHint += ` - [本地] @${c.name} (id=${c.id})\n`;
|
|
5044
5152
|
}
|
|
5153
|
+
for (const c of remoteForDir) {
|
|
5154
|
+
contextHint += ` - [远端, owner=${(c._ownerPublicKey || '').substring(0, 8)}…] @${c.name} (id=${c.id})\n`;
|
|
5155
|
+
}
|
|
5156
|
+
contextHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可, 系统会自动转发。\n\n';
|
|
5045
5157
|
}
|
|
5046
5158
|
}
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
// 将真实 DID 作为上下文前缀,让 AI 使用真实的 DID 而不是自己编造的
|
|
5050
|
-
let contextHint = '';
|
|
5051
|
-
// 2026-08-14: 本地消息带 gateway 链接 → 自动加入, 结果注入 agent 上下文 (5s 上限, 不阻塞 LLM)
|
|
5052
|
-
try {
|
|
5053
|
-
const { maybeAutoJoinGateway } = await import('../agents/gateway-network.js');
|
|
5054
|
-
const gwNote = await Promise.race([
|
|
5055
|
-
maybeAutoJoinGateway(text),
|
|
5056
|
-
new Promise((resolve) => setTimeout(() => resolve(null), 5000)),
|
|
5057
|
-
]);
|
|
5058
|
-
if (gwNote)
|
|
5059
|
-
contextHint += `[系统上下文] ${gwNote}\n`;
|
|
5060
|
-
}
|
|
5061
|
-
catch { /* 自动加入失败不阻塞 */ }
|
|
5062
|
-
// 2026-08-02: slash 命令提示 (在 /message 开头解析, 这里注入)
|
|
5063
|
-
if (slashCommandHint)
|
|
5064
|
-
contextHint += slashCommandHint;
|
|
5065
|
-
if (realChannelDid)
|
|
5066
|
-
contextHint += `[系统上下文] 当前频道名称: ${realChannelName}, 你的真实 DID: ${realChannelDid}\n`;
|
|
5067
|
-
// v3 新增: 标识发送方 — 让 AI 分清内部 owner vs 远端访客
|
|
5068
|
-
contextHint += `[系统上下文] 消息来源: 本地 (channel 内部 owner / 此机器上的用户). 称呼对方时用 "你" 或 "用户" 即可.\n`;
|
|
5069
|
-
if (boundWalletAddress) {
|
|
5070
|
-
contextHint += `[系统上下文] 已绑定的加密钱包地址: ${boundWalletAddress}。当用户授权或启用自动工具调用时, 可使用该地址发起链上操作。\n`;
|
|
5071
|
-
}
|
|
5072
|
-
if (autoToolsEnabled) {
|
|
5073
|
-
contextHint += `[系统上下文] 自动工具调用已开启: 你可以使用受信任的本地工具 (shell / 文件 / skill) 而无需逐次询问用户。\n`;
|
|
5074
|
-
}
|
|
5075
|
-
else {
|
|
5076
|
-
contextHint += `[系统上下文] 自动工具调用已关闭: 每次执行工具前必须先与用户确认。\n`;
|
|
5077
|
-
}
|
|
5078
|
-
// 2026-08-02 fix: 本地路径注入远端 channel 目录 (dirHint) — 之前只有远端路径 (agent.chat.send)
|
|
5079
|
-
// 有 dirHint, 本地智能体对话时看不到远端 channel 列表 → 无法 @ 远程智能体交流.
|
|
5080
|
-
// 现在注入: 可用渠道列表 (本地 + 远端), 让 LLM 知道 @ 谁、发什么.
|
|
5081
|
-
try {
|
|
5082
|
-
const localChs = await loadChannels();
|
|
5083
|
-
const remoteForDir = [];
|
|
5084
|
-
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
5085
|
-
for (const ch of list) {
|
|
5086
|
-
remoteForDir.push({ ...ch, _ownerPublicKey: peerPk });
|
|
5087
|
-
}
|
|
5159
|
+
catch (dirErr) {
|
|
5160
|
+
// 静默 — dirHint 不是核心
|
|
5088
5161
|
}
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5162
|
+
// v3: 注入 channel 绑定的判断力 (judgment_ids)
|
|
5163
|
+
// 这是 v3 的核心 — channel 跑 LLM 时, 它的判断力 = 绑定的 judgment 列表
|
|
5164
|
+
const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
|
|
5165
|
+
if (judgmentHint)
|
|
5166
|
+
contextHint += judgmentHint;
|
|
5167
|
+
// 2026-06-10: 注入 skills 列表 (本机 ~/.bolloon/skills/ 下所有 skills)
|
|
5168
|
+
// 让 LLM 知道有哪些 skill 可用, 在回复中提示用户
|
|
5169
|
+
try {
|
|
5170
|
+
const { loadSkillsFromPaths, defaultSkillPaths, describeSkill } = await import('../agents/skill-loader.js');
|
|
5171
|
+
const paths = defaultSkillPaths();
|
|
5172
|
+
const skills = await loadSkillsFromPaths(paths);
|
|
5173
|
+
if (skills.length > 0) {
|
|
5174
|
+
contextHint += `[系统上下文] 本机已加载的 skills (${skills.length} 个, 你可以提示用户主动调用):\n`;
|
|
5175
|
+
for (const s of skills.slice(0, 20)) { // 上限 20 避免 prompt 过长
|
|
5176
|
+
const desc = (s.description || '').slice(0, 80);
|
|
5177
|
+
contextHint += ` - /${s.name}${desc ? ' — ' + desc : ''}\n`;
|
|
5178
|
+
}
|
|
5179
|
+
contextHint += '调用语法: 用户说 "/技能名 ..." 或 你回复时建议 "/技能名 ..." 让用户主动触发.\n\n';
|
|
5098
5180
|
}
|
|
5099
|
-
contextHint += '语法: 在回复中写 "@渠道名 我要说的话" 即可, 系统会自动转发。\n\n';
|
|
5100
5181
|
}
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
// 静默 — dirHint 不是核心
|
|
5104
|
-
}
|
|
5105
|
-
// v3: 注入 channel 绑定的判断力 (judgment_ids)
|
|
5106
|
-
// 这是 v3 的核心 — channel 跑 LLM 时, 它的判断力 = 绑定的 judgment 列表
|
|
5107
|
-
const judgmentHint = await buildJudgmentHint(channelForJudgment, channelId);
|
|
5108
|
-
if (judgmentHint)
|
|
5109
|
-
contextHint += judgmentHint;
|
|
5110
|
-
// 2026-06-10: 注入 skills 列表 (本机 ~/.bolloon/skills/ 下所有 skills)
|
|
5111
|
-
// 让 LLM 知道有哪些 skill 可用, 在回复中提示用户
|
|
5112
|
-
try {
|
|
5113
|
-
const { loadSkillsFromPaths, defaultSkillPaths, describeSkill } = await import('../agents/skill-loader.js');
|
|
5114
|
-
const paths = defaultSkillPaths();
|
|
5115
|
-
const skills = await loadSkillsFromPaths(paths);
|
|
5116
|
-
if (skills.length > 0) {
|
|
5117
|
-
contextHint += `[系统上下文] 本机已加载的 skills (${skills.length} 个, 你可以提示用户主动调用):\n`;
|
|
5118
|
-
for (const s of skills.slice(0, 20)) { // 上限 20 避免 prompt 过长
|
|
5119
|
-
const desc = (s.description || '').slice(0, 80);
|
|
5120
|
-
contextHint += ` - /${s.name}${desc ? ' — ' + desc : ''}\n`;
|
|
5121
|
-
}
|
|
5122
|
-
contextHint += '调用语法: 用户说 "/技能名 ..." 或 你回复时建议 "/技能名 ..." 让用户主动触发.\n\n';
|
|
5182
|
+
catch (err) {
|
|
5183
|
+
// 静默失败 — skills 不是核心, 加载失败不阻塞
|
|
5123
5184
|
}
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
const decision = (j.decision || '').slice(0, 80);
|
|
5138
|
-
contextHint += ` - ${decision}\n`;
|
|
5185
|
+
// 2026-06-10: 注入 human values 摘要 (最常用的 judgment / 价值偏好)
|
|
5186
|
+
// 与 judgment 不同: values 是更宏观的"用户偏好", judgment 是针对具体决策的约束
|
|
5187
|
+
try {
|
|
5188
|
+
const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
|
|
5189
|
+
const allJudgments = await loadAllJudgments().catch(() => []);
|
|
5190
|
+
// 把所有 judgment 视作软参考 (跟 buildJudgmentHint 的 candidates 同理)
|
|
5191
|
+
if (Array.isArray(allJudgments) && allJudgments.length > 0) {
|
|
5192
|
+
contextHint += `[系统上下文] 用户的核心价值倾向 (来自 ${allJudgments.length} 条历史 judgment, 软参考, 体现而非复述):\n`;
|
|
5193
|
+
for (const j of allJudgments.slice(0, 8)) {
|
|
5194
|
+
const decision = (j.decision || '').slice(0, 80);
|
|
5195
|
+
contextHint += ` - ${decision}\n`;
|
|
5196
|
+
}
|
|
5197
|
+
contextHint += '\n';
|
|
5139
5198
|
}
|
|
5140
|
-
contextHint += '\n';
|
|
5141
5199
|
}
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5200
|
+
catch (err) {
|
|
5201
|
+
// 静默失败
|
|
5202
|
+
}
|
|
5203
|
+
// 2026-06-10: 注入 documents 列表 (本机 documents/ 目录的文档元数据)
|
|
5204
|
+
// 让 LLM 知道有文档存在, 用户可主动要求读
|
|
5205
|
+
try {
|
|
5206
|
+
const { documentStore } = await import('../documents/store.js');
|
|
5207
|
+
const docs = await documentStore.getReceivedDocuments(50).catch(() => []);
|
|
5208
|
+
if (Array.isArray(docs) && docs.length > 0) {
|
|
5209
|
+
contextHint += `[系统上下文] 本机 documents (${docs.length} 篇, 用户可让你读):\n`;
|
|
5210
|
+
for (const d of docs.slice(0, 10)) {
|
|
5211
|
+
const name = d.fileName || d.id || '(未命名)';
|
|
5212
|
+
const size = d.fileSize ? ` (${Math.round(d.fileSize / 1024)}KB)` : '';
|
|
5213
|
+
const sender = d.fromNodeId ? ` [来自 ${d.fromNodeIdShort || d.fromNodeId.substring(0, 8)}…]` : '';
|
|
5214
|
+
contextHint += ` - ${name}${size}${sender}\n`;
|
|
5215
|
+
}
|
|
5216
|
+
contextHint += '用户提到某文档时, 你可以调用读文档工具读取并总结.\n\n';
|
|
5158
5217
|
}
|
|
5159
|
-
contextHint += '用户提到某文档时, 你可以调用读文档工具读取并总结.\n\n';
|
|
5160
5218
|
}
|
|
5161
|
-
|
|
5162
|
-
|
|
5163
|
-
|
|
5164
|
-
|
|
5165
|
-
|
|
5166
|
-
|
|
5167
|
-
|
|
5168
|
-
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
contextHint += ` 兴趣: ${chPersona.interests.join('、')}\n`;
|
|
5182
|
-
}
|
|
5183
|
-
contextHint += '回复时应自然体现这个角色 (不要硬搬原文, 像这个角色说话即可).\n\n';
|
|
5184
|
-
}
|
|
5185
|
-
// 2026-08-02: memory 回读 — 把本 channel 的历史记忆摘要注入 contextHint.
|
|
5186
|
-
// memory-compressor 每次 /message 后把增量摘要 append 到
|
|
5187
|
-
// ~/.bolloon/memory/<agentId>/sessions/<safe-channel>__<safe-session>.summary.md,
|
|
5188
|
-
// 但之前只写不读, 对话完全无记忆. 这里取尾部最近 3 段增量摘要回灌给 LLM.
|
|
5189
|
-
try {
|
|
5190
|
-
const { getSessionSummaryPath, getMemoryDir, sanitizeAgentId } = await import('../bootstrap/memory-compressor.js');
|
|
5191
|
-
const { readFile, readdir } = await import('fs/promises');
|
|
5192
|
-
const { join } = await import('path');
|
|
5193
|
-
const agentId = channelForJudgment?.agentId;
|
|
5194
|
-
if (agentId) {
|
|
5195
|
-
// 优先读当前 channel 的 summary (如果有)
|
|
5196
|
-
const currentSummary = getSessionSummaryPath(agentId, channelId, currentSessionId);
|
|
5197
|
-
const summariesToRead = [];
|
|
5198
|
-
try {
|
|
5199
|
-
const raw = await readFile(currentSummary, 'utf-8');
|
|
5200
|
-
if (raw.trim())
|
|
5201
|
-
summariesToRead.push(raw);
|
|
5219
|
+
catch (err) {
|
|
5220
|
+
// 静默失败
|
|
5221
|
+
}
|
|
5222
|
+
// 2026-06-11: 注入此 channel 专属的 persona + 关联文档 (从 channel 字段读, LLM 长期记忆)
|
|
5223
|
+
const chPersona = channelForJudgment?.persona;
|
|
5224
|
+
if (chPersona && typeof chPersona === 'object') {
|
|
5225
|
+
contextHint += '[系统上下文] 此 channel 的人设 (你是这个角色):\n';
|
|
5226
|
+
if (chPersona.name)
|
|
5227
|
+
contextHint += ` 名字: ${chPersona.name}\n`;
|
|
5228
|
+
if (chPersona.description)
|
|
5229
|
+
contextHint += ` 描述: ${chPersona.description}\n`;
|
|
5230
|
+
if (chPersona.personality)
|
|
5231
|
+
contextHint += ` 性格: ${chPersona.personality}\n`;
|
|
5232
|
+
if (chPersona.greeting)
|
|
5233
|
+
contextHint += ` 问候: ${chPersona.greeting}\n`;
|
|
5234
|
+
if (Array.isArray(chPersona.capabilities) && chPersona.capabilities.length > 0) {
|
|
5235
|
+
contextHint += ` 能力: ${chPersona.capabilities.join('、')}\n`;
|
|
5236
|
+
}
|
|
5237
|
+
if (Array.isArray(chPersona.interests) && chPersona.interests.length > 0) {
|
|
5238
|
+
contextHint += ` 兴趣: ${chPersona.interests.join('、')}\n`;
|
|
5202
5239
|
}
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5240
|
+
contextHint += '回复时应自然体现这个角色 (不要硬搬原文, 像这个角色说话即可).\n\n';
|
|
5241
|
+
}
|
|
5242
|
+
// 2026-08-02: memory 回读 — 把本 channel 的历史记忆摘要注入 contextHint.
|
|
5243
|
+
// memory-compressor 每次 /message 后把增量摘要 append 到
|
|
5244
|
+
// ~/.bolloon/memory/<agentId>/sessions/<safe-channel>__<safe-session>.summary.md,
|
|
5245
|
+
// 但之前只写不读, 对话完全无记忆. 这里取尾部最近 3 段增量摘要回灌给 LLM.
|
|
5246
|
+
try {
|
|
5247
|
+
const { getSessionSummaryPath, getMemoryDir, sanitizeAgentId } = await import('../bootstrap/memory-compressor.js');
|
|
5248
|
+
const { readFile, readdir } = await import('fs/promises');
|
|
5249
|
+
const { join } = await import('path');
|
|
5250
|
+
const agentId = channelForJudgment?.agentId;
|
|
5251
|
+
if (agentId) {
|
|
5252
|
+
// 优先读当前 channel 的 summary (如果有)
|
|
5253
|
+
const currentSummary = getSessionSummaryPath(agentId, channelId, currentSessionId);
|
|
5254
|
+
const summariesToRead = [];
|
|
5206
5255
|
try {
|
|
5207
|
-
const
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5256
|
+
const raw = await readFile(currentSummary, 'utf-8');
|
|
5257
|
+
if (raw.trim())
|
|
5258
|
+
summariesToRead.push(raw);
|
|
5259
|
+
}
|
|
5260
|
+
catch { /* 当前 channel 还没有摘要 */ }
|
|
5261
|
+
// 再兜底读同 agent 其他 channel 的最近摘要 (跨 channel 记忆)
|
|
5262
|
+
if (summariesToRead.length === 0) {
|
|
5263
|
+
try {
|
|
5264
|
+
const sessionsDir = join(getMemoryDir(agentId), 'sessions');
|
|
5265
|
+
const files = (await readdir(sessionsDir)).filter(f => f.endsWith('.summary.md'));
|
|
5266
|
+
// 按文件名排序取最近 2 个 (文件名含时间戳, 近似排序)
|
|
5267
|
+
const recent = files.sort().slice(-2);
|
|
5268
|
+
for (const f of recent) {
|
|
5269
|
+
if (f.includes(sanitizeAgentId(channelId)))
|
|
5270
|
+
continue; // 跳过已读的当前 channel
|
|
5271
|
+
try {
|
|
5272
|
+
const raw = await readFile(join(sessionsDir, f), 'utf-8');
|
|
5273
|
+
if (raw.trim())
|
|
5274
|
+
summariesToRead.push(raw);
|
|
5275
|
+
}
|
|
5276
|
+
catch { /* 单个摘要读失败跳过 */ }
|
|
5218
5277
|
}
|
|
5219
|
-
catch { /* 单个摘要读失败跳过 */ }
|
|
5220
5278
|
}
|
|
5279
|
+
catch { /* 无 sessions 目录 */ }
|
|
5221
5280
|
}
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
contextHint += `[系统上下文] 动态状态层 · chat-worksite (DID 目录同步记忆, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
|
|
5281
|
+
if (summariesToRead.length > 0) {
|
|
5282
|
+
const memBlock = summariesToRead.map(s => s.trim().slice(-1500)).join('\n\n---\n\n');
|
|
5283
|
+
contextHint += `[系统上下文] 动态状态层 · chat-worksite (上次做到哪 — 本 channel 历史记忆, 来自 memory-compressor 摘要, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
|
|
5284
|
+
}
|
|
5285
|
+
else {
|
|
5286
|
+
// 2026-08-08 (DID 目录读侧): 磁盘无摘要时, 回退读 DID 目录 memory 表 (跨设备同步来的记忆)
|
|
5287
|
+
try {
|
|
5288
|
+
const { openUserCatalog, catalogMemoryRows } = await import('../storage/did-catalog-bridge.js');
|
|
5289
|
+
const cat = await openUserCatalog();
|
|
5290
|
+
if (cat) {
|
|
5291
|
+
const rows = catalogMemoryRows(cat, agentId);
|
|
5292
|
+
if (rows.length > 0) {
|
|
5293
|
+
const memBlock = rows.slice(0, 2)
|
|
5294
|
+
.map(({ row }) => String(row.data.summary || '').slice(-1500))
|
|
5295
|
+
.filter(Boolean)
|
|
5296
|
+
.join('\n\n---\n\n');
|
|
5297
|
+
if (memBlock) {
|
|
5298
|
+
contextHint += `[系统上下文] 动态状态层 · chat-worksite (DID 目录同步记忆, 引用而非复述):\n${memBlock.slice(-2500)}\n\n`;
|
|
5299
|
+
}
|
|
5242
5300
|
}
|
|
5243
5301
|
}
|
|
5244
5302
|
}
|
|
5303
|
+
catch { /* 目录读失败静默 */ }
|
|
5245
5304
|
}
|
|
5246
|
-
catch { /* 目录读失败静默 */ }
|
|
5247
5305
|
}
|
|
5248
5306
|
}
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
// 静默失败 — memory 回读不是核心, 失败不阻塞
|
|
5252
|
-
}
|
|
5253
|
-
// 2026-08-02: plan 回读 — 把进行中的计划注入 contextHint (在 memory 之后)
|
|
5254
|
-
// 配合 create_plan / update_plan / review_plan 工具, 让 LLM 每次对话都能看到
|
|
5255
|
-
// 未完成的计划并继续推进.
|
|
5256
|
-
try {
|
|
5257
|
-
const { listActivePlans, planToContext } = await import('../agents/plan-store.js');
|
|
5258
|
-
const plans = await listActivePlans();
|
|
5259
|
-
if (plans.length > 0) {
|
|
5260
|
-
const plansBlock = plans.slice(0, 3).map(p => planToContext(p)).join('\n\n');
|
|
5261
|
-
contextHint += `[系统上下文] 动态状态层 · focus (此刻优先级与时间边界 — 进行中的计划, 来自 plan-store, 执行中每完成一步调 update_plan 勾选):\n${plansBlock}\n\n`;
|
|
5262
|
-
}
|
|
5263
|
-
}
|
|
5264
|
-
catch (planErr) {
|
|
5265
|
-
// 静默失败
|
|
5266
|
-
}
|
|
5267
|
-
// 2026-08-03 (Context OS P5): 资产层目录注入 — LLM 知道有 12+3 层资产,
|
|
5268
|
-
// 需要细节时用 read_context_assets 按层路由读取, 不全仓扫描 (Context OS §4).
|
|
5269
|
-
try {
|
|
5270
|
-
const { readContextAssets, formatLayerListing } = await import('../bootstrap/context-os.js');
|
|
5271
|
-
const listings = await readContextAssets();
|
|
5272
|
-
const total = listings.reduce((s, l) => s + l.fileCount, 0);
|
|
5273
|
-
if (total > 0) {
|
|
5274
|
-
contextHint += `[系统上下文] 资产层 (Context OS 12+3 层, ${total} 篇资产 — 任务需要细节时用 read_context_assets 按层读取, 不要假装知道没读过的内容):\n${formatLayerListing(listings)}`;
|
|
5307
|
+
catch (memErr) {
|
|
5308
|
+
// 静默失败 — memory 回读不是核心, 失败不阻塞
|
|
5275
5309
|
}
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
//
|
|
5279
|
-
}
|
|
5280
|
-
const linkedIds = channelForJudgment?.linkedDocumentIds;
|
|
5281
|
-
if (Array.isArray(linkedIds) && linkedIds.length > 0) {
|
|
5310
|
+
// 2026-08-02: plan 回读 — 把进行中的计划注入 contextHint (在 memory 之后)
|
|
5311
|
+
// 配合 create_plan / update_plan / review_plan 工具, 让 LLM 每次对话都能看到
|
|
5312
|
+
// 未完成的计划并继续推进.
|
|
5282
5313
|
try {
|
|
5283
|
-
const {
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
if (!doc)
|
|
5289
|
-
continue;
|
|
5290
|
-
const name = doc.metadata?.fileName || docId;
|
|
5291
|
-
const content = (doc.content || '').slice(0, 1500); // 单篇 1.5KB 上限, 总 prompt 防爆
|
|
5292
|
-
contextHint += `\n--- 文档: ${name} ---\n${content}\n--- 文档结束 ---\n`;
|
|
5293
|
-
loaded++;
|
|
5314
|
+
const { listActivePlans, planToContext } = await import('../agents/plan-store.js');
|
|
5315
|
+
const plans = await listActivePlans();
|
|
5316
|
+
if (plans.length > 0) {
|
|
5317
|
+
const plansBlock = plans.slice(0, 3).map(p => planToContext(p)).join('\n\n');
|
|
5318
|
+
contextHint += `[系统上下文] 动态状态层 · focus (此刻优先级与时间边界 — 进行中的计划, 来自 plan-store, 执行中每完成一步调 update_plan 勾选):\n${plansBlock}\n\n`;
|
|
5294
5319
|
}
|
|
5295
|
-
if (loaded === 0) {
|
|
5296
|
-
contextHint += `(但加载失败, 文档可能已被删除)\n\n`;
|
|
5297
|
-
}
|
|
5298
|
-
else {
|
|
5299
|
-
contextHint += '\n';
|
|
5300
|
-
}
|
|
5301
|
-
}
|
|
5302
|
-
catch (err) {
|
|
5303
|
-
console.warn('[v3-persona] 加载关联文档失败 (非致命):', err.message);
|
|
5304
|
-
}
|
|
5305
|
-
}
|
|
5306
|
-
// v3 新增: 注入"可用渠道"目录, 让 LLM 知道可以 @-mention 哪些 channel
|
|
5307
|
-
// - 本地 channels (除了自己)
|
|
5308
|
-
// - 远端 channels (remoteChannelCache 缓存的)
|
|
5309
|
-
const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
|
|
5310
|
-
const remoteChannels = [];
|
|
5311
|
-
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
5312
|
-
for (const ch of list) {
|
|
5313
|
-
remoteChannels.push({ ...ch, _ownerPublicKey: peerPk });
|
|
5314
|
-
}
|
|
5315
|
-
}
|
|
5316
|
-
if (localChannels.length > 0 || remoteChannels.length > 0) {
|
|
5317
|
-
contextHint += '[系统上下文] 可用渠道 (你可以用 @渠道名 消息内容 给它们发消息):\n';
|
|
5318
|
-
for (const c of localChannels) {
|
|
5319
|
-
contextHint += ` - [本地] @${c.name} (id=${c.id})\n`;
|
|
5320
5320
|
}
|
|
5321
|
-
|
|
5322
|
-
|
|
5321
|
+
catch (planErr) {
|
|
5322
|
+
// 静默失败
|
|
5323
5323
|
}
|
|
5324
|
-
|
|
5325
|
-
//
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5324
|
+
// 2026-08-03 (Context OS P5): 资产层目录注入 — LLM 知道有 12+3 层资产,
|
|
5325
|
+
// 需要细节时用 read_context_assets 按层路由读取, 不全仓扫描 (Context OS §4).
|
|
5326
|
+
try {
|
|
5327
|
+
const { readContextAssets, formatLayerListing } = await import('../bootstrap/context-os.js');
|
|
5328
|
+
const listings = await readContextAssets();
|
|
5329
|
+
const total = listings.reduce((s, l) => s + l.fileCount, 0);
|
|
5330
|
+
if (total > 0) {
|
|
5331
|
+
contextHint += `[系统上下文] 资产层 (Context OS 12+3 层, ${total} 篇资产 — 任务需要细节时用 read_context_assets 按层读取, 不要假装知道没读过的内容):\n${formatLayerListing(listings)}`;
|
|
5332
|
+
}
|
|
5329
5333
|
}
|
|
5330
|
-
|
|
5331
|
-
|
|
5334
|
+
catch (ctxErr) {
|
|
5335
|
+
// 静默失败
|
|
5332
5336
|
}
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5345
|
-
|
|
5346
|
-
|
|
5347
|
-
|
|
5348
|
-
|
|
5349
|
-
|
|
5350
|
-
|
|
5351
|
-
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
let extraHint = '';
|
|
5355
|
-
const hint = nextPromptHints.get(channelId);
|
|
5356
|
-
if (hint) {
|
|
5357
|
-
extraHint = hint + '\n\n';
|
|
5358
|
-
nextPromptHints.delete(channelId);
|
|
5359
|
-
}
|
|
5360
|
-
// 2026-07-15 修 Bug 3: 拖拽附件 — attachmentContext 提到 contextHint 最前, LLM 第一眼看到文件清单
|
|
5361
|
-
const markedPrompt = `${extraHint}【本轮用户请求】\n${text}\n【请求结束】\n\n${attachmentContext}${contextHint}`;
|
|
5362
|
-
fullResponse = await agent.promptStream(markedPrompt, streamCallback, runState.abortController?.signal, channelId);
|
|
5363
|
-
}
|
|
5364
|
-
catch (err) {
|
|
5365
|
-
// abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
|
|
5366
|
-
if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
|
|
5367
|
-
console.log(`[chat] aborted channel=${channelId}`);
|
|
5368
|
-
// 2026-07-06: abort 时 fullResponse 可能为空 (LLM 还没输出), 必须给用户一个反馈
|
|
5369
|
-
if (!fullResponse.trim()) {
|
|
5370
|
-
fullResponse = '⚠️ 生成已中断 (用户取消)';
|
|
5337
|
+
const linkedIds = channelForJudgment?.linkedDocumentIds;
|
|
5338
|
+
if (Array.isArray(linkedIds) && linkedIds.length > 0) {
|
|
5339
|
+
try {
|
|
5340
|
+
const { documentStore } = await import('../documents/store.js');
|
|
5341
|
+
contextHint += `[系统上下文] 此 channel 关联了 ${linkedIds.length} 篇文档 (已自动加载内容, 你应基于它们回答):\n`;
|
|
5342
|
+
let loaded = 0;
|
|
5343
|
+
for (const docId of linkedIds.slice(0, 10)) {
|
|
5344
|
+
const doc = await documentStore.readDocument(docId).catch(() => null);
|
|
5345
|
+
if (!doc)
|
|
5346
|
+
continue;
|
|
5347
|
+
const name = doc.metadata?.fileName || docId;
|
|
5348
|
+
const content = (doc.content || '').slice(0, 1500); // 单篇 1.5KB 上限, 总 prompt 防爆
|
|
5349
|
+
contextHint += `\n--- 文档: ${name} ---\n${content}\n--- 文档结束 ---\n`;
|
|
5350
|
+
loaded++;
|
|
5351
|
+
}
|
|
5352
|
+
if (loaded === 0) {
|
|
5353
|
+
contextHint += `(但加载失败, 文档可能已被删除)\n\n`;
|
|
5354
|
+
}
|
|
5355
|
+
else {
|
|
5356
|
+
contextHint += '\n';
|
|
5357
|
+
}
|
|
5371
5358
|
}
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
// M1.2 (2026-06-17): 不再 rethrow — 把错误塞到 fullResponse 让 broadcast 出来, 后续 session 仍保存
|
|
5375
|
-
// 旧行为: 抛到外层 catch, 触发 500 + 用户消息丢失 (session 不保存)
|
|
5376
|
-
console.error(`[chat] LLM 调用失败 channel=${channelId}:`, err);
|
|
5377
|
-
fullResponse = `[错误: LLM 调用失败] ${String(err?.message || err).slice(0, 300)}`;
|
|
5378
|
-
broadcast({ type: 'error', content: fullResponse }, channelId);
|
|
5379
|
-
}
|
|
5380
|
-
}
|
|
5381
|
-
// abort 模式: 给 partial 拼后缀 — 只在 LLM 真有输出时加中断标记,
|
|
5382
|
-
// 跳过空响应 + 跳过 [AI 服务调用失败] fallback (这些本身就是错误占位, 拼
|
|
5383
|
-
// "[生成已中断]" 会误导用户)
|
|
5384
|
-
const hasRealLlmOutput = fullResponse.trim().length > 0 &&
|
|
5385
|
-
!fullResponse.trimStart().startsWith('[AI 服务调用失败]');
|
|
5386
|
-
if (runState.abortController?.signal.aborted && hasRealLlmOutput) {
|
|
5387
|
-
fullResponse = fullResponse + '\n\n_[生成已中断]_';
|
|
5388
|
-
}
|
|
5389
|
-
// 2026-06-18: 修 lastFinalReply 没设 → /api/loop/inspect 永远返回空
|
|
5390
|
-
runState.lastFinalReply = fullResponse;
|
|
5391
|
-
// 2026-07-06: 防御性兜底 — fullResponse 为空时, 前端 segmentChatReply('') 返回 [],
|
|
5392
|
-
// 导致气泡不渲染, 用户只看到"思考中"占位符. 这里保证永远有内容可渲染.
|
|
5393
|
-
if (!fullResponse.trim()) {
|
|
5394
|
-
fullResponse = '⚠️ AI 未返回内容, 请重试';
|
|
5395
|
-
console.warn(`[chat] fullResponse empty for channel=${channelId}, using fallback`);
|
|
5396
|
-
}
|
|
5397
|
-
// v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
|
|
5398
|
-
await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
|
|
5399
|
-
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
5400
|
-
const existingSession = await loadSession(channelId, currentSessionId);
|
|
5401
|
-
const session = existingSession || { channelId, sessionId: currentSessionId, messages: [], lastUpdated: new Date().toISOString() };
|
|
5402
|
-
session.sessionId = currentSessionId;
|
|
5403
|
-
// v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
|
|
5404
|
-
// 2026-07-15 修 Bug 4: client.ts sendMessage 已经通过 persistLastMessageToServer PATCH /sessions/.../...
|
|
5405
|
-
// 把 user msg 落盘一次 (立即落, 切走再切回不丢). 这里再 push 一次 → session.messages 出现两条相同的 user msg,
|
|
5406
|
-
// loadSession 重渲染时两条都上屏, 表现"每条 user 气泡重复两次".
|
|
5407
|
-
// 修法: 持久化以 client PATCH 为准, /message 这边只 push ai 消息. 同时去重检查上次的 user 避免极端竞态 (并行 PATCH).
|
|
5408
|
-
const lastMsg = session.messages[session.messages.length - 1];
|
|
5409
|
-
const userAlreadyPushed = lastMsg && lastMsg.type === 'user' && lastMsg.content === text;
|
|
5410
|
-
if (!userAlreadyPushed) {
|
|
5411
|
-
session.messages.push({ id: crypto.randomUUID(), type: 'user', content: text, timestamp: new Date().toISOString(), source: 'local' });
|
|
5412
|
-
}
|
|
5413
|
-
session.messages.push({
|
|
5414
|
-
id: crypto.randomUUID(),
|
|
5415
|
-
type: 'ai',
|
|
5416
|
-
content: fullResponse,
|
|
5417
|
-
timestamp: new Date().toISOString(),
|
|
5418
|
-
source: 'local',
|
|
5419
|
-
// P0.5: 这条 AI 回复引用了哪些 judgment (注入门回传)
|
|
5420
|
-
...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
|
|
5421
|
-
});
|
|
5422
|
-
session.lastUpdated = new Date().toISOString();
|
|
5423
|
-
await saveSession(session);
|
|
5424
|
-
// 2026-07-04: 写 session 后, 调 memory-compressor 把消息历史压缩到
|
|
5425
|
-
// ~/.bolloon/memory/<channel.agentId>/sessions/<safe-channel>__<safe-session>.summary.md
|
|
5426
|
-
// 失败静默, 不阻塞 /message 主路径.
|
|
5427
|
-
try {
|
|
5428
|
-
const channelsForMemory = await loadChannels();
|
|
5429
|
-
const channelForMemory = channelsForMemory.find(c => c.id === channelId);
|
|
5430
|
-
if (channelForMemory?.agentId) {
|
|
5431
|
-
const { compressSessionToMemory } = await import('../bootstrap/memory-compressor.js');
|
|
5432
|
-
const compressRes = await compressSessionToMemory({
|
|
5433
|
-
agentId: channelForMemory.agentId,
|
|
5434
|
-
channelId,
|
|
5435
|
-
sessionId: currentSessionId,
|
|
5436
|
-
});
|
|
5437
|
-
if (!compressRes.skipped && compressRes.bytesWritten > 0) {
|
|
5438
|
-
console.log(`[memory] compressed ${compressRes.messagesCount} new messages for ${channelForMemory.agentId}/${channelId} → ${compressRes.summaryPath} (${compressRes.bytesWritten}B)`);
|
|
5359
|
+
catch (err) {
|
|
5360
|
+
console.warn('[v3-persona] 加载关联文档失败 (非致命):', err.message);
|
|
5439
5361
|
}
|
|
5440
5362
|
}
|
|
5441
|
-
|
|
5442
|
-
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
// 触发条件: 消息里出现过 @远端 channel, 或者 session 历史上与该 peer 通信过
|
|
5447
|
-
// 失败静默, 不阻塞主对话
|
|
5448
|
-
try {
|
|
5449
|
-
const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
|
|
5450
|
-
// 找到这条消息涉及到的远端 peer (从 routeMentionsInReply 的结果推断 — 简单起见直接遍历 remoteChannels)
|
|
5451
|
-
const localChannels2 = (await loadChannels());
|
|
5452
|
-
const remoteChannels2 = [];
|
|
5363
|
+
// v3 新增: 注入"可用渠道"目录, 让 LLM 知道可以 @-mention 哪些 channel
|
|
5364
|
+
// - 本地 channels (除了自己)
|
|
5365
|
+
// - 远端 channels (remoteChannelCache 缓存的)
|
|
5366
|
+
const localChannels = (await loadChannels()).filter(c => c.id !== channelId);
|
|
5367
|
+
const remoteChannels = [];
|
|
5453
5368
|
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
5454
5369
|
for (const ch of list) {
|
|
5455
|
-
|
|
5370
|
+
remoteChannels.push({ ...ch, _ownerPublicKey: peerPk });
|
|
5456
5371
|
}
|
|
5457
5372
|
}
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5468
|
-
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
entry: {
|
|
5475
|
-
ts: new Date().toISOString(),
|
|
5476
|
-
source: 'local',
|
|
5477
|
-
channelId, channelName: (await loadChannels()).find(c => c.id === channelId)?.name,
|
|
5478
|
-
text: `[本节点] user: ${text.slice(0, 500)}\n[本节点] ai: ${fullResponse.slice(0, 800)}`,
|
|
5479
|
-
fromPublicKey: v3P2PRef?.getPublicKey(),
|
|
5480
|
-
msgType: 'user',
|
|
5481
|
-
}
|
|
5482
|
-
});
|
|
5483
|
-
}
|
|
5484
|
-
}
|
|
5485
|
-
catch (archiveErr) {
|
|
5486
|
-
console.warn('[chat-archive] append failed (non-fatal):', archiveErr?.message?.slice(0, 200));
|
|
5487
|
-
}
|
|
5488
|
-
// 2026-07-05: 懒加载触发器 — 探测到 @-mention 远端 / 连续失败 / 关键词, 拉对方 manifest
|
|
5489
|
-
// 把 capability-index 拼到下一次 prompt 上下文 (只生效 1 次, 不污染主循环)
|
|
5490
|
-
try {
|
|
5491
|
-
const { detectLoadTrigger, loadPeerManifest, clearFailure } = await import('../agents/peer-manifest-loader.js');
|
|
5492
|
-
const remoteChannelsForDetect = [];
|
|
5493
|
-
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
5494
|
-
for (const ch of list) {
|
|
5495
|
-
remoteChannelsForDetect.push({ ...ch, _ownerPublicKey: peerPk });
|
|
5373
|
+
if (localChannels.length > 0 || remoteChannels.length > 0) {
|
|
5374
|
+
contextHint += '[系统上下文] 可用渠道 (你可以用 @渠道名 消息内容 给它们发消息):\n';
|
|
5375
|
+
for (const c of localChannels) {
|
|
5376
|
+
contextHint += ` - [本地] @${c.name} (id=${c.id})\n`;
|
|
5377
|
+
}
|
|
5378
|
+
for (const c of remoteChannels) {
|
|
5379
|
+
contextHint += ` - [远端, owner=${(c._ownerPublicKey || '').substring(0, 8)}…] @${c.name} (id=${c.id})\n`;
|
|
5380
|
+
}
|
|
5381
|
+
contextHint += '语法: 当你想给其他渠道发消息, 在回复中写 "@渠道名 我要说的话" 即可. 消息会持久化到目标 channel 的 session, 你之后能看到"自己"在那里说的话.\n';
|
|
5382
|
+
// 2026-06-10 强化: 当用户消息里出现 @渠道名, 默认是请你代为转发, 务必在回复里包含对应的 @ 转发
|
|
5383
|
+
if (remoteChannels.length > 0) {
|
|
5384
|
+
contextHint += '重要: 上面列表里 [远端] 标记的 channel 在另一台机器上, 你可以像 @本地 channel 一样 @ 它们 — 我会通过 P2P 自动把消息送达对方智能体, 对方智能体的回复也会同步回来.\n';
|
|
5385
|
+
contextHint += '当用户在消息里 @ 了某个 (本地或远端) channel, 默认意图是希望你代为转发 — 你应该在回复中写出对应的 "@渠道名 转发内容", 否则用户的请求不会被路由出去.\n\n';
|
|
5386
|
+
}
|
|
5387
|
+
else {
|
|
5388
|
+
contextHint += '\n';
|
|
5496
5389
|
}
|
|
5497
5390
|
}
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5504
|
-
if (
|
|
5505
|
-
const
|
|
5391
|
+
if (contextHint)
|
|
5392
|
+
contextHint += '\n';
|
|
5393
|
+
// 2026-07-06: 全局 contextHint 硬裁 — MiniMax-M3 context window 8K token ≈ 32K 字符
|
|
5394
|
+
// 如果 contextHint 超过 14K 字符 (留 ~18K 给 userText + LLM 输出 + system), 主动截断
|
|
5395
|
+
const CONTEXT_LIMIT = 14000;
|
|
5396
|
+
let contextTruncated = false;
|
|
5397
|
+
if (contextHint.length > CONTEXT_LIMIT) {
|
|
5398
|
+
const originalLen = contextHint.length;
|
|
5399
|
+
contextHint = contextHint.slice(0, CONTEXT_LIMIT);
|
|
5400
|
+
contextHint += `\n[系统上下文] (...剩余 ${originalLen - CONTEXT_LIMIT} 字符因 context window 限制已截断, 完整内容请用 /judgments 查)\n`;
|
|
5401
|
+
contextTruncated = true;
|
|
5402
|
+
console.warn(`[chat] channel=${channelId} contextHint truncated: ${originalLen} -> ${contextHint.length} chars`);
|
|
5403
|
+
}
|
|
5404
|
+
try {
|
|
5405
|
+
// 2026-06-15: 把 user text 单独 marker 包起来, LLM 不会被 8K+ 的 system context 吞掉
|
|
5406
|
+
// (之前 contextHint + text 拼成一整段当 user role, 24 字符的 user input 埋在 8K+ 里看不出)
|
|
5407
|
+
// 修法: contextHint 当 "背景信息", text 当 "本轮用户请求" — 显式 marker 让 LLM 区分
|
|
5408
|
+
// 2026-06-15 二次修: 把 text 放在最前 (LLM 看到 input 第一眼是 user text, 不会被 judgmentHint 末尾
|
|
5409
|
+
// 的 "..." 误判为整个 input 截断)
|
|
5410
|
+
// 2026-07-05: prepend nextPromptHints (manifest-loader 加载的对方能力, 仅本次生效)
|
|
5411
|
+
let extraHint = '';
|
|
5412
|
+
const hint = nextPromptHints.get(channelId);
|
|
5413
|
+
if (hint) {
|
|
5414
|
+
extraHint = hint + '\n\n';
|
|
5415
|
+
nextPromptHints.delete(channelId);
|
|
5416
|
+
}
|
|
5417
|
+
// 2026-07-15 修 Bug 3: 拖拽附件 — attachmentContext 提到 contextHint 最前, LLM 第一眼看到文件清单
|
|
5418
|
+
const markedPrompt = `${extraHint}【本轮用户请求】\n${text}\n【请求结束】\n\n${attachmentContext}${contextHint}`;
|
|
5419
|
+
// 闭包里 TS 不再保留 null 收窄 ⇒ 先收成局部 const
|
|
5420
|
+
const agentForRun = agent;
|
|
5421
|
+
// **K5 步骤⑦**: 入口只交一个一次性 `ExecutionRequest` (绑定/流/取消都在请求里), Pi 侧 runExecution 统一派发
|
|
5422
|
+
const request = {
|
|
5423
|
+
input: markedPrompt,
|
|
5506
5424
|
channelId,
|
|
5507
|
-
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
//
|
|
5523
|
-
|
|
5524
|
-
|
|
5425
|
+
signal: abortSignal,
|
|
5426
|
+
onStream: streamCallback,
|
|
5427
|
+
};
|
|
5428
|
+
fullResponse = await deliverThroughActor(agentForRun, () => agentForRun.runExecution(request));
|
|
5429
|
+
}
|
|
5430
|
+
catch (err) {
|
|
5431
|
+
// abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
|
|
5432
|
+
if (abortSignal?.aborted || err?.name === 'AbortError') {
|
|
5433
|
+
console.log(`[chat] aborted channel=${channelId}`);
|
|
5434
|
+
// 2026-07-06: abort 时 fullResponse 可能为空 (LLM 还没输出), 必须给用户一个反馈
|
|
5435
|
+
if (!fullResponse.trim()) {
|
|
5436
|
+
fullResponse = '⚠️ 生成已中断 (用户取消)';
|
|
5437
|
+
}
|
|
5438
|
+
}
|
|
5439
|
+
else {
|
|
5440
|
+
// M1.2 (2026-06-17): 不再 rethrow — 把错误塞到 fullResponse 让 broadcast 出来, 后续 session 仍保存
|
|
5441
|
+
// 旧行为: 抛到外层 catch, 触发 500 + 用户消息丢失 (session 不保存)
|
|
5442
|
+
console.error(`[chat] LLM 调用失败 channel=${channelId}:`, err);
|
|
5443
|
+
fullResponse = `[错误: LLM 调用失败] ${String(err?.message || err).slice(0, 300)}`;
|
|
5444
|
+
broadcast({ type: 'error', content: fullResponse }, channelId);
|
|
5525
5445
|
}
|
|
5526
5446
|
}
|
|
5527
|
-
|
|
5528
|
-
|
|
5529
|
-
|
|
5447
|
+
// abort 模式: 给 partial 拼后缀 — 只在 LLM 真有输出时加中断标记,
|
|
5448
|
+
// 跳过空响应 + 跳过 [AI 服务调用失败] fallback (这些本身就是错误占位, 拼
|
|
5449
|
+
// "[生成已中断]" 会误导用户)
|
|
5450
|
+
const hasRealLlmOutput = fullResponse.trim().length > 0 &&
|
|
5451
|
+
!fullResponse.trimStart().startsWith('[AI 服务调用失败]');
|
|
5452
|
+
if (abortSignal?.aborted && hasRealLlmOutput) {
|
|
5453
|
+
fullResponse = fullResponse + '\n\n_[生成已中断]_';
|
|
5454
|
+
}
|
|
5455
|
+
// 2026-06-18: 修 lastFinalReply 没设 → /api/loop/inspect 永远返回空
|
|
5456
|
+
runState.lastFinalReply = fullResponse;
|
|
5457
|
+
// 2026-07-06: 防御性兜底 — fullResponse 为空时, 前端 segmentChatReply('') 返回 [],
|
|
5458
|
+
// 导致气泡不渲染, 用户只看到"思考中"占位符. 这里保证永远有内容可渲染.
|
|
5459
|
+
if (!fullResponse.trim()) {
|
|
5460
|
+
fullResponse = '⚠️ AI 未返回内容, 请重试';
|
|
5461
|
+
console.warn(`[chat] fullResponse empty for channel=${channelId}, using fallback`);
|
|
5462
|
+
}
|
|
5463
|
+
// v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
|
|
5464
|
+
await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
|
|
5465
|
+
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
5466
|
+
const existingSession = await loadSession(channelId, currentSessionId);
|
|
5467
|
+
const session = existingSession || { channelId, sessionId: currentSessionId, messages: [], lastUpdated: new Date().toISOString() };
|
|
5468
|
+
session.sessionId = currentSessionId;
|
|
5469
|
+
// v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
|
|
5470
|
+
// 2026-07-15 修 Bug 4: client.ts sendMessage 已经通过 persistLastMessageToServer PATCH /sessions/.../...
|
|
5471
|
+
// 把 user msg 落盘一次 (立即落, 切走再切回不丢). 这里再 push 一次 → session.messages 出现两条相同的 user msg,
|
|
5472
|
+
// loadSession 重渲染时两条都上屏, 表现"每条 user 气泡重复两次".
|
|
5473
|
+
// 修法: 持久化以 client PATCH 为准, /message 这边只 push ai 消息. 同时去重检查上次的 user 避免极端竞态 (并行 PATCH).
|
|
5474
|
+
const lastMsg = session.messages[session.messages.length - 1];
|
|
5475
|
+
const userAlreadyPushed = lastMsg && lastMsg.type === 'user' && lastMsg.content === text;
|
|
5476
|
+
if (!userAlreadyPushed) {
|
|
5477
|
+
session.messages.push({ id: crypto.randomUUID(), type: 'user', content: text, timestamp: new Date().toISOString(), source: 'local' });
|
|
5530
5478
|
}
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
// 现在改名逻辑挪到 /api/agent-rename 端点, 用户主动触发才跑
|
|
5540
|
-
if (channel) {
|
|
5541
|
-
channel.updatedAt = new Date().toISOString();
|
|
5542
|
-
// 2026-08-02 fix: 原子写入, 防并发覆盖丢 channel
|
|
5543
|
-
await updateChannels((chs) => {
|
|
5544
|
-
const c = chs.find(x => x.id === channelId);
|
|
5545
|
-
if (c)
|
|
5546
|
-
c.updatedAt = channel.updatedAt;
|
|
5547
|
-
return chs;
|
|
5479
|
+
session.messages.push({
|
|
5480
|
+
id: crypto.randomUUID(),
|
|
5481
|
+
type: 'ai',
|
|
5482
|
+
content: fullResponse,
|
|
5483
|
+
timestamp: new Date().toISOString(),
|
|
5484
|
+
source: 'local',
|
|
5485
|
+
// P0.5: 这条 AI 回复引用了哪些 judgment (注入门回传)
|
|
5486
|
+
...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
|
|
5548
5487
|
});
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
|
|
5552
|
-
|
|
5488
|
+
session.lastUpdated = new Date().toISOString();
|
|
5489
|
+
await saveSession(session);
|
|
5490
|
+
// 2026-07-04: 写 session 后, 调 memory-compressor 把消息历史压缩到
|
|
5491
|
+
// ~/.bolloon/memory/<channel.agentId>/sessions/<safe-channel>__<safe-session>.summary.md
|
|
5492
|
+
// 失败静默, 不阻塞 /message 主路径.
|
|
5553
5493
|
try {
|
|
5554
|
-
const
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
console.log(`[D-hook ${channelId}] throttled (within 5min)`);
|
|
5566
|
-
broadcast({ type: 'phase', phase: 'd_skip', detail: 'throttled' }, channelId);
|
|
5567
|
-
return null;
|
|
5494
|
+
const channelsForMemory = await loadChannels();
|
|
5495
|
+
const channelForMemory = channelsForMemory.find(c => c.id === channelId);
|
|
5496
|
+
if (channelForMemory?.agentId) {
|
|
5497
|
+
const { compressSessionToMemory } = await import('../bootstrap/memory-compressor.js');
|
|
5498
|
+
const compressRes = await compressSessionToMemory({
|
|
5499
|
+
agentId: channelForMemory.agentId,
|
|
5500
|
+
channelId,
|
|
5501
|
+
sessionId: currentSessionId,
|
|
5502
|
+
});
|
|
5503
|
+
if (!compressRes.skipped && compressRes.bytesWritten > 0) {
|
|
5504
|
+
console.log(`[memory] compressed ${compressRes.messagesCount} new messages for ${channelForMemory.agentId}/${channelId} → ${compressRes.summaryPath} (${compressRes.bytesWritten}B)`);
|
|
5568
5505
|
}
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
5506
|
+
}
|
|
5507
|
+
}
|
|
5508
|
+
catch (memErr) {
|
|
5509
|
+
console.warn('[memory] compressSessionToMemory failed (non-fatal):', memErr?.message?.slice(0, 200));
|
|
5510
|
+
}
|
|
5511
|
+
// 2026-07-05: 把 session 里的 user/ai 消息按涉及到的远端 peer 归档到月度 markdown
|
|
5512
|
+
// 触发条件: 消息里出现过 @远端 channel, 或者 session 历史上与该 peer 通信过
|
|
5513
|
+
// 失败静默, 不阻塞主对话
|
|
5514
|
+
try {
|
|
5515
|
+
const { appendChatArchive } = await import('../bootstrap/chat-archiver.js');
|
|
5516
|
+
// 找到这条消息涉及到的远端 peer (从 routeMentionsInReply 的结果推断 — 简单起见直接遍历 remoteChannels)
|
|
5517
|
+
const localChannels2 = (await loadChannels());
|
|
5518
|
+
const remoteChannels2 = [];
|
|
5519
|
+
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
5520
|
+
for (const ch of list) {
|
|
5521
|
+
remoteChannels2.push({ ...ch, _ownerPublicKey: peerPk });
|
|
5576
5522
|
}
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5523
|
+
}
|
|
5524
|
+
// 找出本次 text/fullResponse 里出现 @ 的 channel 对应的 owner pk
|
|
5525
|
+
const peerSet = new Set();
|
|
5526
|
+
const mentionRe = /@([一-龥A-Za-z0-9_\-]{1,30})/g;
|
|
5527
|
+
const mentionedNames = new Set();
|
|
5528
|
+
for (const m of text.matchAll(mentionRe))
|
|
5529
|
+
mentionedNames.add(m[1]);
|
|
5530
|
+
for (const m of fullResponse.matchAll(mentionRe))
|
|
5531
|
+
mentionedNames.add(m[1]);
|
|
5532
|
+
for (const rc of remoteChannels2) {
|
|
5533
|
+
if (mentionedNames.has(rc.name))
|
|
5534
|
+
peerSet.add(rc._ownerPublicKey);
|
|
5535
|
+
}
|
|
5536
|
+
// 追加到每个相关 peer 的月度归档
|
|
5537
|
+
for (const pk of peerSet) {
|
|
5538
|
+
await appendChatArchive({
|
|
5539
|
+
publicKey: pk,
|
|
5540
|
+
entry: {
|
|
5541
|
+
ts: new Date().toISOString(),
|
|
5542
|
+
source: 'local',
|
|
5543
|
+
channelId, channelName: (await loadChannels()).find(c => c.id === channelId)?.name,
|
|
5544
|
+
text: `[本节点] user: ${text.slice(0, 500)}\n[本节点] ai: ${fullResponse.slice(0, 800)}`,
|
|
5545
|
+
fromPublicKey: v3P2PRef?.getPublicKey(),
|
|
5546
|
+
msgType: 'user',
|
|
5547
|
+
}
|
|
5548
|
+
});
|
|
5549
|
+
}
|
|
5550
|
+
}
|
|
5551
|
+
catch (archiveErr) {
|
|
5552
|
+
console.warn('[chat-archive] append failed (non-fatal):', archiveErr?.message?.slice(0, 200));
|
|
5553
|
+
}
|
|
5554
|
+
// 2026-07-05: 懒加载触发器 — 探测到 @-mention 远端 / 连续失败 / 关键词, 拉对方 manifest
|
|
5555
|
+
// 把 capability-index 拼到下一次 prompt 上下文 (只生效 1 次, 不污染主循环)
|
|
5556
|
+
try {
|
|
5557
|
+
const { detectLoadTrigger, loadPeerManifest, clearFailure } = await import('../agents/peer-manifest-loader.js');
|
|
5558
|
+
const remoteChannelsForDetect = [];
|
|
5559
|
+
for (const [peerPk, list] of remoteChannelCache.entries()) {
|
|
5560
|
+
for (const ch of list) {
|
|
5561
|
+
remoteChannelsForDetect.push({ ...ch, _ownerPublicKey: peerPk });
|
|
5580
5562
|
}
|
|
5581
|
-
}
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5563
|
+
}
|
|
5564
|
+
// 检测 + 加载
|
|
5565
|
+
const detection = detectLoadTrigger({
|
|
5566
|
+
text,
|
|
5567
|
+
channelId,
|
|
5568
|
+
remoteChannels: remoteChannelsForDetect,
|
|
5585
5569
|
});
|
|
5570
|
+
if (detection.shouldLoad && detection.remotePublicKey && v3P2PRef) {
|
|
5571
|
+
const result = await loadPeerManifest({
|
|
5572
|
+
channelId,
|
|
5573
|
+
channelName: (await loadChannels()).find(c => c.id === channelId)?.name,
|
|
5574
|
+
reason: detection.reason,
|
|
5575
|
+
triggerValue: detection.triggerValue,
|
|
5576
|
+
remotePublicKey: detection.remotePublicKey,
|
|
5577
|
+
}, { p2p: v3P2PRef });
|
|
5578
|
+
if (result && result.promptBlock) {
|
|
5579
|
+
console.log(`[manifest-loader] ${detection.reason} → ${detection.remotePublicKey.slice(0, 12)}... (${result.durationMs}ms, rpc=${result.rpcTriggered}, agents=${result.agentDescriptions.length})`);
|
|
5580
|
+
// 把 promptBlock 推给前端 + 写入一次性的 prompt 附加 (nextPromptHint 全局变量, 在下次 agent.promptStream 前 prepend)
|
|
5581
|
+
broadcast({
|
|
5582
|
+
type: 'peer-manifest-loaded',
|
|
5583
|
+
fromPublicKey: detection.remotePublicKey,
|
|
5584
|
+
reason: detection.reason,
|
|
5585
|
+
promptBlock: result.promptBlock,
|
|
5586
|
+
capabilityIndex: result.capabilityIndex,
|
|
5587
|
+
}, channelId);
|
|
5588
|
+
// 缓存到 channel 维度的 nextPromptHint (下一次 LLM prompt 前 inject)
|
|
5589
|
+
nextPromptHints.set(channelId, (nextPromptHints.get(channelId) || '') + '\n\n' + result.promptBlock);
|
|
5590
|
+
clearFailure(channelId);
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
else {
|
|
5594
|
+
// 没触发, 也清掉连续失败计数 (用户消息说明对话还在正常进行)
|
|
5595
|
+
clearFailure(channelId);
|
|
5596
|
+
}
|
|
5586
5597
|
}
|
|
5587
|
-
catch (
|
|
5588
|
-
console.warn(
|
|
5598
|
+
catch (loaderErr) {
|
|
5599
|
+
console.warn('[manifest-loader] failed (non-fatal):', loaderErr?.message?.slice(0, 200));
|
|
5600
|
+
}
|
|
5601
|
+
const channels = await loadChannels();
|
|
5602
|
+
const channel = channels.find(c => c.id === channelId);
|
|
5603
|
+
// 2026-06-11: 移除 suggestRename 的二次 LLM 调用 — 之前每次用户发消息, 智能体 channel 都会再调一次 LLM (5-8s) 自动改名
|
|
5604
|
+
// 影响: (1) /message 端点被拖慢 5-8s (2) LLM 客户端排队, 其他 channel 跟着卡
|
|
5605
|
+
// 现在改名逻辑挪到 /api/agent-rename 端点, 用户主动触发才跑
|
|
5606
|
+
if (channel) {
|
|
5607
|
+
channel.updatedAt = new Date().toISOString();
|
|
5608
|
+
// 2026-08-02 fix: 原子写入, 防并发覆盖丢 channel
|
|
5609
|
+
await updateChannels((chs) => {
|
|
5610
|
+
const c = chs.find(x => x.id === channelId);
|
|
5611
|
+
if (c)
|
|
5612
|
+
c.updatedAt = channel.updatedAt;
|
|
5613
|
+
return chs;
|
|
5614
|
+
});
|
|
5589
5615
|
}
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
if (!res.headersSent)
|
|
5593
|
-
res.json({ ok: true });
|
|
5594
|
-
}
|
|
5595
|
-
catch (err) {
|
|
5596
|
-
broadcast({ type: 'error', content: err.message }, channelId);
|
|
5597
|
-
broadcast({ type: 'done' }, channelId);
|
|
5598
|
-
if (!res.headersSent)
|
|
5599
|
-
res.status(500).json({ error: err.message });
|
|
5600
|
-
}
|
|
5601
|
-
finally {
|
|
5602
|
-
// 2026-07-04: 清掉 pivot 强制 timeout timer, 避免 hang 情况下 timer 在
|
|
5603
|
-
// process 里一直挂着. forceTimeout 在 try 外创建, finally 在闭包里
|
|
5604
|
-
// 能直接拿到引用.
|
|
5605
|
-
clearTimeout(forceTimeout);
|
|
5606
|
-
// queue dequeue: 跑完或失败都要清状态
|
|
5607
|
-
runState.running = false;
|
|
5608
|
-
runState.abortController = null;
|
|
5609
|
-
broadcastQueueUpdate(channelId);
|
|
5610
|
-
// 2026-07-15 修 Bug 8: 队列自动 drain — 之前只清状态不抽下一条, 用户连续发的所有
|
|
5611
|
-
// 第二轮起全卡在 queue 里出不来, 表现"第二轮没反应".
|
|
5612
|
-
// 修复: finally 里查 queue, 非空就异步 fire-and-forget 起下一轮.
|
|
5613
|
-
// 实现要点:
|
|
5614
|
-
// 1. 用 setImmediate / Promise.resolve() 让 res.headersSent 干净
|
|
5615
|
-
// 2. 不能 await (否则阻塞 finally 后面的 saveSession; 而且这就是 fire-and-forget)
|
|
5616
|
-
// 3. 重新 build contextHint, 走相同 LLM 路径
|
|
5617
|
-
if (runState.queue.length > 0) {
|
|
5618
|
-
const next = runState.queue.shift();
|
|
5619
|
-
console.log(`[queue-drain] channel=${next.channelId} text="${next.text.slice(0, 30)}" attach=${(next.attachments?.length ?? 0)}`);
|
|
5620
|
-
// 异步跑下一条 (fire-and-forget)
|
|
5616
|
+
broadcast({ type: 'done' }, channelId);
|
|
5617
|
+
// D 触发: AI 被动捕获判断力 (后台异步, 不阻塞主对话)
|
|
5621
5618
|
setImmediate(() => {
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5619
|
+
try {
|
|
5620
|
+
const lastTurns = session.messages.slice(-6).map((m) => ({
|
|
5621
|
+
role: (m.type === 'user' ? 'human' : 'agent'),
|
|
5622
|
+
content: m.content,
|
|
5623
|
+
}));
|
|
5624
|
+
if (lastTurns.length < 2)
|
|
5625
|
+
return;
|
|
5626
|
+
broadcast({ type: 'phase', phase: 'd_detect', detail: '监测对话...' }, channelId);
|
|
5627
|
+
import('../pi-ecosystem-judgment/human-value-pipeline.js')
|
|
5628
|
+
.then(async ({ detectAndDistillFromChannel, throttleDHook }) => {
|
|
5629
|
+
// channel 维度 5min 节流, 防对话卡顿时 LLM 反复触发
|
|
5630
|
+
if (!throttleDHook(channelId, 5 * 60_000)) {
|
|
5631
|
+
console.log(`[D-hook ${channelId}] throttled (within 5min)`);
|
|
5632
|
+
broadcast({ type: 'phase', phase: 'd_skip', detail: 'throttled' }, channelId);
|
|
5633
|
+
return null;
|
|
5634
|
+
}
|
|
5635
|
+
broadcast({ type: 'phase', phase: 'd_distill', detail: '蒸馏判断力...' }, channelId);
|
|
5636
|
+
return detectAndDistillFromChannel(lastTurns, { channelId });
|
|
5637
|
+
})
|
|
5638
|
+
.then((result) => {
|
|
5639
|
+
if (result && result.triggered) {
|
|
5640
|
+
console.log(`[D-hook ${channelId}] stored: ${result.reason}`, result.evolved);
|
|
5641
|
+
broadcast({ type: 'phase', phase: 'd_done', detail: result.reason }, channelId);
|
|
5642
|
+
}
|
|
5643
|
+
else if (result && result.reason) {
|
|
5644
|
+
console.log(`[D-hook ${channelId}] skipped: ${result.reason}`);
|
|
5645
|
+
broadcast({ type: 'phase', phase: 'd_skip', detail: result.reason }, channelId);
|
|
5646
|
+
}
|
|
5647
|
+
})
|
|
5648
|
+
.catch((err) => {
|
|
5649
|
+
console.warn(`[D-hook ${channelId}] failed:`, err);
|
|
5650
|
+
broadcast({ type: 'phase', phase: 'd_error', detail: String(err) }, channelId);
|
|
5651
|
+
});
|
|
5652
|
+
}
|
|
5653
|
+
catch (err) {
|
|
5654
|
+
console.warn(`[D-hook ${channelId}] sync error:`, err);
|
|
5655
|
+
}
|
|
5625
5656
|
});
|
|
5657
|
+
// 2026-06-11: 202 已发的话, 不要重复 res.json (会抛 ERR_HTTP_HEADERS_SENT)
|
|
5658
|
+
if (!res.headersSent)
|
|
5659
|
+
res.json({ ok: true });
|
|
5626
5660
|
}
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
|
|
5661
|
+
catch (err) {
|
|
5662
|
+
broadcast({ type: 'error', content: err.message }, channelId);
|
|
5663
|
+
broadcast({ type: 'done' }, channelId);
|
|
5664
|
+
if (!res.headersSent)
|
|
5665
|
+
res.status(500).json({ error: err.message });
|
|
5666
|
+
}
|
|
5667
|
+
finally {
|
|
5668
|
+
// 2026-07-04: 清掉 pivot 强制 timeout timer, 避免 hang 情况下 timer 在
|
|
5669
|
+
// process 里一直挂着. forceTimeout 在 try 外创建, finally 在闭包里
|
|
5670
|
+
// 能直接拿到引用.
|
|
5671
|
+
clearTimeout(forceTimeout);
|
|
5672
|
+
finishChannelRun(channelId, runState);
|
|
5673
|
+
// 2026-07-01 (v0.2.5): 持久化当前 messageHistory — 让 web 用户跨刷新保留对话.
|
|
5674
|
+
// saveCurrentSession 失败静默, 不阻塞 channel 状态清理.
|
|
5675
|
+
// saveCurrentSession 内部走 SessionStore (默认 ~/.bolloon/sessions/cache/<sessionKey>.json).
|
|
5676
|
+
// agent 可能在 try 抛错后仍为 null (getAgentForChannel 失败), null-guard 跳过 save.
|
|
5677
|
+
if (agent) {
|
|
5678
|
+
try {
|
|
5679
|
+
await agent.saveCurrentSession(sessionKey);
|
|
5680
|
+
}
|
|
5681
|
+
catch (saveErr) {
|
|
5682
|
+
console.warn(`[web] saveCurrentSession failed (non-fatal): ${saveErr.message?.slice(0, 100)}`);
|
|
5683
|
+
}
|
|
5634
5684
|
}
|
|
5635
|
-
|
|
5636
|
-
|
|
5685
|
+
// 2026-08-02: run-end skill 候选扫描 (fire-and-forget, 不阻塞 finally)
|
|
5686
|
+
// 从本轮 lastSteps 提取连续成功的工具调用模式, 写入 ~/.bolloon/skill-candidates/.
|
|
5687
|
+
// agent 之后可调 list_skill_candidates / promote_skill 决定是否转正.
|
|
5688
|
+
// 2026-08-04: 抽公共函数 writeRunEndSkillCandidates (Web server 与 CLI 共用)
|
|
5689
|
+
try {
|
|
5690
|
+
const steps = runState.lastSteps || [];
|
|
5691
|
+
const okTools = steps.filter(s => s.status === 'ok' && s.name && s.name !== 'system');
|
|
5692
|
+
if (okTools.length >= 2) {
|
|
5693
|
+
setImmediate(async () => {
|
|
5694
|
+
try {
|
|
5695
|
+
const { writeRunEndSkillCandidates } = await import('../agents/skill-writer.js');
|
|
5696
|
+
const r = await writeRunEndSkillCandidates(steps, `channel:${channelId}`);
|
|
5697
|
+
if (r.wrote)
|
|
5698
|
+
console.log(`[skill-candidates] 写入候选 ${r.file} (${r.count} tools)`);
|
|
5699
|
+
}
|
|
5700
|
+
catch (candErr) {
|
|
5701
|
+
console.warn('[skill-candidates] 写入失败 (non-fatal):', candErr?.message?.slice(0, 150));
|
|
5702
|
+
}
|
|
5703
|
+
});
|
|
5704
|
+
}
|
|
5637
5705
|
}
|
|
5706
|
+
catch { /* 非致命 */ }
|
|
5638
5707
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5708
|
+
};
|
|
5709
|
+
// 投进该 channel 的内核邮箱: 同通道 FIFO 串行 / 跨通道并行 (与 didFix 共用同一邮箱, 语义一致)。
|
|
5710
|
+
// 失败必须**响亮** (不许静默吞掉一条没跑的消息)。
|
|
5711
|
+
void getChannelQueue(channelId).submit(runChannelMessage).catch((e) => {
|
|
5712
|
+
console.error('[K8] runChannelMessage 失败:', e?.message?.slice(0, 200));
|
|
5643
5713
|
try {
|
|
5644
|
-
|
|
5645
|
-
const okTools = steps.filter(s => s.status === 'ok' && s.name && s.name !== 'system');
|
|
5646
|
-
if (okTools.length >= 2) {
|
|
5647
|
-
setImmediate(async () => {
|
|
5648
|
-
try {
|
|
5649
|
-
const { writeRunEndSkillCandidates } = await import('../agents/skill-writer.js');
|
|
5650
|
-
const r = await writeRunEndSkillCandidates(steps, `channel:${channelId}`);
|
|
5651
|
-
if (r.wrote)
|
|
5652
|
-
console.log(`[skill-candidates] 写入候选 ${r.file} (${r.count} tools)`);
|
|
5653
|
-
}
|
|
5654
|
-
catch (candErr) {
|
|
5655
|
-
console.warn('[skill-candidates] 写入失败 (non-fatal):', candErr?.message?.slice(0, 150));
|
|
5656
|
-
}
|
|
5657
|
-
});
|
|
5658
|
-
}
|
|
5714
|
+
broadcast({ type: 'error', content: `执行失败: ${String(e?.message || e).slice(0, 200)}` }, channelId);
|
|
5659
5715
|
}
|
|
5660
|
-
catch { /*
|
|
5661
|
-
|
|
5716
|
+
catch { /* 广播失败不影响错误已记 */ }
|
|
5717
|
+
try {
|
|
5718
|
+
broadcast({ type: 'done' }, channelId);
|
|
5719
|
+
}
|
|
5720
|
+
catch { /* 同上 */ }
|
|
5721
|
+
});
|
|
5662
5722
|
});
|
|
5663
5723
|
// ---------- 频道元数据后台修复队列 ----------
|
|
5664
5724
|
// 关键点: 旧实现会在每次 GET /channels 时同步执行 KeyManager.generate() + IPFS POST,
|
|
5665
5725
|
// 多频道场景下持续分配密钥对 + 发起 HTTP 请求, 几轮就会把 Node 内存撑爆。
|
|
5666
5726
|
// 新实现: 入队 + 节流(2s) + 单飞, 立刻返回当前 channels, 修复异步进行。
|
|
5667
|
-
const didFixQueue = new Set(); // 待修复的 channelId
|
|
5668
|
-
let didFixRunning = false;
|
|
5727
|
+
const didFixQueue = new Set(); // 待修复的 channelId (调度节流的**入队缓冲**, 执行已交内核邮箱)
|
|
5669
5728
|
let didFixTimer = null;
|
|
5670
5729
|
// 2026-08-02: channelRunState 已提升为模块级 (triggerRemoteFollowup 也访问), 这里复用
|
|
5671
5730
|
function getOrCreateRunState(channelId) {
|
|
5672
5731
|
let s = channelRunState.get(channelId);
|
|
5673
5732
|
if (!s) {
|
|
5674
|
-
s = { running: false,
|
|
5733
|
+
s = { running: false, lastSteps: [], lastSummary: '', lastFinalReply: '', lastTokens: {} };
|
|
5675
5734
|
channelRunState.set(channelId, s);
|
|
5676
5735
|
}
|
|
5677
5736
|
return s;
|
|
5678
5737
|
}
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
return '';
|
|
5683
|
-
return `[系统上下文] 用户上传了 ${parsedAttachments.length} 个附件: ` +
|
|
5684
|
-
parsedAttachments.map((a) => `${a.filename || a.attachmentId} (id=${a.attachmentId}, mime=${a.mimeType || '?'}, size=${a.size ?? '?'}B, URL=/api/attachments/${a.attachmentId})`).join('; ') +
|
|
5685
|
-
`\n你可以调用文件读工具 (curl /api/attachments/<id>) 拉取真实内容。\n\n`;
|
|
5686
|
-
}
|
|
5738
|
+
// K8 正刀 (2026-10-02): `runMessageFromQueue` (排在队尾执行的**简化版**第二条路径) 与它专用的
|
|
5739
|
+
// `buildAttachmentContextForQueue` 一并删除 —— 每条消息现在都走 `runChannelMessage` 这**一条**完整路径,
|
|
5740
|
+
// 通道自带的 queue 与异步 handoff 都不复存在 (旧 handoff 会静默丢消息, 见先行门 ③)。
|
|
5687
5741
|
/**
|
|
5688
|
-
* 2026-
|
|
5689
|
-
* 跑下一条 queued 消息.
|
|
5742
|
+
* 一轮跑完的收尾 (2026-10-02 K8 → K8 正刀)。
|
|
5690
5743
|
*
|
|
5691
|
-
*
|
|
5692
|
-
*
|
|
5693
|
-
*
|
|
5694
|
-
*
|
|
5695
|
-
*
|
|
5696
|
-
* - 处理 attachments: 跟主路径一样
|
|
5744
|
+
* **它现在只做三件事**: 清"在飞"标志 · 清 abort 句柄 · 广播状态。
|
|
5745
|
+
* **不再排空队列** —— 正刀后每条消息一进来就投该 channel 的**内核邮箱**
|
|
5746
|
+
* (`getChannelQueue(channelId).submit(runChannelMessage)`), 同通道 FIFO 串行由内核保证 (与 K5 同一条语义),
|
|
5747
|
+
* 通道自己不再兼任调度器, 也不再有"同步置 `running = false` + 异步 handoff"这一步
|
|
5748
|
+
* (正是那一步会让排队消息被静默丢掉 —— 见 `src/test/k8-mailbox-scheduling.test.ts` ③ 的机制复刻)。
|
|
5697
5749
|
*
|
|
5698
|
-
*
|
|
5699
|
-
*
|
|
5750
|
+
* 顺带删掉的: `runMessageFromQueue` (排在队尾执行的**简化版第二条路径**, 没有 judgment hint / persona /
|
|
5751
|
+
* slash 命令 / 附件上下文) 与它专用的 `buildAttachmentContextForQueue` —— 现在只有**一条**完整路径。
|
|
5700
5752
|
*/
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
runState.abortController = new AbortController();
|
|
5708
|
-
const currentSessionId = (await loadChannels()).find(c => c.id === channelId)?.currentSessionId || 'default';
|
|
5709
|
-
const realChannelDid = reqChannelDid || (await loadChannels()).find(c => c.id === channelId)?.did || '';
|
|
5710
|
-
const parsedAttachments = Array.isArray(attachments) ? attachments : [];
|
|
5711
|
-
const attachmentContext = buildAttachmentContextForQueue(parsedAttachments);
|
|
5712
|
-
const sessionKey = `${channelId}:${currentSessionId}`;
|
|
5713
|
-
// 防 LLM hang 安全网
|
|
5714
|
-
const PIVOT_FORCE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
5715
|
-
const forceTimeout = setTimeout(() => {
|
|
5716
|
-
console.warn(`[server] queue-drain pivot 强制 timeout, aborting`);
|
|
5717
|
-
runState.abortController?.abort();
|
|
5718
|
-
}, PIVOT_FORCE_TIMEOUT_MS);
|
|
5719
|
-
let agent = null;
|
|
5720
|
-
try {
|
|
5721
|
-
// 1) broadcast user 给前端 (跟主路径一致)
|
|
5722
|
-
broadcast({ type: 'user', content: text }, channelId);
|
|
5723
|
-
// 2) 取 agent + session
|
|
5724
|
-
agent = await getAgentForChannel(channelId, currentSessionId).catch(() => null);
|
|
5725
|
-
if (!agent) {
|
|
5726
|
-
throw new Error(`No agent for channel=${channelId}`);
|
|
5727
|
-
}
|
|
5728
|
-
// 3) 重 build contextHint (基本版, 不全 500 行 hook)
|
|
5729
|
-
// 原因: queue-drain 是常见调试路径, 全量 hooks 性能大. 关键是 attachments 带上.
|
|
5730
|
-
const contextHint = attachmentContext + `[系统上下文] 队列消息 (auto-drain)\n`;
|
|
5731
|
-
// 4) promptStream
|
|
5732
|
-
const markedPrompt = `【本轮用户请求】\n${text}\n【请求结束】\n\n${contextHint}`;
|
|
5733
|
-
const fullResponse = await agent.promptStream(markedPrompt, () => { }, runState.abortController?.signal, channelId);
|
|
5734
|
-
if (!fullResponse.trim()) {
|
|
5735
|
-
broadcast({ type: 'error', content: '⚠️ AI 未返回内容' }, channelId);
|
|
5736
|
-
}
|
|
5737
|
-
else {
|
|
5738
|
-
broadcast({ type: 'ai', content: fullResponse }, channelId);
|
|
5739
|
-
// 落 session
|
|
5740
|
-
try {
|
|
5741
|
-
const existing = await loadSession(channelId, currentSessionId);
|
|
5742
|
-
const session = existing || { channelId, sessionId: currentSessionId, messages: [], lastUpdated: new Date().toISOString() };
|
|
5743
|
-
session.sessionId = currentSessionId;
|
|
5744
|
-
// 跟主路径相同: 不重复 push user (主路径已 broadcast/push), 只 push ai
|
|
5745
|
-
session.messages.push({
|
|
5746
|
-
id: crypto.randomUUID(),
|
|
5747
|
-
type: 'ai',
|
|
5748
|
-
content: fullResponse,
|
|
5749
|
-
timestamp: new Date().toISOString(),
|
|
5750
|
-
source: 'local',
|
|
5751
|
-
});
|
|
5752
|
-
session.lastUpdated = new Date().toISOString();
|
|
5753
|
-
await saveSession(session);
|
|
5754
|
-
}
|
|
5755
|
-
catch (e) {
|
|
5756
|
-
console.warn('[queue-drain] saveSession failed:', e?.message?.slice(0, 100));
|
|
5757
|
-
}
|
|
5758
|
-
}
|
|
5759
|
-
broadcast({ type: 'done' }, channelId);
|
|
5760
|
-
}
|
|
5761
|
-
catch (err) {
|
|
5762
|
-
console.warn('[queue-drain] failed:', err?.message?.slice(0, 200));
|
|
5763
|
-
broadcast({ type: 'error', content: 'queue-drain: ' + (err?.message || 'failed') }, channelId);
|
|
5764
|
-
}
|
|
5765
|
-
finally {
|
|
5766
|
-
clearTimeout(forceTimeout);
|
|
5767
|
-
runState.running = false;
|
|
5768
|
-
runState.abortController = null;
|
|
5769
|
-
broadcastQueueUpdate(channelId);
|
|
5770
|
-
// 递归 drain — 同 /message 主路径 finally 行为保持一致
|
|
5771
|
-
if (runState.queue.length > 0) {
|
|
5772
|
-
const next = runState.queue.shift();
|
|
5773
|
-
console.log(`[queue-drain-recursive] channel=${next.channelId} text="${next.text.slice(0, 30)}"`);
|
|
5774
|
-
setImmediate(() => {
|
|
5775
|
-
void runMessageFromQueue(next).catch((e) => {
|
|
5776
|
-
console.error('[queue-drain-recursive] error:', e?.message?.slice(0, 200));
|
|
5777
|
-
});
|
|
5778
|
-
});
|
|
5779
|
-
}
|
|
5780
|
-
if (agent) {
|
|
5781
|
-
try {
|
|
5782
|
-
await agent.saveCurrentSession(sessionKey);
|
|
5783
|
-
}
|
|
5784
|
-
catch { }
|
|
5785
|
-
}
|
|
5786
|
-
}
|
|
5753
|
+
function finishChannelRun(channelId, runState) {
|
|
5754
|
+
// K8 正刀 (2026-10-02): **这里不再排空队列** —— 每条消息自己进内核邮箱, 通道不持有队列,
|
|
5755
|
+
// 也就不存在"同步置 running=false + 异步 handoff"那一步 (那一步会静默丢消息, 见先行门 ③)。
|
|
5756
|
+
runState.running = false;
|
|
5757
|
+
// K8 收尾: 取消位不在这里复位 —— 它属于 actor, 下一次 beginCancellation() 自然换代
|
|
5758
|
+
broadcastQueueUpdate(channelId);
|
|
5787
5759
|
}
|
|
5788
5760
|
function broadcastQueueUpdate(channelId) {
|
|
5789
5761
|
const s = channelRunState.get(channelId);
|
|
5790
|
-
const queueLength = s ? s.queue.length : 0;
|
|
5791
5762
|
const running = s ? s.running : false;
|
|
5763
|
+
// K8 正刀 (2026-10-02): 队列长度改读**内核邮箱** (通道不再自己数) —— pending 含正在跑的那条, 减掉它才是"等待中"。
|
|
5764
|
+
const pending = getChannelQueue(channelId).pending;
|
|
5765
|
+
const queueLength = Math.max(0, pending - (running ? 1 : 0));
|
|
5792
5766
|
try {
|
|
5793
5767
|
broadcast({ type: 'queue_update', channelId, queueLength, running }, channelId);
|
|
5794
5768
|
}
|
|
@@ -5803,25 +5777,28 @@ fetchState();
|
|
|
5803
5777
|
void runDidFixOnce();
|
|
5804
5778
|
}, 2000);
|
|
5805
5779
|
}
|
|
5780
|
+
/**
|
|
5781
|
+
* K8 收口 (2026-10-02): 待办**不再由通道自己单飞**。
|
|
5782
|
+
*
|
|
5783
|
+
* 旧写法 = `didFixRunning` 全局单飞 + while 循环自己取件 ⇒ "各通道自己的调度状态"。
|
|
5784
|
+
* 新写法 = 执行投进该 channel 的**内核邮箱** (`getChannelQueue`), 由 `SerialMailbox` 串行:
|
|
5785
|
+
* · 同 channel 的修复**串行** (不会两个 repairs 同时 load/save 同一份 channels)
|
|
5786
|
+
* · 跨 channel 可并行 (K5 既定语义: 同通道串行 / 跨通道并行)
|
|
5787
|
+
* 2s 节流 + 入队 Set 保留 (那是**调度策略**, 不是执行机制)。
|
|
5788
|
+
*
|
|
5789
|
+
* ⚠️ 行为差量 (如实记): 旧实现全局一次一个 repair; 新实现同通道串行、跨通道可并行。
|
|
5790
|
+
*/
|
|
5806
5791
|
async function runDidFixOnce() {
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5812
|
-
const id = didFixQueue.values().next().value;
|
|
5813
|
-
didFixQueue.delete(id);
|
|
5814
|
-
try {
|
|
5815
|
-
await fixOneChannelDID(id);
|
|
5816
|
-
}
|
|
5817
|
-
catch (e) {
|
|
5818
|
-
console.log(`[DID 修复] ${id} 失败: ${e.message}`);
|
|
5819
|
-
}
|
|
5792
|
+
const ids = [...didFixQueue];
|
|
5793
|
+
didFixQueue.clear();
|
|
5794
|
+
await Promise.all(ids.map((id) => getChannelQueue(id).submit(async () => {
|
|
5795
|
+
try {
|
|
5796
|
+
await fixOneChannelDID(id);
|
|
5820
5797
|
}
|
|
5821
|
-
|
|
5822
|
-
|
|
5823
|
-
|
|
5824
|
-
}
|
|
5798
|
+
catch (e) {
|
|
5799
|
+
console.log(`[DID 修复] ${id} 失败: ${e.message}`);
|
|
5800
|
+
}
|
|
5801
|
+
})));
|
|
5825
5802
|
}
|
|
5826
5803
|
async function fixOneChannelDID(channelId) {
|
|
5827
5804
|
const channels = await loadChannels();
|
|
@@ -6932,7 +6909,8 @@ fetchState();
|
|
|
6932
6909
|
// 2026-06-15: 同 /message 路径, 用显式 marker 包裹 userMessage, 避免 LLM 把它当背景信息
|
|
6933
6910
|
const regenHint = await buildJudgmentHint(channel, channelId);
|
|
6934
6911
|
const markedRegen = `${regenHint}\n\n【本轮用户请求】\n${userMessage}\n【请求结束】\n`;
|
|
6935
|
-
|
|
6912
|
+
// 2026-10-02 (K8): 同上 (零差量)
|
|
6913
|
+
fullResponse = await deliverThroughActor(agent, () => requireRunExecution(agent)({ input: markedRegen, onStream: streamCallback, channelId }));
|
|
6936
6914
|
// 2026-07-06: 防御性兜底 — 同 /message 路径
|
|
6937
6915
|
if (!fullResponse.trim()) {
|
|
6938
6916
|
fullResponse = '⚠️ AI 未返回内容, 请重试';
|
|
@@ -8011,10 +7989,10 @@ fetchState();
|
|
|
8011
7989
|
const { channelId } = req.body;
|
|
8012
7990
|
if (!channelId)
|
|
8013
7991
|
return res.status(400).json({ error: 'channelId required' });
|
|
8014
|
-
|
|
8015
|
-
|
|
8016
|
-
|
|
8017
|
-
console.log(`[abort] user aborted channel=${channelId}`);
|
|
7992
|
+
// K8 收尾: 中止经内核 actor 注册表 (按 channel 扫描; 返回**真的中止了几个**)
|
|
7993
|
+
const aborted = abortActorsOfChannel(channelId, 'user abort');
|
|
7994
|
+
if (aborted > 0) {
|
|
7995
|
+
console.log(`[abort] user aborted channel=${channelId} (${aborted} 个 actor)`);
|
|
8018
7996
|
// 2026-08-02 fix: abort 后立即广播 done — 之前前端靠 1.5s 兜底 setTimeout 切回 idle,
|
|
8019
7997
|
// 视觉上"点了没反应". 这里主动推 done, 前端 finalizeTimelineAsMessage + setSendMode('idle') 立刻生效.
|
|
8020
7998
|
try {
|