@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
@@ -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 { createHyperswarmCommunicator, createTopic, KeyManager, AgentAuthManager, } from '@diap/sdk';
9
10
  import { documentReader } from '../documents/reader.js';
10
11
  import { initMinimax, getMinimax } from '../constraints/index.js';
@@ -657,7 +658,13 @@ async function handleV3P2PMessage(parsed, conn, comm) {
657
658
  let fullResponse = '';
658
659
  // v3 新增: 流式 token 节流推给 B — 让 B 看到过程
659
660
  let lastFlushAt = 0;
661
+ let usedJudgmentIds = [];
660
662
  const streamCallback = (event) => {
663
+ // P0.5: 注入门回传
664
+ if (event?.type === 'used_judgments' && Array.isArray(event.usedIds)) {
665
+ usedJudgmentIds = event.usedIds;
666
+ return;
667
+ }
661
668
  if (event.type === 'token') {
662
669
  fullResponse += event.content;
663
670
  if (fullResponse.length - lastFlushAt >= 20) {
@@ -671,7 +678,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
671
678
  }
672
679
  };
673
680
  const agent = await getAgentForChannel(channelId, ch.did || '', ch.name, ch.didDocRef);
674
- fullResponse = await agent.promptStream(fullPrompt, streamCallback);
681
+ fullResponse = await agent.promptStream(fullPrompt, streamCallback, undefined, channelId);
675
682
  // v3 新增: 存 A 的 assistant 消息到 session — B 拉历史时能看到完整对话
676
683
  try {
677
684
  const existing = await loadSession(channelId, 'default');
@@ -682,6 +689,7 @@ async function handleV3P2PMessage(parsed, conn, comm) {
682
689
  id: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
683
690
  type: 'ai',
684
691
  content: fullResponse,
692
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
685
693
  timestamp: new Date().toISOString()
686
694
  });
687
695
  session.lastUpdated = new Date().toISOString();
@@ -967,6 +975,16 @@ export async function createWebServer(port = 3000, options = {}) {
967
975
  process.on('unhandledRejection', (reason, promise) => {
968
976
  console.error('[警告] 未处理的 Promise 拒绝:', reason);
969
977
  });
978
+ // Bolloon Bootstrap (幂等, 重复调不会重复挂定时器)
979
+ // 这里独立调一次以保证 CLI-only 模式 (无 index.ts 引导) 也能 bootstrap
980
+ try {
981
+ const { bootstrapBolloon } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
982
+ const bs = await bootstrapBolloon({ cwd: process.cwd() });
983
+ console.log(`[createWebServer] bootstrap 完成 (${bs.durationMs}ms)`);
984
+ }
985
+ catch (err) {
986
+ console.warn('[createWebServer] bootstrap 失败 (非致命):', err);
987
+ }
970
988
  // 重置旧的 agent session,确保使用新的 LLM 配置
971
989
  const { resetAgentSession } = await import('../agents/pi-sdk.js');
972
990
  resetAgentSession();
@@ -1410,10 +1428,40 @@ export async function createWebServer(port = 3000, options = {}) {
1410
1428
  const autoToolsEnabled = channel?.autoInvokeTools !== false; // 默认开启
1411
1429
  // 捕获外层 channel 到独立变量, 避免被 try 块内 (line 740+) 的 const channel 遮蔽
1412
1430
  const channelForJudgment = channel;
1431
+ // per-channel queue 检查: 已在跑就入队, 等当前跑完自动接上
1432
+ const runState = getOrCreateRunState(channelId);
1433
+ if (runState.running) {
1434
+ runState.queue.push({ channelId, text, boundWalletAddress, autoToolsEnabled });
1435
+ broadcastQueueUpdate(channelId);
1436
+ console.log(`[queue] /message 入队 channel=${channelId}, queue len=${runState.queue.length}`);
1437
+ return;
1438
+ }
1439
+ runState.running = true;
1440
+ runState.abortController = new AbortController();
1441
+ broadcastQueueUpdate(channelId);
1413
1442
  try {
1414
1443
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
1415
1444
  let fullResponse = '';
1445
+ // P0.5: 注入门回传的 usedIds, 落 session message metadata, UI 可查
1446
+ let usedJudgmentIds = [];
1416
1447
  const streamCallback = (event) => {
1448
+ // P0.5: 捕获注入门回传
1449
+ if (event.type === 'used_judgments' && Array.isArray(event.usedIds)) {
1450
+ usedJudgmentIds = event.usedIds;
1451
+ // 同步推给前端 (用于 finalizeTimelineAsMessage 时给 addMessage 传 usedIds)
1452
+ broadcast({ type: 'used_judgments', usedIds: usedJudgmentIds }, channelId);
1453
+ return;
1454
+ }
1455
+ // 阶段事件 (注入门 / D 触发)
1456
+ if (event.type === 'phase') {
1457
+ broadcast({
1458
+ type: 'phase',
1459
+ phase: event.phase,
1460
+ detail: event.detail,
1461
+ usedCount: event.usedCount,
1462
+ }, channelId);
1463
+ return;
1464
+ }
1417
1465
  // 同时发送给流式显示和工作流显示
1418
1466
  if (event.type === 'token' || event.type === 'thinking') {
1419
1467
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
@@ -1583,7 +1631,22 @@ export async function createWebServer(port = 3000, options = {}) {
1583
1631
  }
1584
1632
  if (contextHint)
1585
1633
  contextHint += '\n';
1586
- fullResponse = await agent.promptStream(contextHint + text, streamCallback);
1634
+ try {
1635
+ fullResponse = await agent.promptStream(contextHint + text, streamCallback, runState.abortController?.signal, channelId);
1636
+ }
1637
+ catch (err) {
1638
+ // abort 抛错: 保留已输出的部分 (fullResponse 可能是空字符串)
1639
+ if (runState.abortController?.signal.aborted || err?.name === 'AbortError') {
1640
+ console.log(`[chat] aborted channel=${channelId}`);
1641
+ }
1642
+ else {
1643
+ throw err;
1644
+ }
1645
+ }
1646
+ // abort 模式: 给 partial 拼后缀
1647
+ if (runState.abortController?.signal.aborted && fullResponse.trim().length > 0) {
1648
+ fullResponse = fullResponse + '\n\n_[生成已中断]_';
1649
+ }
1587
1650
  // v3 新增: 解析 LLM 回复里的 @-mentions, 转发到目标 channel
1588
1651
  await routeMentionsInReply(channelId, fullResponse, localChannels, remoteChannels);
1589
1652
  broadcast({ type: 'ai', content: fullResponse }, channelId);
@@ -1592,7 +1655,15 @@ export async function createWebServer(port = 3000, options = {}) {
1592
1655
  session.sessionId = currentSessionId;
1593
1656
  // v3: 加 source 标记 (local = 内部 owner, remote = 远端访客)
1594
1657
  session.messages.push({ id: crypto.randomUUID(), type: 'user', content: text, timestamp: new Date().toISOString(), source: 'local' });
1595
- session.messages.push({ id: crypto.randomUUID(), type: 'ai', content: fullResponse, timestamp: new Date().toISOString(), source: 'local' });
1658
+ session.messages.push({
1659
+ id: crypto.randomUUID(),
1660
+ type: 'ai',
1661
+ content: fullResponse,
1662
+ timestamp: new Date().toISOString(),
1663
+ source: 'local',
1664
+ // P0.5: 这条 AI 回复引用了哪些 judgment (注入门回传)
1665
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
1666
+ });
1596
1667
  session.lastUpdated = new Date().toISOString();
1597
1668
  await saveSession(session);
1598
1669
  const channels = await loadChannels();
@@ -1605,6 +1676,46 @@ export async function createWebServer(port = 3000, options = {}) {
1605
1676
  await saveChannels(channels);
1606
1677
  }
1607
1678
  broadcast({ type: 'done' }, channelId);
1679
+ // D 触发: AI 被动捕获判断力 (后台异步, 不阻塞主对话)
1680
+ setImmediate(() => {
1681
+ try {
1682
+ const lastTurns = session.messages.slice(-6).map((m) => ({
1683
+ role: (m.type === 'user' ? 'human' : 'agent'),
1684
+ content: m.content,
1685
+ }));
1686
+ if (lastTurns.length < 2)
1687
+ return;
1688
+ broadcast({ type: 'phase', phase: 'd_detect', detail: '监测对话...' }, channelId);
1689
+ import('../pi-ecosystem-judgment/human-value-pipeline.js')
1690
+ .then(async ({ detectAndDistillFromChannel, throttleDHook }) => {
1691
+ // channel 维度 5min 节流, 防对话卡顿时 LLM 反复触发
1692
+ if (!throttleDHook(channelId, 5 * 60_000)) {
1693
+ console.log(`[D-hook ${channelId}] throttled (within 5min)`);
1694
+ broadcast({ type: 'phase', phase: 'd_skip', detail: 'throttled' }, channelId);
1695
+ return null;
1696
+ }
1697
+ broadcast({ type: 'phase', phase: 'd_distill', detail: '蒸馏判断力...' }, channelId);
1698
+ return detectAndDistillFromChannel(lastTurns, { channelId });
1699
+ })
1700
+ .then((result) => {
1701
+ if (result && result.triggered) {
1702
+ console.log(`[D-hook ${channelId}] stored: ${result.reason}`, result.evolved);
1703
+ broadcast({ type: 'phase', phase: 'd_done', detail: result.reason }, channelId);
1704
+ }
1705
+ else if (result && result.reason) {
1706
+ console.log(`[D-hook ${channelId}] skipped: ${result.reason}`);
1707
+ broadcast({ type: 'phase', phase: 'd_skip', detail: result.reason }, channelId);
1708
+ }
1709
+ })
1710
+ .catch((err) => {
1711
+ console.warn(`[D-hook ${channelId}] failed:`, err);
1712
+ broadcast({ type: 'phase', phase: 'd_error', detail: String(err) }, channelId);
1713
+ });
1714
+ }
1715
+ catch (err) {
1716
+ console.warn(`[D-hook ${channelId}] sync error:`, err);
1717
+ }
1718
+ });
1608
1719
  // 2026-06-11: 202 已发的话, 不要重复 res.json (会抛 ERR_HTTP_HEADERS_SENT)
1609
1720
  if (!res.headersSent)
1610
1721
  res.json({ ok: true });
@@ -1615,6 +1726,15 @@ export async function createWebServer(port = 3000, options = {}) {
1615
1726
  if (!res.headersSent)
1616
1727
  res.status(500).json({ error: err.message });
1617
1728
  }
1729
+ finally {
1730
+ // queue dequeue: 跑完或失败都要清状态
1731
+ // 当前实现: 自动接下一条需要把 ~200 行 try 块抽函数, 暂不抽.
1732
+ // 替代: 用户点 [队列 +N] 按钮时, 客户端发起一个特殊的 HTTP 请求触发下一条
1733
+ // (在 client.js 实现). 这里只清状态 + 广播.
1734
+ runState.running = false;
1735
+ runState.abortController = null;
1736
+ broadcastQueueUpdate(channelId);
1737
+ }
1618
1738
  });
1619
1739
  // ---------- 频道元数据后台修复队列 ----------
1620
1740
  // 关键点: 旧实现会在每次 GET /channels 时同步执行 KeyManager.generate() + IPFS POST,
@@ -1623,6 +1743,24 @@ export async function createWebServer(port = 3000, options = {}) {
1623
1743
  const didFixQueue = new Set(); // 待修复的 channelId
1624
1744
  let didFixRunning = false;
1625
1745
  let didFixTimer = null;
1746
+ const channelRunState = new Map();
1747
+ function getOrCreateRunState(channelId) {
1748
+ let s = channelRunState.get(channelId);
1749
+ if (!s) {
1750
+ s = { running: false, queue: [], abortController: null };
1751
+ channelRunState.set(channelId, s);
1752
+ }
1753
+ return s;
1754
+ }
1755
+ function broadcastQueueUpdate(channelId) {
1756
+ const s = channelRunState.get(channelId);
1757
+ const queueLength = s ? s.queue.length : 0;
1758
+ const running = s ? s.running : false;
1759
+ try {
1760
+ broadcast({ type: 'queue_update', channelId, queueLength, running }, channelId);
1761
+ }
1762
+ catch { /* */ }
1763
+ }
1626
1764
  function scheduleDidFix(channelId) {
1627
1765
  didFixQueue.add(channelId);
1628
1766
  if (didFixTimer)
@@ -2312,7 +2450,13 @@ export async function createWebServer(port = 3000, options = {}) {
2312
2450
  broadcast({ type: 'regenerating', channelId }, channelId);
2313
2451
  const agent = await getAgentForChannel(channelId, realChannelDid, realChannelName, realChannelDidDoc);
2314
2452
  let fullResponse = '';
2453
+ let usedJudgmentIds = [];
2315
2454
  const streamCallback = (event) => {
2455
+ // P0.5: 注入门回传
2456
+ if (event.type === 'used_judgments' && Array.isArray(event.usedIds)) {
2457
+ usedJudgmentIds = event.usedIds;
2458
+ return;
2459
+ }
2316
2460
  if (event.type === 'token' || event.type === 'thinking') {
2317
2461
  broadcast({ type: 'stream', streamType: event.type, content: event.content }, channelId);
2318
2462
  }
@@ -2325,7 +2469,7 @@ export async function createWebServer(port = 3000, options = {}) {
2325
2469
  };
2326
2470
  // 重新生成时只发送用户消息 (v3: 同时注入 channel 绑定的判断力)
2327
2471
  const regenHint = await buildJudgmentHint(channel, channelId);
2328
- fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback);
2472
+ fullResponse = await agent.promptStream(regenHint + userMessage, streamCallback, undefined, channelId);
2329
2473
  broadcast({ type: 'ai', content: fullResponse }, channelId);
2330
2474
  // 更新 session
2331
2475
  const existingSession = await loadSession(channelId, currentSessionId);
@@ -2339,7 +2483,8 @@ export async function createWebServer(port = 3000, options = {}) {
2339
2483
  id: crypto.randomUUID(),
2340
2484
  type: 'ai',
2341
2485
  content: fullResponse,
2342
- timestamp: new Date().toISOString()
2486
+ timestamp: new Date().toISOString(),
2487
+ ...(usedJudgmentIds.length > 0 ? { metadata: { usedJudgmentIds } } : {}),
2343
2488
  });
2344
2489
  existingSession.lastUpdated = new Date().toISOString();
2345
2490
  await saveSession(existingSession);
@@ -3664,6 +3809,24 @@ export async function createWebServer(port = 3000, options = {}) {
3664
3809
  res.status(500).json({ error: err.message });
3665
3810
  }
3666
3811
  });
3812
+ // 终止当前 channel 的 LLM 流 (UI 终止按钮)
3813
+ app.post('/api/chat/abort', async (req, res) => {
3814
+ try {
3815
+ const { channelId } = req.body;
3816
+ if (!channelId)
3817
+ return res.status(400).json({ error: 'channelId required' });
3818
+ const s = channelRunState.get(channelId);
3819
+ if (s?.abortController) {
3820
+ s.abortController.abort();
3821
+ console.log(`[abort] user aborted channel=${channelId}`);
3822
+ return res.json({ ok: true, aborted: true });
3823
+ }
3824
+ res.json({ ok: true, aborted: false });
3825
+ }
3826
+ catch (err) {
3827
+ res.status(500).json({ error: err.message });
3828
+ }
3829
+ });
3667
3830
  // 主人审阅: 批准 draft
3668
3831
  app.post('/api/chat/approve', async (req, res) => {
3669
3832
  try {
@@ -4046,20 +4209,378 @@ export async function createWebServer(port = 3000, options = {}) {
4046
4209
  res.status(500).json({ error: err.message });
4047
4210
  }
4048
4211
  });
4049
- app.get('/api/judgments', async (_req, res) => {
4212
+ app.get('/api/judgments', async (req, res) => {
4050
4213
  try {
4051
- const { loadAllJudgments, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
4214
+ const { listJudgmentsByStatus, initializeValueStore } = await import('../pi-ecosystem-judgment/human-value-store.js');
4052
4215
  await initializeValueStore();
4053
- const all = await loadAllJudgments();
4054
- // 新的在前
4216
+ const status = (typeof req.query.status === 'string' ? req.query.status : 'all');
4217
+ const all = await listJudgmentsByStatus(status);
4055
4218
  all.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));
4056
- res.json({ count: all.length, judgments: all });
4219
+ res.json({ count: all.length, status, judgments: all });
4057
4220
  }
4058
4221
  catch (err) {
4059
4222
  console.error('[judgments] GET failed:', err);
4060
4223
  res.status(500).json({ error: err.message });
4061
4224
  }
4062
4225
  });
4226
+ // 蒸馏 B 触发 (人类点按钮) — 同步执行演化对齐
4227
+ app.post('/api/judgments/distill-from-conversation', async (req, res) => {
4228
+ try {
4229
+ const { channelId, messageId, recentTurns } = req.body;
4230
+ if (!channelId) {
4231
+ return res.status(400).json({ error: 'channelId required' });
4232
+ }
4233
+ // 取 channel 最近的对话
4234
+ const channels = await loadChannels();
4235
+ const channel = channels.find((c) => c.id === channelId);
4236
+ if (!channel)
4237
+ return res.status(404).json({ error: 'channel not found' });
4238
+ const currentSessionId = channel.currentSessionId;
4239
+ if (!currentSessionId) {
4240
+ return res.status(400).json({ error: 'no active session in channel' });
4241
+ }
4242
+ const session = await loadSession(channelId, currentSessionId);
4243
+ if (!session)
4244
+ return res.status(404).json({ error: 'session not found' });
4245
+ // 取最近 N 轮 (默认 10), 转成 DistillTurn 格式
4246
+ const limit = Math.min(Math.max(recentTurns ?? 10, 2), 30);
4247
+ const turns = session.messages.slice(-limit).map((m) => ({
4248
+ role: (m.type === 'user' ? 'human' : 'agent'),
4249
+ content: m.content,
4250
+ }));
4251
+ const { distillAndStoreFromChannel } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4252
+ const result = await distillAndStoreFromChannel(turns, { channelId });
4253
+ res.json({
4254
+ ok: true,
4255
+ triggered: result.triggered,
4256
+ reason: result.reason,
4257
+ judgment: result.judgment,
4258
+ evolved: result.evolved,
4259
+ });
4260
+ }
4261
+ catch (err) {
4262
+ console.error('[judgments] distill-from-conversation failed:', err);
4263
+ res.status(500).json({ error: err.message });
4264
+ }
4265
+ });
4266
+ // 蒸馏 D 触发 (AI 被动) — 后台异步,不阻塞 HTTP 响应
4267
+ app.post('/api/judgments/detect-and-distill', async (req, res) => {
4268
+ try {
4269
+ const { channelId, turns } = req.body;
4270
+ // 先立即返回 202, 不等 LLM
4271
+ res.status(202).json({ ok: true, queued: true });
4272
+ if (!channelId || !Array.isArray(turns) || turns.length === 0) {
4273
+ return;
4274
+ }
4275
+ // 异步处理 (不 await, 不阻塞响应)
4276
+ setImmediate(async () => {
4277
+ try {
4278
+ const { detectAndDistillFromChannel } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4279
+ const result = await detectAndDistillFromChannel(turns, { channelId });
4280
+ if (result.triggered) {
4281
+ console.log(`[D-hook] ${channelId}: ${result.reason}`, result.evolved);
4282
+ }
4283
+ }
4284
+ catch (err) {
4285
+ console.warn('[D-hook] background failed:', err);
4286
+ }
4287
+ });
4288
+ }
4289
+ catch (err) {
4290
+ console.error('[judgments] detect-and-distill failed:', err);
4291
+ res.status(500).json({ error: err.message });
4292
+ }
4293
+ });
4294
+ // 判断力使用回溯 (P0.5): 给定 judgmentIds, 反查对应的 decision 文本
4295
+ // 用途: UI 上"这条 AI 回复引用了哪些原则"
4296
+ app.post('/api/judgments/resolve-usage', async (req, res) => {
4297
+ try {
4298
+ const { ids } = req.body;
4299
+ if (!Array.isArray(ids) || ids.length === 0) {
4300
+ return res.json({ items: [] });
4301
+ }
4302
+ const { loadAllJudgments } = await import('../pi-ecosystem-judgment/human-value-store.js');
4303
+ const all = await loadAllJudgments();
4304
+ const byId = new Map(all.map((j) => [j.id, j]));
4305
+ const items = ids
4306
+ .map((id) => byId.get(id))
4307
+ .filter((j) => Boolean(j))
4308
+ .map((j) => ({
4309
+ id: j.id,
4310
+ decision: j.decision,
4311
+ status: j.status ?? 'active',
4312
+ timestamp: j.timestamp,
4313
+ }));
4314
+ res.json({ items });
4315
+ }
4316
+ catch (err) {
4317
+ console.error('[judgments] resolve-usage failed:', err);
4318
+ res.status(500).json({ error: err.message });
4319
+ }
4320
+ });
4321
+ // 判断力违规日志 (P3 UI): 读 violations.jsonl
4322
+ app.get('/api/judgments/violations', async (req, res) => {
4323
+ try {
4324
+ const { getRecentViolations } = await import('../pi-ecosystem-judgment/monitor-gate.js');
4325
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
4326
+ const items = await getRecentViolations(limit);
4327
+ res.json({ count: items.length, items });
4328
+ }
4329
+ catch (err) {
4330
+ console.error('[judgments] violations failed:', err);
4331
+ res.status(500).json({ error: err.message });
4332
+ }
4333
+ });
4334
+ // 类 B 自适应扫描: 读 judgments.json + usage.jsonl, 给出 stale/rising/unused 建议
4335
+ // ?force=1 跳过 24h 缓存
4336
+ app.get('/api/judgments/adaptive-suggestions', async (req, res) => {
4337
+ try {
4338
+ const { getCachedScan } = await import('../pi-ecosystem-judgment/adaptive-scan.js');
4339
+ const force = String(req.query.force ?? '') === '1';
4340
+ const result = await getCachedScan(force);
4341
+ res.json(result);
4342
+ }
4343
+ catch (err) {
4344
+ console.error('[judgments] adaptive-scan failed:', err);
4345
+ res.status(500).json({ error: err.message });
4346
+ }
4347
+ });
4348
+ // Bootstrap Context 调试视图: 返出完整 BolloonContext
4349
+ app.get('/api/bolloon/context', async (req, res) => {
4350
+ try {
4351
+ const { getCachedBolloonContext } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4352
+ const force = String(req.query.force ?? '') === '1';
4353
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() }, force);
4354
+ res.json(ctx);
4355
+ }
4356
+ catch (err) {
4357
+ console.error('[bolloon] context failed:', err);
4358
+ res.status(500).json({ error: err.message });
4359
+ }
4360
+ });
4361
+ // 阶段 B: 周报 (weekly-report.ts 产物) — 仅 API 读取, 不做 UI tab
4362
+ // GET /api/reports → { files: ['2026-W24.md', ...] }
4363
+ // GET /api/reports/2026-W24 → { week, content }
4364
+ app.get('/api/reports', async (_req, res) => {
4365
+ try {
4366
+ const dir = path.join(os.homedir(), '.bolloon', 'reports');
4367
+ try {
4368
+ const entries = await fs.readdir(dir);
4369
+ const files = entries
4370
+ .filter((f) => f.endsWith('.md'))
4371
+ .sort()
4372
+ .reverse(); // 新的在前
4373
+ res.json({ dir, files });
4374
+ }
4375
+ catch {
4376
+ res.json({ dir, files: [] });
4377
+ }
4378
+ }
4379
+ catch (err) {
4380
+ res.status(500).json({ error: err.message });
4381
+ }
4382
+ });
4383
+ app.get('/api/reports/:week', async (req, res) => {
4384
+ try {
4385
+ const week = req.params.week;
4386
+ // 严格校验, 防路径穿越
4387
+ if (!/^\d{4}-W\d{1,2}$/.test(week)) {
4388
+ return res.status(400).json({ error: 'week must match YYYY-Www' });
4389
+ }
4390
+ const file = path.join(os.homedir(), '.bolloon', 'reports', `${week}.md`);
4391
+ try {
4392
+ const content = await fs.readFile(file, 'utf-8');
4393
+ res.json({ week, content, length: content.length });
4394
+ }
4395
+ catch {
4396
+ res.status(404).json({ error: 'not found', week });
4397
+ }
4398
+ }
4399
+ catch (err) {
4400
+ res.status(500).json({ error: err.message });
4401
+ }
4402
+ });
4403
+ // 阶段 C 护栏 5: auto-evolve baseline 管理 (无 UI, 仅 API)
4404
+ // GET /api/auto-evolve/baselines → 列出所有 baseline tag
4405
+ // GET /api/auto-evolve/baselines/:tag/diff → 看某 baseline 的 diff 摘要
4406
+ // POST /api/auto-evolve/rollback {tag} → 回滚到指定 baseline
4407
+ app.get('/api/auto-evolve/baselines', async (_req, res) => {
4408
+ try {
4409
+ const { execFile } = await import('child_process');
4410
+ const { promisify } = await import('util');
4411
+ const pExec = promisify(execFile);
4412
+ const { stdout } = await pExec('git', [
4413
+ 'tag', '-l', 'auto-evolve-baseline-*', '--format=%(refname:short)|%(contents)|%(objectname:short)|%(taggerdate:iso)',
4414
+ ], { cwd: process.cwd() });
4415
+ const tags = stdout.trim().split('\n').filter(Boolean).map((line) => {
4416
+ const [tag, msg, sha, date] = line.split('|');
4417
+ return { tag, message: msg || '', sha, date };
4418
+ });
4419
+ res.json({ tags, count: tags.length });
4420
+ }
4421
+ catch (err) {
4422
+ res.status(500).json({ error: err.message });
4423
+ }
4424
+ });
4425
+ app.get('/api/auto-evolve/baselines/:tag/diff', async (req, res) => {
4426
+ try {
4427
+ const { execFile } = await import('child_process');
4428
+ const { promisify } = await import('util');
4429
+ const pExec = promisify(execFile);
4430
+ const tag = req.params.tag;
4431
+ if (!/^auto-evolve-baseline-[\w-]+$/.test(tag)) {
4432
+ return res.status(400).json({ error: 'tag must match auto-evolve-baseline-*' });
4433
+ }
4434
+ const { stdout } = await pExec('git', ['show', '--stat', '--no-color', tag], { cwd: process.cwd() });
4435
+ res.json({ tag, diff: stdout.slice(0, 5000) }); // 限长 5KB
4436
+ }
4437
+ catch (err) {
4438
+ res.status(500).json({ error: err.message });
4439
+ }
4440
+ });
4441
+ // Bootstrap Context → 拼好的 system prompt 片段 (供调试看注入效果)
4442
+ app.get('/api/bolloon/context/system-prompt', async (req, res) => {
4443
+ try {
4444
+ const { getCachedBolloonContext } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4445
+ const { formatContextForSystemPrompt } = await import('../bootstrap/project-context.js');
4446
+ const ctx = await getCachedBolloonContext({ cwd: process.cwd() });
4447
+ const systemAddition = formatContextForSystemPrompt(ctx, {
4448
+ maxChars: parseInt(String(req.query.max ?? '4000'), 10) || 4000,
4449
+ });
4450
+ res.json({ systemAddition, length: systemAddition.length, truncated: systemAddition.includes('截断模式') });
4451
+ }
4452
+ catch (err) {
4453
+ console.error('[bolloon] context/system-prompt failed:', err);
4454
+ res.status(500).json({ error: err.message });
4455
+ }
4456
+ });
4457
+ // 自适应接受/拒绝: 写 evolution.jsonl 留痕, 接受时同时 patch judgments.json
4458
+ // body: { action: 'accept'|'reject'|'revert', suggestion, appliedPatch? }
4459
+ // query: ?auto=1 → 类 B 自动路径, 受 auto-evolve-policy 网关保护
4460
+ // 缺省 → 用户在 UI 手动触发, 不查开关 (避免阻塞用户)
4461
+ app.post('/api/judgments/adaptive-apply', async (req, res) => {
4462
+ try {
4463
+ const isAuto = req.query.auto === '1' || req.query.auto === 'true';
4464
+ const { action, suggestion, appliedPatch } = req.body;
4465
+ if (!action || !suggestion?.judgmentId) {
4466
+ return res.status(400).json({ error: 'action and suggestion.judgmentId required' });
4467
+ }
4468
+ const { updateJudgmentStatus } = await import('../pi-ecosystem-judgment/human-value-store.js');
4469
+ const { logEvolution } = await import('../pi-ecosystem-judgment/adaptive-scan.js');
4470
+ // accept 时: 真正改库
4471
+ if (action === 'accept') {
4472
+ // 阶段 A: 自动路径需先过 auto-evolve-policy 网关
4473
+ if (isAuto) {
4474
+ const { requireDataLayerAutoEvolve } = await import('../utils/auto-evolve-policy.js');
4475
+ try {
4476
+ await requireDataLayerAutoEvolve('adaptive-apply.auto.deprecate');
4477
+ }
4478
+ catch (err) {
4479
+ return res.status(423).json({
4480
+ error: 'data-layer-auto-evolve-disabled',
4481
+ message: err.message,
4482
+ hint: '设 BOLLOON_AUTO_EVOLVE_DATA=1 或在 self-improve-policy.json 加 dataLayerAutoEvolve: true',
4483
+ });
4484
+ }
4485
+ }
4486
+ if (suggestion.action === 'deprecate') {
4487
+ // 标记 superseded (语义: 不再用, 但保留可回滚)
4488
+ await updateJudgmentStatus(suggestion.judgmentId, 'superseded', {
4489
+ evolutionReason: 'merged', // 借 merged 字段表达"被自适应废弃"
4490
+ });
4491
+ }
4492
+ else if (suggestion.action === 'boost') {
4493
+ // boost: 用户手动接受后, 不改库本身 (weight 在 getRelevantValues 里动态算),
4494
+ // 但写 evolution 留痕, 未来可以基于此调整算法
4495
+ // 当前不直接改库, 仅留痕
4496
+ }
4497
+ // 'review' 类不需要自动改库, 仅 log 接受
4498
+ }
4499
+ await logEvolution({
4500
+ ts: new Date().toISOString(),
4501
+ action,
4502
+ suggestion: suggestion,
4503
+ appliedPatch,
4504
+ });
4505
+ res.json({ ok: true });
4506
+ }
4507
+ catch (err) {
4508
+ console.error('[judgments] adaptive-apply failed:', err);
4509
+ res.status(500).json({ error: err.message });
4510
+ }
4511
+ });
4512
+ // 演化日志 (audit / 一键回滚源)
4513
+ app.get('/api/judgments/evolution-log', async (req, res) => {
4514
+ try {
4515
+ const { readEvolutionLog } = await import('../pi-ecosystem-judgment/adaptive-scan.js');
4516
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '50'), 10) || 50, 1), 200);
4517
+ const items = await readEvolutionLog(limit);
4518
+ res.json({ count: items.length, items });
4519
+ }
4520
+ catch (err) {
4521
+ console.error('[judgments] evolution-log failed:', err);
4522
+ res.status(500).json({ error: err.message });
4523
+ }
4524
+ });
4525
+ // 阶段 2: Causal-judge 4 个 endpoint
4526
+ app.get('/api/judgments/causal/correlation', async (req, res) => {
4527
+ try {
4528
+ const { runCorrelationAnalysis } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4529
+ const topN = Math.min(Math.max(parseInt(String(req.query.topN ?? '5'), 10) || 5, 1), 50);
4530
+ const useLLM = String(req.query.useLLM ?? '1') !== '0';
4531
+ const items = await runCorrelationAnalysis({ topN, useLLM });
4532
+ res.json({ count: items.length, items });
4533
+ }
4534
+ catch (err) {
4535
+ console.error('[causal] correlation failed:', err);
4536
+ res.status(500).json({ error: err.message });
4537
+ }
4538
+ });
4539
+ app.get('/api/judgments/causal/intervention', async (req, res) => {
4540
+ try {
4541
+ const { runIntervention } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4542
+ const { judgmentId, scenario } = req.query;
4543
+ if (!judgmentId)
4544
+ return res.status(400).json({ error: 'judgmentId required' });
4545
+ const result = await runIntervention(judgmentId, { scenarioContext: scenario });
4546
+ res.json(result);
4547
+ }
4548
+ catch (err) {
4549
+ console.error('[causal] intervention failed:', err);
4550
+ res.status(500).json({ error: err.message });
4551
+ }
4552
+ });
4553
+ app.post('/api/judgments/causal/counterfactual', async (req, res) => {
4554
+ try {
4555
+ const { runCounterfactualAudit } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4556
+ const { userInput, aiReply, violatedPrinciples } = req.body;
4557
+ if (!userInput || !aiReply) {
4558
+ return res.status(400).json({ error: 'userInput and aiReply required' });
4559
+ }
4560
+ const audit = await runCounterfactualAudit({
4561
+ userInput,
4562
+ aiReply,
4563
+ violatedPrinciples: violatedPrinciples ?? [],
4564
+ });
4565
+ res.json(audit);
4566
+ }
4567
+ catch (err) {
4568
+ console.error('[causal] counterfactual failed:', err);
4569
+ res.status(500).json({ error: err.message });
4570
+ }
4571
+ });
4572
+ app.get('/api/judgments/causal/audit-log', async (req, res) => {
4573
+ try {
4574
+ const { readCounterfactualLog } = await import('../pi-ecosystem-judgment/human-value-pipeline.js');
4575
+ const limit = Math.min(Math.max(parseInt(String(req.query.limit ?? '20'), 10) || 20, 1), 200);
4576
+ const items = await readCounterfactualLog(limit);
4577
+ res.json({ count: items.length, items });
4578
+ }
4579
+ catch (err) {
4580
+ console.error('[causal] audit-log failed:', err);
4581
+ res.status(500).json({ error: err.message });
4582
+ }
4583
+ });
4063
4584
  // 导入判断: 接受 { filename, content (base64), context }.
4064
4585
  // 支持 .json / .yaml / .yml / .md / .txt / .html. 完全离线解析, 不调 LLM.
4065
4586
  // 解析规则: