@bolloon/bolloon-agent 0.1.34 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.auto-evolve-calls +1 -0
  2. package/.last-auto-evolve-baseline +1 -0
  3. package/Bolloon.md +103 -0
  4. package/dist/agents/pi-sdk.js +264 -12
  5. package/dist/bootstrap/bootstrap.js +114 -0
  6. package/dist/bootstrap/context-collector.js +296 -0
  7. package/dist/bootstrap/lifecycle-hooks.js +109 -0
  8. package/dist/bootstrap/project-context.js +151 -0
  9. package/dist/index.js +11 -0
  10. package/dist/llm/pi-ai.js +31 -21
  11. package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
  12. package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
  13. package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
  14. package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
  15. package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
  16. package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
  17. package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
  18. package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
  19. package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
  20. package/dist/security/builtin-guards.js +124 -0
  21. package/dist/security/context-router-tool.js +106 -0
  22. package/dist/security/react-harness.js +143 -0
  23. package/dist/security/tool-gate.js +235 -0
  24. package/dist/utils/auto-evolve-policy.js +117 -0
  25. package/dist/utils/clamp.js +7 -0
  26. package/dist/utils/double.js +6 -0
  27. package/dist/web/client.js +668 -204
  28. package/dist/web/index.html +24 -4
  29. package/dist/web/server.js +531 -10
  30. package/lefthook.yml +29 -0
  31. package/package.json +3 -2
  32. package/scripts/auto-evolve-loop.ts +376 -0
  33. package/scripts/auto-evolve-oneshot.sh +155 -0
  34. package/scripts/auto-evolve-snapshot.sh +136 -0
  35. package/scripts/detect-schema-changes.sh +48 -0
  36. package/scripts/diff-reviewer.ts +159 -0
  37. package/scripts/weekly-report.ts +364 -0
  38. package/src/agents/pi-sdk.ts +293 -15
  39. package/src/bootstrap/bootstrap.ts +132 -0
  40. package/src/bootstrap/context-collector.ts +342 -0
  41. package/src/bootstrap/lifecycle-hooks.ts +176 -0
  42. package/src/bootstrap/project-context.ts +163 -0
  43. package/src/index.ts +11 -0
  44. package/src/llm/pi-ai.ts +33 -22
  45. package/src/security/builtin-guards.ts +162 -0
  46. package/src/security/context-router-tool.ts +122 -0
  47. package/src/security/react-harness.ts +177 -0
  48. package/src/security/tool-gate.ts +294 -0
  49. package/src/utils/auto-evolve-policy.ts +138 -0
  50. package/src/utils/clamp.ts +5 -0
  51. package/src/web/client.js +668 -204
  52. package/src/web/index.html +24 -4
  53. package/src/web/server.ts +596 -10
  54. package/staging/auto-evolve/clean-001/.review-verdict +9 -0
  55. package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
  56. package/staging/auto-evolve/e2e-001/.patch-id +1 -0
  57. package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
  58. package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
  59. package/staging/auto-evolve/test-bad/.review-verdict +12 -0
  60. package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
package/src/web/server.ts CHANGED
@@ -5,6 +5,7 @@ import { fileURLToPath } from 'url';
5
5
  import * as fs from 'fs/promises';
6
6
  import * as fsSync from 'fs';
7
7
  import * as path from 'path';
8
+ import * as os from 'os';
8
9
  import {
9
10
  HyperswarmCommunicator,
10
11
  createHyperswarmCommunicator,
@@ -816,7 +817,13 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
816
817
  let fullResponse = '';
817
818
  // v3 新增: 流式 token 节流推给 B — 让 B 看到过程
818
819
  let lastFlushAt = 0;
820
+ let usedJudgmentIds: string[] = [];
819
821
  const streamCallback: any = (event: any) => {
822
+ // P0.5: 注入门回传
823
+ if (event?.type === 'used_judgments' && Array.isArray(event.usedIds)) {
824
+ usedJudgmentIds = event.usedIds;
825
+ return;
826
+ }
820
827
  if (event.type === 'token') {
821
828
  fullResponse += event.content;
822
829
  if (fullResponse.length - lastFlushAt >= 20) {
@@ -830,7 +837,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
830
837
  }
831
838
  };
832
839
  const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
833
- fullResponse = await agent.promptStream(fullPrompt, streamCallback);
840
+ fullResponse = await agent.promptStream(fullPrompt, streamCallback, undefined, channelId);
834
841
 
835
842
  // v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
836
843
  try {
@@ -842,6 +849,7 @@ async function handleV3P2PMessage(parsed: any, conn: P2PConnection, comm: Hypers
842
849
  id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
843
850
  type: 'ai',
844
851
  content: fullResponse,
852
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
845
853
  timestamp: new Date().toISOString()
846
854
  });
847
855
  session.lastUpdated = new Date().toISOString();
@@ -1170,6 +1178,18 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1170
1178
  console.error('[警告] 未处理的 Promise 拒绝:', reason);
1171
1179
  });
1172
1180
 
1181
+ // Bolloon Bootstrap (幂等, 重复调不会重复挂定时器)
1182
+ // 这里独立调一次以保证 CLI-only 模式 (无 index.ts 引导) 也能 bootstrap
1183
+ try {
1184
+ const { bootstrapBolloon } = await import(
1185
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
1186
+ );
1187
+ const bs = await bootstrapBolloon({ cwd: process.cwd() });
1188
+ console.log(`[createWebServer] bootstrap 完成 (${bs.durationMs}ms)`);
1189
+ } catch (err) {
1190
+ console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
1191
+ }
1192
+
1173
1193
  // 重置旧的 agent session,确保使用新的 LLM 配置
1174
1194
  const { resetAgentSession } = await import('../agents/pi-sdk.js');
1175
1195
  resetAgentSession();
@@ -1630,11 +1650,42 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1630
1650
  // 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
1631
1651
  const channelForJudgment = channel;
1632
1652
 
1653
+ // per-channel queue 检查: 已在跑就入队, 等当前跑完自动接上
1654
+ const runState = getOrCreateRunState(channelId);
1655
+ if (runState.running) {
1656
+ runState.queue.push({ channelId, text, boundWalletAddress, autoToolsEnabled });
1657
+ broadcastQueueUpdate(channelId);
1658
+ console.log(`[queue] /message 入队 channel=${channelId}, queue len=${runState.queue.length}`);
1659
+ return;
1660
+ }
1661
+ runState.running = true;
1662
+ runState.abortController = new AbortController();
1663
+ broadcastQueueUpdate(channelId);
1664
+
1633
1665
  try {
1634
1666
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
1635
1667
  let fullResponse = '';
1668
+ // P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
1669
+ let usedJudgmentIds: string[] = [];
1636
1670
 
1637
1671
  const streamCallback: StreamCallback = (event: StreamEvent) => {
1672
+ // P0.5: 捕获注入门回传
1673
+ if ((event as any).type === 'used_judgments' && Array.isArray((event as any).usedIds)) {
1674
+ usedJudgmentIds = (event as any).usedIds;
1675
+ // 同步推给前端 (用于 finalizeTimelineAsMessage 时给 addMessage 传 usedIds)
1676
+ broadcast({ type: 'used_judgments', usedIds: usedJudgmentIds }, channelId);
1677
+ return;
1678
+ }
1679
+ // 阶段事件 (注入门 / D 触发)
1680
+ if ((event as any).type === 'phase') {
1681
+ broadcast({
1682
+ type: 'phase',
1683
+ phase: (event as any).phase,
1684
+ detail: (event as any).detail,
1685
+ usedCount: (event as any).usedCount,
1686
+ }, channelId);
1687
+ return;
1688
+ }
1638
1689
  // 同时发送给流式显示和工作流显示
1639
1690
  if (event.type === 'token' || event.type === 'thinking') {
1640
1691
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
@@ -1796,7 +1847,20 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1796
1847
  }
1797
1848
 
1798
1849
  if (contextHint) contextHint += '\n';
1799
- fullResponse = await agent.promptStream(contextHint + text, streamCallback);
1850
+ try {
1851
+ fullResponse = await agent.promptStream(contextHint + text, streamCallback, runState.abortController?.signal, channelId);
1852
+ } catch (err: any) {
1853
+ // abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
1854
+ if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
1855
+ console.log(`[chat] aborted channel=${channelId}`);
1856
+ } else {
1857
+ throw err;
1858
+ }
1859
+ }
1860
+ // abort 模式: 给 partial 拼后缀
1861
+ if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
1862
+ fullResponse = fullResponse + '\n\n_[生成已中断]_';
1863
+ }
1800
1864
 
1801
1865
  // v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
1802
1866
  await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
@@ -1808,7 +1872,15 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1808
1872
  session.sessionId = currentSessionId;
1809
1873
  // v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
1810
1874
  session.messages.push({ id: crypto.randomUUID(), type: 'user' as const, content: text, timestamp: new Date().toISOString(), source: 'local' as any });
1811
- session.messages.push({ id: crypto.randomUUID(), type: 'ai' as const, content: fullResponse, timestamp: new Date().toISOString(), source: 'local' as any });
1875
+ session.messages.push({
1876
+ id: crypto.randomUUID(),
1877
+ type: 'ai' as const,
1878
+ content: fullResponse,
1879
+ timestamp: new Date().toISOString(),
1880
+ source: 'local' as any,
1881
+ // P0.5: 这条 AI 回复引用了哪些 judgment (注入门回传)
1882
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
1883
+ });
1812
1884
  session.lastUpdated = new Date().toISOString();
1813
1885
  await saveSession(session);
1814
1886
 
@@ -1823,12 +1895,62 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1823
1895
  }
1824
1896
 
1825
1897
  broadcast({ type: 'done' }, channelId);
1898
+
1899
+ // D 触发: AI 被动捕获判断力 (后台异步, 不阻塞主对话)
1900
+ setImmediate(() => {
1901
+ try {
1902
+ const lastTurns = session.messages.slice(-6).map((m) => ({
1903
+ role: (m.type === 'user' ? 'human' : 'agent') as 'human' | 'agent',
1904
+ content: m.content,
1905
+ }));
1906
+ if (lastTurns.length < 2) return;
1907
+ broadcast({ type: 'phase', phase: 'd_detect', detail: '监测对话...' }, channelId);
1908
+ import('../pi-ecosystem-judgment/human-value-pipeline.js')
1909
+ .then(async ({ detectAndDistillFromChannel, throttleDHook }) => {
1910
+ // channel 维度 5min 节流, 防对话卡顿时 LLM 反复触发
1911
+ if (!throttleDHook(channelId, 5 * 60_000)) {
1912
+ console.log(`[D-hook ${channelId}] throttled (within 5min)`);
1913
+ broadcast({ type: 'phase', phase: 'd_skip', detail: 'throttled' }, channelId);
1914
+ return null;
1915
+ }
1916
+ broadcast({ type: 'phase', phase: 'd_distill', detail: '蒸馏判断力...' }, channelId);
1917
+ return detectAndDistillFromChannel(lastTurns, { channelId });
1918
+ })
1919
+ .then((result) => {
1920
+ if (result && result.triggered) {
1921
+ console.log(
1922
+ `[D-hook ${channelId}] stored: ${result.reason}`,
1923
+ result.evolved
1924
+ );
1925
+ broadcast({ type: 'phase', phase: 'd_done', detail: result.reason }, channelId);
1926
+ } else if (result && result.reason) {
1927
+ console.log(`[D-hook ${channelId}] skipped: ${result.reason}`);
1928
+ broadcast({ type: 'phase', phase: 'd_skip', detail: result.reason }, channelId);
1929
+ }
1930
+ })
1931
+ .catch((err) => {
1932
+ console.warn(`[D-hook ${channelId}] failed:`, err);
1933
+ broadcast({ type: 'phase', phase: 'd_error', detail: String(err) }, channelId);
1934
+ });
1935
+ } catch (err) {
1936
+ console.warn(`[D-hook ${channelId}] sync error:`, err);
1937
+ }
1938
+ });
1939
+
1826
1940
  // 2026-06-11: 202 已发的话, 不要重复 res.json (会抛 ERR_HTTP_HEADERS_SENT)
1827
1941
  if (!res.headersSent) res.json({ ok: true });
1828
1942
  } catch (err: any) {
1829
1943
  broadcast({ type: 'error', content: err.message }, channelId);
1830
1944
  broadcast({ type: 'done' }, channelId);
1831
1945
  if (!res.headersSent) res.status(500).json({ error: err.message });
1946
+ } finally {
1947
+ // queue dequeue: 跑完或失败都要清状态
1948
+ // 当前实现: 自动接下一条需要把 ~200 行 try 块抽函数, 暂不抽.
1949
+ // 替代: 用户点 [队列 +N] 按钮时, 客户端发起一个特殊的 HTTP 请求触发下一条
1950
+ // (在 client.js 实现). 这里只清状态 + 广播.
1951
+ runState.running = false;
1952
+ runState.abortController = null;
1953
+ broadcastQueueUpdate(channelId);
1832
1954
  }
1833
1955
  });
1834
1956
 
@@ -1840,6 +1962,36 @@ export async function createWebServer(port: number = 3000, options: CreateWebSer
1840
1962
  let didFixRunning = false;
1841
1963
  let didFixTimer: NodeJS.Timeout | null = null;
1842
1964
 
1965
+ // ---------- per-channel 消息 queue + abort 状态 ----------
1966
+ // 同 channel 串行 (避免 LLM 调用互踩上下文), 跨 channel 互不干扰
1967
+ interface PendingMessage {
1968
+ channelId: string;
1969
+ text: string;
1970
+ boundWalletAddress?: string;
1971
+ autoToolsEnabled?: boolean;
1972
+ // (req, res 已经在 /message 里 res.status(202) 返回, 入队的只是要重跑的内容参数)
1973
+ }
1974
+ interface ChannelRunState {
1975
+ running: boolean;
1976
+ queue: PendingMessage[];
1977
+ abortController: AbortController | null;
1978
+ }
1979
+ const channelRunState: Map<string, ChannelRunState> = new Map();
1980
+ function getOrCreateRunState(channelId: string): ChannelRunState {
1981
+ let s = channelRunState.get(channelId);
1982
+ if (!s) {
1983
+ s = { running: false, queue: [], abortController: null };
1984
+ channelRunState.set(channelId, s);
1985
+ }
1986
+ return s;
1987
+ }
1988
+ function broadcastQueueUpdate(channelId: string): void {
1989
+ const s = channelRunState.get(channelId);
1990
+ const queueLength = s ? s.queue.length : 0;
1991
+ const running = s ? s.running : false;
1992
+ try { broadcast({ type: 'queue_update', channelId, queueLength, running }, channelId); } catch { /* */ }
1993
+ }
1994
+
1843
1995
  function scheduleDidFix(channelId: string) {
1844
1996
  didFixQueue.add(channelId);
1845
1997
  if (didFixTimer) return;
@@ -2555,8 +2707,14 @@ app.get('/channels', async (_req, res) => {
2555
2707
 
2556
2708
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
2557
2709
  let fullResponse = '';
2710
+ let usedJudgmentIds: string[] = [];
2558
2711
 
2559
2712
  const streamCallback: StreamCallback = (event: StreamEvent) => {
2713
+ // P0.5: 注入门回传
2714
+ if ((event as any).type === 'used_judgments' && Array.isArray((event as any).usedIds)) {
2715
+ usedJudgmentIds = (event as any).usedIds;
2716
+ return;
2717
+ }
2560
2718
  if (event.type === 'token' || event.type === 'thinking') {
2561
2719
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
2562
2720
  } else if (event.type === 'status' || event.type === 'tool') {
@@ -2568,7 +2726,7 @@ app.get('/channels', async (_req, res) => {
2568
2726
 
2569
2727
  // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
2570
2728
  const regenHint = await buildJudgmentHint(channel, channelId);
2571
- fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
2729
+ fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback, undefined, channelId);
2572
2730
 
2573
2731
  broadcast({ type: 'ai', content: fullResponse }, channelId);
2574
2732
 
@@ -2584,7 +2742,8 @@ app.get('/channels', async (_req, res) => {
2584
2742
  id: crypto.randomUUID(),
2585
2743
  type: 'ai' as const,
2586
2744
  content: fullResponse,
2587
- timestamp: new Date().toISOString()
2745
+ timestamp: new Date().toISOString(),
2746
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
2588
2747
  });
2589
2748
  existingSession.lastUpdated = new Date().toISOString();
2590
2749
  await saveSession(existingSession);
@@ -4002,6 +4161,23 @@ app.get('/channels', async (_req, res) => {
4002
4161
  }
4003
4162
  });
4004
4163
 
4164
+ // 终止当前 channel 的 LLM 流 (UI 终止按钮)
4165
+ app.post('/api/chat/abort', async (req, res) => {
4166
+ try {
4167
+ const { channelId } = req.body as { channelId?: string };
4168
+ if (!channelId) return res.status(400).json({ error: 'channelId required' });
4169
+ const s = channelRunState.get(channelId);
4170
+ if (s?.abortController) {
4171
+ s.abortController.abort();
4172
+ console.log(`[abort] user aborted channel=${channelId}`);
4173
+ return res.json({ ok: true, aborted: true });
4174
+ }
4175
+ res.json({ ok: true, aborted: false });
4176
+ } catch (err: any) {
4177
+ res.status(500).json({ error: err.message });
4178
+ }
4179
+ });
4180
+
4005
4181
  // 主人审阅: 批准 draft
4006
4182
  app.post('/api/chat/approve', async (req, res) => {
4007
4183
  try {
@@ -4417,22 +4593,432 @@ app.get('/channels', async (_req, res) => {
4417
4593
  }
4418
4594
  });
4419
4595
 
4420
- app.get('/api/judgments', async (_req, res) => {
4596
+ app.get('/api/judgments', async (req, res) => {
4421
4597
  try {
4422
- const { loadAllJudgments, initializeValueStore } = await import(
4598
+ const { listJudgmentsByStatus, initializeValueStore } = await import(
4423
4599
  '../pi-ecosystem-judgment/human-value-store.js'
4424
4600
  );
4425
4601
  await initializeValueStore();
4426
- const all = await loadAllJudgments();
4427
- // 新的在前
4602
+ const status = (typeof req.query.status === 'string' ? req.query.status : 'all') as
4603
+ | 'active'
4604
+ | 'pending'
4605
+ | 'superseded'
4606
+ | 'rejected'
4607
+ | 'all';
4608
+ const all = await listJudgmentsByStatus(status);
4428
4609
  all.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
4429
- res.json({ count: all.length, judgments: all });
4610
+ res.json({ count: all.length, status, judgments: all });
4430
4611
  } catch (err: any) {
4431
4612
  console.error('[judgments] GET failed:', err);
4432
4613
  res.status(500).json({ error: err.message });
4433
4614
  }
4434
4615
  });
4435
4616
 
4617
+ // 蒸馏 B 触发 (人类点按钮) — 同步执行演化对齐
4618
+ app.post('/api/judgments/distill-from-conversation', async (req, res) => {
4619
+ try {
4620
+ const { channelId, messageId, recentTurns } = req.body as {
4621
+ channelId?: string;
4622
+ messageId?: string;
4623
+ recentTurns?: number;
4624
+ };
4625
+ if (!channelId) {
4626
+ return res.status(400).json({ error: 'channelId required' });
4627
+ }
4628
+
4629
+ // 取 channel 最近的对话
4630
+ const channels = await loadChannels();
4631
+ const channel = channels.find((c) => c.id === channelId);
4632
+ if (!channel) return res.status(404).json({ error: 'channel not found' });
4633
+
4634
+ const currentSessionId = channel.currentSessionId;
4635
+ if (!currentSessionId) {
4636
+ return res.status(400).json({ error: 'no active session in channel' });
4637
+ }
4638
+ const session = await loadSession(channelId, currentSessionId);
4639
+ if (!session) return res.status(404).json({ error: 'session not found' });
4640
+
4641
+ // 取最近 N 轮 (默认 10), 转成 DistillTurn 格式
4642
+ const limit = Math.min(Math.max(recentTurns ?? 10, 2), 30);
4643
+ const turns = session.messages.slice(-limit).map((m) => ({
4644
+ role: (m.type === 'user' ? 'human' : 'agent') as 'human' | 'agent',
4645
+ content: m.content,
4646
+ }));
4647
+
4648
+ const { distillAndStoreFromChannel } = await import(
4649
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4650
+ );
4651
+ const result = await distillAndStoreFromChannel(turns, { channelId });
4652
+
4653
+ res.json({
4654
+ ok: true,
4655
+ triggered: result.triggered,
4656
+ reason: result.reason,
4657
+ judgment: result.judgment,
4658
+ evolved: result.evolved,
4659
+ });
4660
+ } catch (err: any) {
4661
+ console.error('[judgments] distill-from-conversation failed:', err);
4662
+ res.status(500).json({ error: err.message });
4663
+ }
4664
+ });
4665
+
4666
+ // 蒸馏 D 触发 (AI 被动) — 后台异步,不阻塞 HTTP 响应
4667
+ app.post('/api/judgments/detect-and-distill', async (req, res) => {
4668
+ try {
4669
+ const { channelId, turns } = req.body as {
4670
+ channelId?: string;
4671
+ turns?: Array<{ role: 'human' | 'agent'; content: string }>;
4672
+ };
4673
+
4674
+ // 先立即返回 202, 不等 LLM
4675
+ res.status(202).json({ ok: true, queued: true });
4676
+
4677
+ if (!channelId || !Array.isArray(turns) || turns.length === 0) {
4678
+ return;
4679
+ }
4680
+
4681
+ // 异步处理 (不 await, 不阻塞响应)
4682
+ setImmediate(async () => {
4683
+ try {
4684
+ const { detectAndDistillFromChannel } = await import(
4685
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4686
+ );
4687
+ const result = await detectAndDistillFromChannel(turns, { channelId });
4688
+ if (result.triggered) {
4689
+ console.log(`[D-hook] ${channelId}: ${result.reason}`, result.evolved);
4690
+ }
4691
+ } catch (err) {
4692
+ console.warn('[D-hook] background failed:', err);
4693
+ }
4694
+ });
4695
+ } catch (err: any) {
4696
+ console.error('[judgments] detect-and-distill failed:', err);
4697
+ res.status(500).json({ error: err.message });
4698
+ }
4699
+ });
4700
+
4701
+ // 判断力使用回溯 (P0.5): 给定 judgmentIds, 反查对应的 decision 文本
4702
+ // 用途: UI 上"这条 AI 回复引用了哪些原则"
4703
+ app.post('/api/judgments/resolve-usage', async (req, res) => {
4704
+ try {
4705
+ const { ids } = req.body as { ids?: string[] };
4706
+ if (!Array.isArray(ids) || ids.length === 0) {
4707
+ return res.json({ items: [] });
4708
+ }
4709
+ const { loadAllJudgments } = await import(
4710
+ '../pi-ecosystem-judgment/human-value-store.js'
4711
+ );
4712
+ const all = await loadAllJudgments();
4713
+ const byId = new Map(all.map((j) => [j.id, j]));
4714
+ const items = ids
4715
+ .map((id) => byId.get(id))
4716
+ .filter((j): j is NonNullable<typeof j> => Boolean(j))
4717
+ .map((j) => ({
4718
+ id: j.id,
4719
+ decision: j.decision,
4720
+ status: j.status ?? 'active',
4721
+ timestamp: j.timestamp,
4722
+ }));
4723
+ res.json({ items });
4724
+ } catch (err: any) {
4725
+ console.error('[judgments] resolve-usage failed:', err);
4726
+ res.status(500).json({ error: err.message });
4727
+ }
4728
+ });
4729
+
4730
+ // 判断力违规日志 (P3 UI): 读 violations.jsonl
4731
+ app.get('/api/judgments/violations', async (req, res) => {
4732
+ try {
4733
+ const { getRecentViolations } = await import(
4734
+ '../pi-ecosystem-judgment/monitor-gate.js'
4735
+ );
4736
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
4737
+ const items = await getRecentViolations(limit);
4738
+ res.json({ count: items.length, items });
4739
+ } catch (err: any) {
4740
+ console.error('[judgments] violations failed:', err);
4741
+ res.status(500).json({ error: err.message });
4742
+ }
4743
+ });
4744
+
4745
+ // 类 B 自适应扫描: 读 judgments.json + usage.jsonl, 给出 stale/rising/unused 建议
4746
+ // ?force=1 跳过 24h 缓存
4747
+ app.get('/api/judgments/adaptive-suggestions', async (req, res) => {
4748
+ try {
4749
+ const { getCachedScan } = await import(
4750
+ '../pi-ecosystem-judgment/adaptive-scan.js'
4751
+ );
4752
+ const force = String(req.query.force ?? '') === '1';
4753
+ const result = await getCachedScan(force);
4754
+ res.json(result);
4755
+ } catch (err: any) {
4756
+ console.error('[judgments] adaptive-scan failed:', err);
4757
+ res.status(500).json({ error: err.message });
4758
+ }
4759
+ });
4760
+
4761
+ // Bootstrap Context 调试视图: 返出完整 BolloonContext
4762
+ app.get('/api/bolloon/context', async (req, res) => {
4763
+ try {
4764
+ const { getCachedBolloonContext } = await import(
4765
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4766
+ );
4767
+ const force = String(req.query.force ?? '') === '1';
4768
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() }, force);
4769
+ res.json(ctx);
4770
+ } catch (err: any) {
4771
+ console.error('[bolloon] context failed:', err);
4772
+ res.status(500).json({ error: err.message });
4773
+ }
4774
+ });
4775
+
4776
+ // 阶段 B: 周报 (weekly-report.ts 产物) — 仅 API 读取, 不做 UI tab
4777
+ // GET /api/reports → { files: ['2026-W24.md', ...] }
4778
+ // GET /api/reports/2026-W24 → { week, content }
4779
+ app.get('/api/reports', async (_req, res) => {
4780
+ try {
4781
+ const dir = path.join(os.homedir(), '.bolloon', 'reports');
4782
+ try {
4783
+ const entries = await fs.readdir(dir);
4784
+ const files = entries
4785
+ .filter((f) => f.endsWith('.md'))
4786
+ .sort()
4787
+ .reverse(); // 新的在前
4788
+ res.json({ dir, files });
4789
+ } catch {
4790
+ res.json({ dir, files: [] });
4791
+ }
4792
+ } catch (err: any) {
4793
+ res.status(500).json({ error: err.message });
4794
+ }
4795
+ });
4796
+
4797
+ app.get('/api/reports/:week', async (req, res) => {
4798
+ try {
4799
+ const week = req.params.week;
4800
+ // 严格校验, 防路径穿越
4801
+ if (!/^\d{4}-W\d{1,2}$/.test(week)) {
4802
+ return res.status(400).json({ error: 'week must match YYYY-Www' });
4803
+ }
4804
+ const file = path.join(os.homedir(), '.bolloon', 'reports', `${week}.md`);
4805
+ try {
4806
+ const content = await fs.readFile(file, 'utf-8');
4807
+ res.json({ week, content, length: content.length });
4808
+ } catch {
4809
+ res.status(404).json({ error: 'not found', week });
4810
+ }
4811
+ } catch (err: any) {
4812
+ res.status(500).json({ error: err.message });
4813
+ }
4814
+ });
4815
+
4816
+ // 阶段 C 护栏 5: auto-evolve baseline 管理 (无 UI, 仅 API)
4817
+ // GET /api/auto-evolve/baselines → 列出所有 baseline tag
4818
+ // GET /api/auto-evolve/baselines/:tag/diff → 看某 baseline 的 diff 摘要
4819
+ // POST /api/auto-evolve/rollback {tag} → 回滚到指定 baseline
4820
+ app.get('/api/auto-evolve/baselines', async (_req, res) => {
4821
+ try {
4822
+ const { execFile } = await import('child_process');
4823
+ const { promisify } = await import('util');
4824
+ const pExec = promisify(execFile);
4825
+ const { stdout } = await pExec('git', [
4826
+ 'tag', '-l', 'auto-evolve-baseline-*', '--format=%(refname:short)|%(contents)|%(objectname:short)|%(taggerdate:iso)',
4827
+ ], { cwd: process.cwd() });
4828
+ const tags = stdout.trim().split('\n').filter(Boolean).map((line) => {
4829
+ const [tag, msg, sha, date] = line.split('|');
4830
+ return { tag, message: msg || '', sha, date };
4831
+ });
4832
+ res.json({ tags, count: tags.length });
4833
+ } catch (err: any) {
4834
+ res.status(500).json({ error: err.message });
4835
+ }
4836
+ });
4837
+
4838
+ app.get('/api/auto-evolve/baselines/:tag/diff', async (req, res) => {
4839
+ try {
4840
+ const { execFile } = await import('child_process');
4841
+ const { promisify } = await import('util');
4842
+ const pExec = promisify(execFile);
4843
+ const tag = req.params.tag;
4844
+ if (!/^auto-evolve-baseline-[\w-]+$/.test(tag)) {
4845
+ return res.status(400).json({ error: 'tag must match auto-evolve-baseline-*' });
4846
+ }
4847
+ const { stdout } = await pExec('git', ['show', '--stat', '--no-color', tag], { cwd: process.cwd() });
4848
+ res.json({ tag, diff: stdout.slice(0, 5000) }); // 限长 5KB
4849
+ } catch (err: any) {
4850
+ res.status(500).json({ error: err.message });
4851
+ }
4852
+ });
4853
+
4854
+ // Bootstrap Context → 拼好的 system prompt 片段 (供调试看注入效果)
4855
+ app.get('/api/bolloon/context/system-prompt', async (req, res) => {
4856
+ try {
4857
+ const { getCachedBolloonContext } = await import(
4858
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4859
+ );
4860
+ const { formatContextForSystemPrompt } = await import(
4861
+ '../bootstrap/project-context.js'
4862
+ );
4863
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() });
4864
+ const systemAddition = formatContextForSystemPrompt(ctx, {
4865
+ maxChars: parseInt(String(req.query.max ?? '4000'), 10) || 4000,
4866
+ });
4867
+ res.json({ systemAddition, length: systemAddition.length, truncated: systemAddition.includes('截断模式') });
4868
+ } catch (err: any) {
4869
+ console.error('[bolloon] context/system-prompt failed:', err);
4870
+ res.status(500).json({ error: err.message });
4871
+ }
4872
+ });
4873
+
4874
+ // 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
4875
+ // body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
4876
+ // query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
4877
+ // 缺省 → 用户在 UI 手动触发, 不查开关 (避免阻塞用户)
4878
+ app.post('/api/judgments/adaptive-apply', async (req, res) => {
4879
+ try {
4880
+ const isAuto = req.query.auto === '1' || req.query.auto === 'true';
4881
+ const { action, suggestion, appliedPatch } = req.body as {
4882
+ action: 'accept' | 'reject' | 'revert';
4883
+ suggestion: { judgmentId: string; kind: string; decision: string; reason: string; action: string; metrics: unknown; scannedAt: string; key: string };
4884
+ appliedPatch?: Record<string, unknown>;
4885
+ };
4886
+ if (!action || !suggestion?.judgmentId) {
4887
+ return res.status(400).json({ error: 'action and suggestion.judgmentId required' });
4888
+ }
4889
+ const { updateJudgmentStatus } = await import(
4890
+ '../pi-ecosystem-judgment/human-value-store.js'
4891
+ );
4892
+ const { logEvolution } = await import(
4893
+ '../pi-ecosystem-judgment/adaptive-scan.js'
4894
+ );
4895
+ // accept 时: 真正改库
4896
+ if (action === 'accept') {
4897
+ // 阶段 A: 自动路径需先过 auto-evolve-policy 网关
4898
+ if (isAuto) {
4899
+ const { requireDataLayerAutoEvolve } = await import(
4900
+ '../utils/auto-evolve-policy.js'
4901
+ );
4902
+ try {
4903
+ await requireDataLayerAutoEvolve('adaptive-apply.auto.deprecate');
4904
+ } catch (err: any) {
4905
+ return res.status(423).json({
4906
+ error: 'data-layer-auto-evolve-disabled',
4907
+ message: err.message,
4908
+ hint: '设 BOLLOON_AUTO_EVOLVE_DATA=1 或在 self-improve-policy.json 加 dataLayerAutoEvolve: true',
4909
+ });
4910
+ }
4911
+ }
4912
+ if (suggestion.action === 'deprecate') {
4913
+ // 标记 superseded (语义: 不再用, 但保留可回滚)
4914
+ await updateJudgmentStatus(suggestion.judgmentId, 'superseded', {
4915
+ evolutionReason: 'merged', // 借 merged 字段表达"被自适应废弃"
4916
+ });
4917
+ } else if (suggestion.action === 'boost') {
4918
+ // boost: 用户手动接受后, 不改库本身 (weight 在 getRelevantValues 里动态算),
4919
+ // 但写 evolution 留痕, 未来可以基于此调整算法
4920
+ // 当前不直接改库, 仅留痕
4921
+ }
4922
+ // 'review' 类不需要自动改库, 仅 log 接受
4923
+ }
4924
+ await logEvolution({
4925
+ ts: new Date().toISOString(),
4926
+ action,
4927
+ suggestion: suggestion as any,
4928
+ appliedPatch,
4929
+ });
4930
+ res.json({ ok: true });
4931
+ } catch (err: any) {
4932
+ console.error('[judgments] adaptive-apply failed:', err);
4933
+ res.status(500).json({ error: err.message });
4934
+ }
4935
+ });
4936
+
4937
+ // 演化日志 (audit / 一键回滚源)
4938
+ app.get('/api/judgments/evolution-log', async (req, res) => {
4939
+ try {
4940
+ const { readEvolutionLog } = await import(
4941
+ '../pi-ecosystem-judgment/adaptive-scan.js'
4942
+ );
4943
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '50'), 10) || 50, 1), 200);
4944
+ const items = await readEvolutionLog(limit);
4945
+ res.json({ count: items.length, items });
4946
+ } catch (err: any) {
4947
+ console.error('[judgments] evolution-log failed:', err);
4948
+ res.status(500).json({ error: err.message });
4949
+ }
4950
+ });
4951
+
4952
+ // 阶段 2: Causal-judge 4 个 endpoint
4953
+ app.get('/api/judgments/causal/correlation', async (req, res) => {
4954
+ try {
4955
+ const { runCorrelationAnalysis } = await import(
4956
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4957
+ );
4958
+ const topN = Math.min(Math.max(parseInt(String(req.query.topN ?? '5'), 10) || 5, 1), 50);
4959
+ const useLLM = String(req.query.useLLM ?? '1') !== '0';
4960
+ const items = await runCorrelationAnalysis({ topN, useLLM });
4961
+ res.json({ count: items.length, items });
4962
+ } catch (err: any) {
4963
+ console.error('[causal] correlation failed:', err);
4964
+ res.status(500).json({ error: err.message });
4965
+ }
4966
+ });
4967
+
4968
+ app.get('/api/judgments/causal/intervention', async (req, res) => {
4969
+ try {
4970
+ const { runIntervention } = await import(
4971
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4972
+ );
4973
+ const { judgmentId, scenario } = req.query as { judgmentId?: string; scenario?: string };
4974
+ if (!judgmentId) return res.status(400).json({ error: 'judgmentId required' });
4975
+ const result = await runIntervention(judgmentId, { scenarioContext: scenario });
4976
+ res.json(result);
4977
+ } catch (err: any) {
4978
+ console.error('[causal] intervention failed:', err);
4979
+ res.status(500).json({ error: err.message });
4980
+ }
4981
+ });
4982
+
4983
+ app.post('/api/judgments/causal/counterfactual', async (req, res) => {
4984
+ try {
4985
+ const { runCounterfactualAudit } = await import(
4986
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
4987
+ );
4988
+ const { userInput, aiReply, violatedPrinciples } = req.body as {
4989
+ userInput?: string;
4990
+ aiReply?: string;
4991
+ violatedPrinciples?: Array<{ principle: string; reason: string }>;
4992
+ };
4993
+ if (!userInput || !aiReply) {
4994
+ return res.status(400).json({ error: 'userInput and aiReply required' });
4995
+ }
4996
+ const audit = await runCounterfactualAudit({
4997
+ userInput,
4998
+ aiReply,
4999
+ violatedPrinciples: violatedPrinciples ?? [],
5000
+ });
5001
+ res.json(audit);
5002
+ } catch (err: any) {
5003
+ console.error('[causal] counterfactual failed:', err);
5004
+ res.status(500).json({ error: err.message });
5005
+ }
5006
+ });
5007
+
5008
+ app.get('/api/judgments/causal/audit-log', async (req, res) => {
5009
+ try {
5010
+ const { readCounterfactualLog } = await import(
5011
+ '../pi-ecosystem-judgment/human-value-pipeline.js'
5012
+ );
5013
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
5014
+ const items = await readCounterfactualLog(limit);
5015
+ res.json({ count: items.length, items });
5016
+ } catch (err: any) {
5017
+ console.error('[causal] audit-log failed:', err);
5018
+ res.status(500).json({ error: err.message });
5019
+ }
5020
+ });
5021
+
4436
5022
  // 导入判断: 接受 { filename, content (base64), context }.
4437
5023
  // 支持 .json / .yaml / .yml / .md / .txt / .html. 完全离线解析, 不调 LLM.
4438
5024
  // 解析规则: