@bolloon/bolloon-agent 0.1.33 → 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.
- package/.auto-evolve-calls +1 -0
- package/.last-auto-evolve-baseline +1 -0
- package/Bolloon.md +103 -0
- package/README.md +7 -2
- package/dist/agents/pi-sdk.js +264 -12
- package/dist/bollharness-integration/index.js +8 -1
- package/dist/bootstrap/bootstrap.js +114 -0
- package/dist/bootstrap/context-collector.js +296 -0
- package/dist/bootstrap/lifecycle-hooks.js +109 -0
- package/dist/bootstrap/project-context.js +151 -0
- package/dist/heartbeat/Watchdog.js +9 -1
- package/dist/index.js +11 -0
- package/dist/llm/pi-ai.js +31 -21
- package/dist/network/p2p-direct.js +59 -2
- package/dist/pi-ecosystem/index.js +9 -6
- package/dist/pi-ecosystem-judgment/adaptive-scan.js +231 -0
- package/dist/pi-ecosystem-judgment/causal-judge.js +449 -0
- package/dist/pi-ecosystem-judgment/decision.js +5 -2
- package/dist/pi-ecosystem-judgment/detect-hook.js +168 -0
- package/dist/pi-ecosystem-judgment/distill-prompt.js +226 -0
- package/dist/pi-ecosystem-judgment/evolve-judgment.js +170 -0
- package/dist/pi-ecosystem-judgment/human-value-pipeline.js +21 -0
- package/dist/pi-ecosystem-judgment/human-value-store.js +283 -22
- package/dist/pi-ecosystem-judgment/injection-gate.js +166 -0
- package/dist/pi-ecosystem-judgment/monitor-gate.js +188 -0
- package/dist/security/builtin-guards.js +124 -0
- package/dist/security/context-router-tool.js +106 -0
- package/dist/security/react-harness.js +143 -0
- package/dist/security/tool-gate.js +235 -0
- package/dist/social/heartbeat.js +19 -2
- package/dist/utils/auto-evolve-policy.js +117 -0
- package/dist/utils/clamp.js +7 -0
- package/dist/utils/double.js +6 -0
- package/dist/web/api-config.html +3 -3
- package/dist/web/client.js +1328 -351
- package/dist/web/index.html +34 -31
- package/dist/web/server.js +1128 -58
- package/dist/web/style.css +370 -0
- package/lefthook.yml +29 -0
- package/package.json +4 -2
- package/scripts/auto-evolve-loop.ts +376 -0
- package/scripts/auto-evolve-oneshot.sh +155 -0
- package/scripts/auto-evolve-snapshot.sh +136 -0
- package/scripts/detect-schema-changes.sh +48 -0
- package/scripts/diff-reviewer.ts +159 -0
- package/scripts/weekly-report.ts +364 -0
- package/src/agents/pi-sdk.ts +293 -15
- package/src/bollharness-integration/index.ts +8 -32
- package/src/bootstrap/bootstrap.ts +132 -0
- package/src/bootstrap/context-collector.ts +342 -0
- package/src/bootstrap/lifecycle-hooks.ts +176 -0
- package/src/bootstrap/project-context.ts +163 -0
- package/src/heartbeat/Watchdog.ts +9 -1
- package/src/index.ts +11 -0
- package/src/llm/pi-ai.ts +33 -22
- package/src/network/p2p-direct.ts +59 -3
- package/src/security/builtin-guards.ts +162 -0
- package/src/security/context-router-tool.ts +122 -0
- package/src/security/react-harness.ts +177 -0
- package/src/security/tool-gate.ts +294 -0
- package/src/social/ant-colony/index.js +19 -0
- package/src/social/heartbeat.ts +18 -2
- package/src/utils/auto-evolve-policy.ts +138 -0
- package/src/utils/clamp.ts +5 -0
- package/src/web/api-config.html +3 -3
- package/src/web/client.js +1328 -351
- package/src/web/index.html +34 -31
- package/src/web/server.ts +1179 -53
- package/src/web/style.css +370 -0
- package/staging/auto-evolve/clean-001/.review-verdict +9 -0
- package/staging/auto-evolve/clean-001/clean-001.patch +14 -0
- package/staging/auto-evolve/e2e-001/.patch-id +1 -0
- package/staging/auto-evolve/e2e-001/.review-verdict +12 -0
- package/staging/auto-evolve/e2e-001/e2e-001.patch +11 -0
- package/staging/auto-evolve/test-bad/.review-verdict +12 -0
- package/staging/auto-evolve/test-bad/test-bad.patch +11 -0
- package/src/social/ant-colony/AdaptiveHeartbeat.ts +0 -131
- package/src/social/ant-colony/PheromoneEngine.ts +0 -302
- package/src/social/ant-colony/index.ts +0 -18
- package/src/social/ant-colony/types.ts +0 -94
package/dist/llm/pi-ai.js
CHANGED
|
@@ -7,7 +7,7 @@ export class PiAIModel {
|
|
|
7
7
|
this.config = config;
|
|
8
8
|
this.provider = config.provider;
|
|
9
9
|
}
|
|
10
|
-
async chat(message, context) {
|
|
10
|
+
async chat(message, context, signal) {
|
|
11
11
|
const systemPrompt = this.buildSystemPrompt(context);
|
|
12
12
|
const messages = [
|
|
13
13
|
{ role: 'system', content: systemPrompt },
|
|
@@ -16,11 +16,16 @@ export class PiAIModel {
|
|
|
16
16
|
try {
|
|
17
17
|
const response = await this.generateText({
|
|
18
18
|
messages,
|
|
19
|
-
temperature: 0.8
|
|
19
|
+
temperature: 0.8,
|
|
20
|
+
signal,
|
|
20
21
|
});
|
|
21
22
|
return { reply: response };
|
|
22
23
|
}
|
|
23
24
|
catch (error) {
|
|
25
|
+
// abort 不当作错误, 透传一个 sentinel 让上层能识别
|
|
26
|
+
if (signal?.aborted || error?.name === 'AbortError') {
|
|
27
|
+
throw error; // 上层 try/catch 处理
|
|
28
|
+
}
|
|
24
29
|
console.error('PiAI chat error:', error);
|
|
25
30
|
return { reply: '抱歉,AI服务暂时不可用。' };
|
|
26
31
|
}
|
|
@@ -64,7 +69,7 @@ export class PiAIModel {
|
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
async generateText(options) {
|
|
67
|
-
const { messages, temperature = 0.7, maxTokens = 4096 } = options;
|
|
72
|
+
const { messages, temperature = 0.7, maxTokens = 4096, signal } = options;
|
|
68
73
|
switch (this.provider) {
|
|
69
74
|
case 'openai':
|
|
70
75
|
case 'minimax':
|
|
@@ -72,17 +77,17 @@ export class PiAIModel {
|
|
|
72
77
|
case 'kimi':
|
|
73
78
|
case 'glm':
|
|
74
79
|
case 'qwen':
|
|
75
|
-
return this.callOpenAI(messages, temperature, maxTokens);
|
|
80
|
+
return this.callOpenAI(messages, temperature, maxTokens, signal);
|
|
76
81
|
case 'anthropic':
|
|
77
|
-
return this.callAnthropic(messages, temperature, maxTokens);
|
|
82
|
+
return this.callAnthropic(messages, temperature, maxTokens, signal);
|
|
78
83
|
case 'ollama':
|
|
79
|
-
return this.callOllama(messages, temperature);
|
|
84
|
+
return this.callOllama(messages, temperature, signal);
|
|
80
85
|
case 'openrouter':
|
|
81
|
-
return this.callOpenRouter(messages, temperature, maxTokens);
|
|
86
|
+
return this.callOpenRouter(messages, temperature, maxTokens, signal);
|
|
82
87
|
case 'gemini':
|
|
83
|
-
return this.callGemini(messages, temperature, maxTokens);
|
|
88
|
+
return this.callGemini(messages, temperature, maxTokens, signal);
|
|
84
89
|
case 'local':
|
|
85
|
-
return this.callLocal(messages, temperature);
|
|
90
|
+
return this.callLocal(messages, temperature, signal);
|
|
86
91
|
default:
|
|
87
92
|
throw new Error(`Unsupported provider: ${this.provider}`);
|
|
88
93
|
}
|
|
@@ -142,7 +147,7 @@ export class PiAIModel {
|
|
|
142
147
|
};
|
|
143
148
|
return modelMap[this.provider];
|
|
144
149
|
}
|
|
145
|
-
async callOpenAI(messages, temperature, maxTokens) {
|
|
150
|
+
async callOpenAI(messages, temperature, maxTokens, signal) {
|
|
146
151
|
const apiKey = this.getApiKey();
|
|
147
152
|
if (!apiKey) {
|
|
148
153
|
throw new Error('OPENAI_API_KEY not set');
|
|
@@ -158,7 +163,8 @@ export class PiAIModel {
|
|
|
158
163
|
messages,
|
|
159
164
|
temperature,
|
|
160
165
|
max_tokens: maxTokens
|
|
161
|
-
})
|
|
166
|
+
}),
|
|
167
|
+
signal,
|
|
162
168
|
});
|
|
163
169
|
if (!response.ok) {
|
|
164
170
|
throw new Error(`OpenAI API error: ${response.status}`);
|
|
@@ -166,7 +172,7 @@ export class PiAIModel {
|
|
|
166
172
|
const data = await response.json();
|
|
167
173
|
return data.choices?.[0]?.message?.content || '';
|
|
168
174
|
}
|
|
169
|
-
async callAnthropic(messages, temperature, maxTokens) {
|
|
175
|
+
async callAnthropic(messages, temperature, maxTokens, signal) {
|
|
170
176
|
const apiKey = this.getApiKey();
|
|
171
177
|
if (!apiKey) {
|
|
172
178
|
throw new Error('ANTHROPIC_API_KEY not set');
|
|
@@ -187,7 +193,8 @@ export class PiAIModel {
|
|
|
187
193
|
system: systemMessage,
|
|
188
194
|
temperature,
|
|
189
195
|
max_tokens: maxTokens
|
|
190
|
-
})
|
|
196
|
+
}),
|
|
197
|
+
signal,
|
|
191
198
|
});
|
|
192
199
|
if (!response.ok) {
|
|
193
200
|
throw new Error(`Anthropic API error: ${response.status}`);
|
|
@@ -195,7 +202,7 @@ export class PiAIModel {
|
|
|
195
202
|
const data = await response.json();
|
|
196
203
|
return data.content?.[0]?.text || '';
|
|
197
204
|
}
|
|
198
|
-
async callOllama(messages, temperature) {
|
|
205
|
+
async callOllama(messages, temperature, signal) {
|
|
199
206
|
const response = await fetch(`${this.getBaseUrl()}/api/chat`, {
|
|
200
207
|
method: 'POST',
|
|
201
208
|
headers: {
|
|
@@ -206,7 +213,8 @@ export class PiAIModel {
|
|
|
206
213
|
messages,
|
|
207
214
|
temperature,
|
|
208
215
|
stream: false
|
|
209
|
-
})
|
|
216
|
+
}),
|
|
217
|
+
signal,
|
|
210
218
|
});
|
|
211
219
|
if (!response.ok) {
|
|
212
220
|
throw new Error(`Ollama API error: ${response.status}`);
|
|
@@ -214,7 +222,7 @@ export class PiAIModel {
|
|
|
214
222
|
const data = await response.json();
|
|
215
223
|
return data.message?.content || '';
|
|
216
224
|
}
|
|
217
|
-
async callOpenRouter(messages, temperature, maxTokens) {
|
|
225
|
+
async callOpenRouter(messages, temperature, maxTokens, signal) {
|
|
218
226
|
const apiKey = this.getApiKey();
|
|
219
227
|
if (!apiKey) {
|
|
220
228
|
throw new Error('OPENROUTER_API_KEY not set');
|
|
@@ -232,7 +240,8 @@ export class PiAIModel {
|
|
|
232
240
|
messages,
|
|
233
241
|
temperature,
|
|
234
242
|
max_tokens: maxTokens
|
|
235
|
-
})
|
|
243
|
+
}),
|
|
244
|
+
signal,
|
|
236
245
|
});
|
|
237
246
|
if (!response.ok) {
|
|
238
247
|
throw new Error(`OpenRouter API error: ${response.status}`);
|
|
@@ -240,7 +249,7 @@ export class PiAIModel {
|
|
|
240
249
|
const data = await response.json();
|
|
241
250
|
return data.choices?.[0]?.message?.content || '';
|
|
242
251
|
}
|
|
243
|
-
async callGemini(messages, temperature, maxTokens) {
|
|
252
|
+
async callGemini(messages, temperature, maxTokens, signal) {
|
|
244
253
|
const apiKey = this.getApiKey();
|
|
245
254
|
if (!apiKey) {
|
|
246
255
|
throw new Error('GEMINI_API_KEY not set');
|
|
@@ -264,7 +273,8 @@ export class PiAIModel {
|
|
|
264
273
|
temperature,
|
|
265
274
|
maxOutputTokens: maxTokens
|
|
266
275
|
}
|
|
267
|
-
})
|
|
276
|
+
}),
|
|
277
|
+
signal,
|
|
268
278
|
});
|
|
269
279
|
if (!response.ok) {
|
|
270
280
|
throw new Error(`Gemini API error: ${response.status}`);
|
|
@@ -272,8 +282,8 @@ export class PiAIModel {
|
|
|
272
282
|
const data = await response.json();
|
|
273
283
|
return data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
274
284
|
}
|
|
275
|
-
async callLocal(messages, temperature) {
|
|
276
|
-
return this.callOllama(messages, temperature);
|
|
285
|
+
async callLocal(messages, temperature, signal) {
|
|
286
|
+
return this.callOllama(messages, temperature, signal);
|
|
277
287
|
}
|
|
278
288
|
buildSystemPrompt(context) {
|
|
279
289
|
const envDetails = this.getEnvironmentDetails();
|
|
@@ -58,9 +58,9 @@ export class P2PDirect extends EventEmitter {
|
|
|
58
58
|
console.log(`[P2PDirect:${this.name}] 新连接: ${remotePubKeyHex.substring(0, 12)}... (inbound=${info.inbound || false}, type=${typeof conn}, hasWrite=${typeof conn?.write})`);
|
|
59
59
|
// 双向记录 (inbound + outbound 都能拿到)
|
|
60
60
|
this.conns.set(remotePubKeyHex, conn);
|
|
61
|
-
// v3: 触发 'connection' 事件, 上层 (web server) 可以主动给新连接发消息
|
|
62
|
-
this.emit('connection', { remotePublicKey: remotePubKeyHex, conn });
|
|
63
61
|
// 收到数据时 → 触发 'data' 事件
|
|
62
|
+
// 注意: data 监听器必须在 emit('connection') 之前注册,
|
|
63
|
+
// 否则 server 的 connection handler 发送消息后, 对端回复可能在 data 监听器就绪前到达
|
|
64
64
|
conn.on('data', (chunk) => {
|
|
65
65
|
const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
66
66
|
console.log(`[P2PDirect:${this.name}] 收到数据 from ${remotePubKeyHex.substring(0, 12)}... (${buf.length} bytes)`);
|
|
@@ -75,6 +75,9 @@ export class P2PDirect extends EventEmitter {
|
|
|
75
75
|
conn.on('close', () => {
|
|
76
76
|
this.conns.delete(remotePubKeyHex);
|
|
77
77
|
});
|
|
78
|
+
// v3: 触发 'connection' 事件, 上层 (web server) 可以主动给新连接发消息
|
|
79
|
+
// 注意: 放在 data/error/close 监听器之后, 确保 server 的 connection handler 不会先于 data 就绪
|
|
80
|
+
this.emit('connection', { remotePublicKey: remotePubKeyHex, conn });
|
|
78
81
|
});
|
|
79
82
|
await this.swarm.listen(); // server 模式
|
|
80
83
|
this.started = true;
|
|
@@ -127,6 +130,60 @@ export class P2PDirect extends EventEmitter {
|
|
|
127
130
|
return false;
|
|
128
131
|
}
|
|
129
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* 2026-06-10: 真"主动发, 等握手完成"版本 — 修复好友申请 fire-and-forget bug.
|
|
135
|
+
*
|
|
136
|
+
* 之前的问题: server.ts:2914 `await swarm.joinPeer(...)` 只触发握手, conn 还没 push 进 this.conns,
|
|
137
|
+
* 立即调 sendTo 找不到 conn → 静默返回 false → 消息扔进虚空.
|
|
138
|
+
*
|
|
139
|
+
* 现在: sendToWithWait 监听 'connection' 事件, 等到 targetPublicKey 真正出现在 this.conns,
|
|
140
|
+
* 才 write; 超时返回 NO_CONN; 写失败返回 WRITE_FAIL; 成功返回 SENT.
|
|
141
|
+
*
|
|
142
|
+
* 上层调用: const r = await p2p.sendToWithWait(pk, rpc, 5000);
|
|
143
|
+
* if (r !== 'SENT') return 502 给前端.
|
|
144
|
+
*/
|
|
145
|
+
async sendToWithWait(publicKeyHex, data, timeoutMs = 5000) {
|
|
146
|
+
// 2026-06-11: 先主动触发 joinPeer, 否则 DHT 上对面可能没 push conn
|
|
147
|
+
if (this.swarm) {
|
|
148
|
+
try {
|
|
149
|
+
await this.swarm.joinPeer(Buffer.from(publicKeyHex, 'hex'));
|
|
150
|
+
}
|
|
151
|
+
catch { }
|
|
152
|
+
}
|
|
153
|
+
// 1) 已有 conn → 立即试
|
|
154
|
+
let conn = this.conns.get(publicKeyHex);
|
|
155
|
+
if (!conn || conn.destroyed) {
|
|
156
|
+
// 2) 等 'connection' 事件 (this.emit('connection', { remotePublicKey, conn }))
|
|
157
|
+
const waitResult = await new Promise((resolve) => {
|
|
158
|
+
const timer = setTimeout(() => {
|
|
159
|
+
this.off('connection', onConn);
|
|
160
|
+
resolve('TIMEOUT');
|
|
161
|
+
}, timeoutMs);
|
|
162
|
+
const onConn = (evt) => {
|
|
163
|
+
if (evt.remotePublicKey === publicKeyHex) {
|
|
164
|
+
clearTimeout(timer);
|
|
165
|
+
this.off('connection', onConn);
|
|
166
|
+
resolve('READY');
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
this.on('connection', onConn);
|
|
170
|
+
});
|
|
171
|
+
if (waitResult === 'TIMEOUT')
|
|
172
|
+
return 'NO_CONN';
|
|
173
|
+
conn = this.conns.get(publicKeyHex);
|
|
174
|
+
if (!conn || conn.destroyed)
|
|
175
|
+
return 'NO_CONN'; // 双保险
|
|
176
|
+
}
|
|
177
|
+
const buf = typeof data === 'string' ? Buffer.from(data) : data;
|
|
178
|
+
try {
|
|
179
|
+
conn.write(buf);
|
|
180
|
+
return 'SENT';
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
console.error(`[P2PDirect:${this.name}] sendToWithWait 写失败 (${publicKeyHex.substring(0, 12)}...):`, err.message);
|
|
184
|
+
return 'WRITE_FAIL';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
130
187
|
getPublicKey() {
|
|
131
188
|
if (!this.swarm)
|
|
132
189
|
return '';
|
|
@@ -28,7 +28,7 @@ import * as path from 'path';
|
|
|
28
28
|
export { initializeMcpAdapter, discoverMcpServers, registerServer, registerTool, listTools, hasTool, getTool, executeTool, getToolCallLog, clearToolCallLog, startServer, stopServer, discoverTools, createTavilyTool, createAmapTool, getAdapterStatus, on as onMcpEvent, off as offMcpEvent, } from '../pi-ecosystem-mcp/index.js';
|
|
29
29
|
export { createGoal, createGoalQueue, getCurrentGoal, startCurrentGoal, completeCurrentGoal, failCurrentGoal, cutoffCurrentGoal, pauseCurrentGoal, checkBudget, getGoalStats, getQueueSummary, loadGoals, clearGoals, compactQueue, loadTemplates, createFromTemplate, nudgeCurrentGoal, } from '../pi-ecosystem-goals/index.js';
|
|
30
30
|
export { createSubagent, startSubagent, delegateTask, getSubagent, listSubagents, listRunningSubagents, terminateSubagent, getStats as getSubagentStats, parallelDelegate, splitTask, } from '../pi-ecosystem-subagents/index.js';
|
|
31
|
-
export { registerAnt, antScouting, antWorking, antReviewing, antComplete, antFail, antAbort, antTick, createTask, dispatchTask, recordResult, getAnt, listAnts, listAntsByRole, listAntsBySignal, getActiveAnts, getTask, listTasks, getSignalHistory, getColonyStatus, getColonyDump, persistColony, loadColony, onColonyEvent, offColonyEvent, } from '../pi-ecosystem-colony/index.js';
|
|
31
|
+
export { registerAnt, antScouting, antWorking, antReviewing, antComplete, antFail, antAbort, antTick, createTask, dispatchTask, recordResult, getAnt, listAnts, listAntsByRole, listAntsBySignal, getActiveAnts, getTask, listTasks, getSignalHistory, getColonyStatus, getColonyDump, persistColony, loadColony, onColonyEvent, offColonyEvent, } from '../pi-ecosystem-colony/index.js'; // 2026-06-11: 蚁群模块已被用户删除, 这行已经无效 (改用 stub)
|
|
32
32
|
// Judgment exports
|
|
33
33
|
export { createJudgment, updateJudgmentConfidence, getAllJudgments, getJudgmentsByType, getJudgmentsForContext, getCombinedJudgments, calculateConfidence, buildValueFunction, getValueFunction, getJudgmentStats, loadFragmentJudgments, clearCache, } from '../pi-ecosystem-judgment/index.js';
|
|
34
34
|
// Distillation exports
|
|
@@ -198,17 +198,20 @@ export class PiEcosystem {
|
|
|
198
198
|
}
|
|
199
199
|
async registerAnt(name, role) {
|
|
200
200
|
const colony = await import('../pi-ecosystem-colony/index.js');
|
|
201
|
-
|
|
202
|
-
|
|
201
|
+
// 2026-06-11: colony 已退化为 stub, registerAnt 不接参数, 返回 void
|
|
202
|
+
colony.registerAnt();
|
|
203
|
+
return name;
|
|
203
204
|
}
|
|
204
205
|
async createColonyTask(description) {
|
|
205
206
|
const colony = await import('../pi-ecosystem-colony/index.js');
|
|
206
|
-
|
|
207
|
-
|
|
207
|
+
// 2026-06-11: createTask 不接参数, 返回空对象
|
|
208
|
+
const task = colony.createTask();
|
|
209
|
+
return task?.id ?? description;
|
|
208
210
|
}
|
|
209
211
|
async dispatchToColony(taskId, antIds) {
|
|
210
212
|
const colony = await import('../pi-ecosystem-colony/index.js');
|
|
211
|
-
|
|
213
|
+
// 2026-06-11: dispatchTask 已退化为 stub
|
|
214
|
+
colony.dispatchTask();
|
|
212
215
|
}
|
|
213
216
|
async persist() {
|
|
214
217
|
const colony = await import('../pi-ecosystem-colony/index.js');
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adaptive Scan — 类 B 自迭代入口
|
|
3
|
+
*
|
|
4
|
+
* 扫描 ~/.bolloon/human-values/usage.jsonl + judgments.json,
|
|
5
|
+
* 生成"自适应建议" (只读, 不改库). 用户在 UI 接受/拒绝后
|
|
6
|
+
* 才会真正生效, 写入 evolution.jsonl 留作审计.
|
|
7
|
+
*
|
|
8
|
+
* 设计原则 (类 B 边界):
|
|
9
|
+
* - 不自动改库 (避免 AI 越权)
|
|
10
|
+
* - 所有建议可逆 (接受/拒绝都行, 接受后写 evolution.jsonl 留痕)
|
|
11
|
+
* - 失败静默 (主对话不阻塞)
|
|
12
|
+
* - 单次扫描纯本地 + 内存计算, 无 LLM 调用 (避免 LLM 幻觉污染)
|
|
13
|
+
*
|
|
14
|
+
* 触发时机:
|
|
15
|
+
* - 用户主动: UI "📊 自适应" tab 点 "重新扫描"
|
|
16
|
+
* - 被动: 每天首次进入判断力 modal 时自动跑一次 (缓存 24h)
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from 'fs/promises';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { loadAllJudgments } from './human-value-store.js';
|
|
22
|
+
// 用 getter 函数而非模块顶层 const: process.env.HOME 在测试 beforeAll 才设,
|
|
23
|
+
// 模块顶层求值时还是真 home. 运行时求值才能正确响应测试 fixture.
|
|
24
|
+
function getUsageLogPath() {
|
|
25
|
+
return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/usage.jsonl';
|
|
26
|
+
}
|
|
27
|
+
function getEvolutionLogPath() {
|
|
28
|
+
return (os.homedir() || process.env.HOME || '/tmp') + '/.bolloon/human-values/evolution.jsonl';
|
|
29
|
+
}
|
|
30
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
31
|
+
async function readUsageLog() {
|
|
32
|
+
try {
|
|
33
|
+
const content = await fs.readFile(getUsageLogPath(), 'utf-8');
|
|
34
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
35
|
+
return lines
|
|
36
|
+
.map((l) => {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(l);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
.filter((e) => Boolean(e) && Array.isArray(e?.usedIds));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function countByJudgment(entries) {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const cutoff7d = now - 7 * DAY_MS;
|
|
53
|
+
const cutoff30d = now - 30 * DAY_MS;
|
|
54
|
+
const map = new Map();
|
|
55
|
+
for (const e of entries) {
|
|
56
|
+
const ts = new Date(e.ts).getTime();
|
|
57
|
+
for (const id of e.usedIds) {
|
|
58
|
+
const cur = map.get(id) || { total: 0, last7d: 0, last30d: 0, lastUseTs: null };
|
|
59
|
+
cur.total++;
|
|
60
|
+
if (ts >= cutoff7d)
|
|
61
|
+
cur.last7d++;
|
|
62
|
+
if (ts >= cutoff30d)
|
|
63
|
+
cur.last30d++;
|
|
64
|
+
if (cur.lastUseTs === null || ts > cur.lastUseTs)
|
|
65
|
+
cur.lastUseTs = ts;
|
|
66
|
+
map.set(id, cur);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return map;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* 主扫描函数: 纯本地计算, 无 LLM
|
|
73
|
+
*/
|
|
74
|
+
export async function runAdaptiveScan() {
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const nowTs = now.getTime();
|
|
77
|
+
const [judgments, entries] = await Promise.all([
|
|
78
|
+
loadAllJudgments(),
|
|
79
|
+
readUsageLog(),
|
|
80
|
+
]);
|
|
81
|
+
const usage = countByJudgment(entries);
|
|
82
|
+
const suggestions = [];
|
|
83
|
+
for (const j of judgments) {
|
|
84
|
+
if ((j.status ?? 'active') !== 'active')
|
|
85
|
+
continue;
|
|
86
|
+
const id = j.id;
|
|
87
|
+
const u = usage.get(id) || { total: 0, last7d: 0, last30d: 0, lastUseTs: null };
|
|
88
|
+
const daysSinceLastUse = u.lastUseTs === null
|
|
89
|
+
? Math.floor((nowTs - new Date(j.timestamp).getTime()) / DAY_MS)
|
|
90
|
+
: Math.floor((nowTs - u.lastUseTs) / DAY_MS);
|
|
91
|
+
const metrics = {
|
|
92
|
+
usage7d: u.last7d,
|
|
93
|
+
usage30d: u.last30d,
|
|
94
|
+
daysSinceLastUse,
|
|
95
|
+
totalUsage: u.total,
|
|
96
|
+
};
|
|
97
|
+
// rising: 7 天频率 > 30 天均值的 1.5 倍, 且至少用过 2 次 (避免噪声)
|
|
98
|
+
if (u.last30d > 0 && u.last7d >= 2) {
|
|
99
|
+
const dailyAvg30 = u.last30d / 30;
|
|
100
|
+
const dailyRate7 = u.last7d / 7;
|
|
101
|
+
if (dailyRate7 > dailyAvg30 * 1.5) {
|
|
102
|
+
suggestions.push({
|
|
103
|
+
key: `rising:${id}`,
|
|
104
|
+
kind: 'rising',
|
|
105
|
+
judgmentId: id,
|
|
106
|
+
decision: j.decision,
|
|
107
|
+
reason: `7 天使用率 (${dailyRate7.toFixed(2)}/d) 是 30 天均值的 ${(dailyRate7 / dailyAvg30).toFixed(1)} 倍`,
|
|
108
|
+
action: 'boost',
|
|
109
|
+
metrics,
|
|
110
|
+
scannedAt: now.toISOString(),
|
|
111
|
+
});
|
|
112
|
+
continue; // 不再归入 unused/stale
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// stale: 90 天未使用 + 总使用 < 3 (低频 + 长期不用)
|
|
116
|
+
if (u.total < 3 && daysSinceLastUse >= 90) {
|
|
117
|
+
suggestions.push({
|
|
118
|
+
key: `stale:${id}`,
|
|
119
|
+
kind: 'stale',
|
|
120
|
+
judgmentId: id,
|
|
121
|
+
decision: j.decision,
|
|
122
|
+
reason: `已 ${daysSinceLastUse} 天未使用, 总使用仅 ${u.total} 次`,
|
|
123
|
+
action: 'deprecate',
|
|
124
|
+
metrics,
|
|
125
|
+
scannedAt: now.toISOString(),
|
|
126
|
+
});
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
// unused: 30 天未使用 + 总使用 < 5 (中频但近期没在用)
|
|
130
|
+
if (u.total < 5 && daysSinceLastUse >= 30) {
|
|
131
|
+
suggestions.push({
|
|
132
|
+
key: `unused:${id}`,
|
|
133
|
+
kind: 'unused',
|
|
134
|
+
judgmentId: id,
|
|
135
|
+
decision: j.decision,
|
|
136
|
+
reason: `已 ${daysSinceLastUse} 天未使用, 总使用 ${u.total} 次 (可能不再相关)`,
|
|
137
|
+
action: 'review',
|
|
138
|
+
metrics,
|
|
139
|
+
scannedAt: now.toISOString(),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 按 action 重要性排序: rising > stale > unused
|
|
144
|
+
const order = { rising: 0, stale: 1, unused: 2, causal_conflict: 3, low_causal_power: 4 };
|
|
145
|
+
// 阶段 2: causal_conflict — judgment 库内自动检测冲突对
|
|
146
|
+
const { runConflictDetection } = await import('./causal-judge.js');
|
|
147
|
+
const conflictResult = await runConflictDetection();
|
|
148
|
+
for (const a of judgments) {
|
|
149
|
+
if ((a.status ?? 'active') !== 'active')
|
|
150
|
+
continue;
|
|
151
|
+
if (!Array.isArray(a.conflictWith) || a.conflictWith.length === 0)
|
|
152
|
+
continue;
|
|
153
|
+
// 列出每对冲突 (limit 每个 judgment 最多 3 对, 避免 UI 刷屏)
|
|
154
|
+
const conflicts = a.conflictWith.slice(0, 3);
|
|
155
|
+
for (const otherId of conflicts) {
|
|
156
|
+
const other = judgments.find((j) => j.id === otherId);
|
|
157
|
+
if (!other)
|
|
158
|
+
continue;
|
|
159
|
+
const c = await import('./causal-judge.js');
|
|
160
|
+
const det = c.detectConflict(a, other);
|
|
161
|
+
suggestions.push({
|
|
162
|
+
key: `causal_conflict:${a.id}:${other.id}`,
|
|
163
|
+
kind: 'causal_conflict',
|
|
164
|
+
judgmentId: a.id,
|
|
165
|
+
decision: `${a.decision} ↔ ${other.decision}`,
|
|
166
|
+
reason: det.isConflict ? det.reason : '库内已标冲突, 需 LLM 复核',
|
|
167
|
+
action: 'review',
|
|
168
|
+
metrics: { usage7d: 0, usage30d: 0, daysSinceLastUse: 0, totalUsage: 0 },
|
|
169
|
+
scannedAt: now.toISOString(),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// 静默 conflictResult.detected 计数 (已通过 import 触发了)
|
|
174
|
+
void conflictResult;
|
|
175
|
+
// 阶段 2: low_causal_power — 留空 (依赖 do-calculus 跑过后的低边际贡献记录)
|
|
176
|
+
// 当前不主动跑 (cost 高), 仅在 UI 手动触发后, 类 B 下次扫描时捡起来
|
|
177
|
+
// 此处不实现, 留作下个迭代
|
|
178
|
+
suggestions.sort((a, b) => order[a.kind] - order[b.kind]);
|
|
179
|
+
return {
|
|
180
|
+
scannedAt: now.toISOString(),
|
|
181
|
+
judgmentsTotal: judgments.length,
|
|
182
|
+
usageEntriesScanned: entries.length,
|
|
183
|
+
suggestions,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export async function logEvolution(entry) {
|
|
187
|
+
try {
|
|
188
|
+
await fs.mkdir(path.dirname(getEvolutionLogPath()), { recursive: true });
|
|
189
|
+
await fs.appendFile(getEvolutionLogPath(), JSON.stringify(entry) + '\n', 'utf-8');
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
console.warn('[adaptive-scan] logEvolution failed:', err);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
export async function readEvolutionLog(limit = 50) {
|
|
196
|
+
try {
|
|
197
|
+
const content = await fs.readFile(getEvolutionLogPath(), 'utf-8');
|
|
198
|
+
const lines = content.trim().split('\n').filter(Boolean);
|
|
199
|
+
return lines
|
|
200
|
+
.slice(-limit)
|
|
201
|
+
.reverse()
|
|
202
|
+
.map((l) => {
|
|
203
|
+
try {
|
|
204
|
+
return JSON.parse(l);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
.filter((e) => Boolean(e));
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// ============================================================
|
|
217
|
+
// 缓存: 避免每次打开 modal 都扫一次
|
|
218
|
+
// ============================================================
|
|
219
|
+
let lastScan = null;
|
|
220
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
221
|
+
export async function getCachedScan(force = false) {
|
|
222
|
+
if (!force && lastScan && Date.now() - lastScan.at < CACHE_TTL_MS) {
|
|
223
|
+
return lastScan.result;
|
|
224
|
+
}
|
|
225
|
+
const result = await runAdaptiveScan();
|
|
226
|
+
lastScan = { at: Date.now(), result };
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
export function clearAdaptiveScanCache() {
|
|
230
|
+
lastScan = null;
|
|
231
|
+
}
|