@bolloon/bolloon-agent 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/parse-tool-call.js +36 -0
- package/dist/agents/pi-sdk-tools.js +19 -7
- package/dist/cli/loading-tui.js +78 -11
- package/dist/documents/reader.js +3 -0
- package/dist/index.js +31 -2
- package/dist/judgeness/auto-add.js +145 -0
- package/dist/judgeness/protocol.js +214 -0
- package/dist/judgeness/rank.js +78 -0
- package/dist/judgeness/reflect.js +93 -0
- package/dist/judgeness/store.js +481 -0
- package/dist/judgeness/types.js +19 -0
- package/dist/judgeness/visibility.js +118 -0
- package/dist/scripts/dedup-session-messages.js +68 -0
- package/dist/web/client-hearth.js +67 -0
- package/dist/web/client.js +351 -41
- package/dist/web/routes-hearth.js +371 -0
- package/dist/web/server.js +356 -10
- package/dist/web/style.css +27 -0
- package/dist/web/ui/message-renderer.js +13 -2
- package/dist/web/ui/step-timeline.js +17 -0
- package/dist/web/util/dual-mode.js +87 -0
- package/package.json +2 -2
|
@@ -72,6 +72,42 @@ export function parseToolCall(content, ctx) {
|
|
|
72
72
|
return null;
|
|
73
73
|
// === 0. 剥离 思考块 — 必须先做, 否则旧 regex `<(\w+)>...</\1>` 会先匹配 想想标签 ===
|
|
74
74
|
const strippedContent = content.replace(/<think[\s\S]*?<\/think/g, '');
|
|
75
|
+
// [diag-2026-07-12] 临时诊断日志:WebUI 出现 7 个 “必填/参数 undefined” 错误,
|
|
76
|
+
// 怀疑 parseToolCall 没拿到正确的 name/args. 这里只打日志不改任何逻辑.
|
|
77
|
+
// 日志形态: [parseToolCall diag] ok=true/false name=... argKeys=... rawHead=...
|
|
78
|
+
// 用 console.warn 一行 JSON, 方便 grep + 排时间线.
|
|
79
|
+
try {
|
|
80
|
+
const result = (function _diagProbe() {
|
|
81
|
+
// 用与正文完全相同的解析路径,但提前跑一次,记录是否命中
|
|
82
|
+
// 走完下面所有分支再覆盖回原值即可
|
|
83
|
+
return null;
|
|
84
|
+
})();
|
|
85
|
+
void result;
|
|
86
|
+
const probe = (function _doParse() {
|
|
87
|
+
// 内联一份最便宜的 "能不能解出 name" 探测 — 不重复跑全部分支
|
|
88
|
+
const m1 = strippedContent.match(/<invoke\s+name=["']([\w]+)["']/);
|
|
89
|
+
if (m1) {
|
|
90
|
+
return { name: m1[1], args: { __probe_invoketag: '1' } };
|
|
91
|
+
}
|
|
92
|
+
const m2 = strippedContent.match(/<function_calls>[\s\S]*?<invoke\s+name=["']([\w]+)["']/);
|
|
93
|
+
if (m2) {
|
|
94
|
+
return { name: m2[1], args: { __probe_function_calls: '1' } };
|
|
95
|
+
}
|
|
96
|
+
const m3 = strippedContent.match(/\{[\s\S]*?"name"\s*:\s*["']([\w]+)["']/);
|
|
97
|
+
if (m3) {
|
|
98
|
+
return { name: m3[1], args: { __probe_json_name: '1' } };
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
})();
|
|
102
|
+
console.warn('[parseToolCall diag] rawLen=' + content.length +
|
|
103
|
+
' strippedLen=' + strippedContent.length +
|
|
104
|
+
' probeName=' + (probe?.name ?? 'null') +
|
|
105
|
+
' rawHead=' + JSON.stringify(content.slice(0, 500)));
|
|
106
|
+
}
|
|
107
|
+
catch (diagErr) {
|
|
108
|
+
// 诊断日志绝不能影响主路径 — 吞掉任何 diag 自身抛错
|
|
109
|
+
console.warn('[parseToolCall diag] diag-self-failed:', String(diagErr));
|
|
110
|
+
}
|
|
75
111
|
// === 1. JSON function-call (OpenAI / Anthropic / Minimax-style) ===
|
|
76
112
|
const jsonPatterns = [
|
|
77
113
|
// markdown json code block + OpenAI 块, 同时匹配 arguments/input 字段
|
|
@@ -37,10 +37,13 @@ export function registerBuiltinTools(ctx) {
|
|
|
37
37
|
ctx.tools.set('read_document', {
|
|
38
38
|
name: 'read_document',
|
|
39
39
|
description: '读取文档内容,支持 .txt, .md, .pdf, .docx 格式',
|
|
40
|
-
parameters: { path: '
|
|
40
|
+
parameters: { path: '文件路径 (必填)' },
|
|
41
41
|
execute: async (args) => {
|
|
42
42
|
try {
|
|
43
|
-
const
|
|
43
|
+
const path = String(args.path || '').trim();
|
|
44
|
+
if (!path)
|
|
45
|
+
return { success: false, error: 'path 必填' };
|
|
46
|
+
const content = await documentReader.read(path);
|
|
44
47
|
return {
|
|
45
48
|
success: true,
|
|
46
49
|
output: `📄 ${content.metadata.filename}\n大小: ${content.metadata.size} 字节\n\n${content.text.substring(0, 1000)}${content.text.length > 1000 ? '...' : ''}`
|
|
@@ -54,16 +57,19 @@ export function registerBuiltinTools(ctx) {
|
|
|
54
57
|
ctx.tools.set('summarize_document', {
|
|
55
58
|
name: 'summarize_document',
|
|
56
59
|
description: '总结文档内容,分析并生成摘要',
|
|
57
|
-
parameters: { path: '
|
|
60
|
+
parameters: { path: '文件路径 (必填)', context: '可选, 总结上下文提示' },
|
|
58
61
|
execute: async (args) => {
|
|
59
62
|
try {
|
|
63
|
+
const path = String(args.path || '').trim();
|
|
64
|
+
if (!path)
|
|
65
|
+
return { success: false, error: 'path 必填' };
|
|
60
66
|
if (!ctx.minimaxAvailable) {
|
|
61
67
|
return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
|
|
62
68
|
}
|
|
63
69
|
// summarizeDocument 走 PiAgentSession.summarizeDocument, 这里通过 tools 反向调用不好处理
|
|
64
70
|
// 简化: 让 LLM 自己直接走 shell_exec / use_skill 路径
|
|
65
71
|
const llm = getMinimax();
|
|
66
|
-
const content = await documentReader.read(
|
|
72
|
+
const content = await documentReader.read(path);
|
|
67
73
|
const r = await llm.summarize(content.text, args.context);
|
|
68
74
|
return {
|
|
69
75
|
success: true,
|
|
@@ -78,15 +84,21 @@ export function registerBuiltinTools(ctx) {
|
|
|
78
84
|
ctx.tools.set('improve_document', {
|
|
79
85
|
name: 'improve_document',
|
|
80
86
|
description: '根据要求改进文档内容',
|
|
81
|
-
parameters: { path: '
|
|
87
|
+
parameters: { path: '文件路径 (必填)', requirements: '改进要求 (必填)' },
|
|
82
88
|
execute: async (args) => {
|
|
83
89
|
try {
|
|
90
|
+
const path = String(args.path || '').trim();
|
|
91
|
+
if (!path)
|
|
92
|
+
return { success: false, error: 'path 必填' };
|
|
93
|
+
const requirements = String(args.requirements || '').trim();
|
|
94
|
+
if (!requirements)
|
|
95
|
+
return { success: false, error: 'requirements 必填' };
|
|
84
96
|
if (!ctx.minimaxAvailable) {
|
|
85
97
|
return { success: false, error: 'LLM未初始化,请设置 MINIMAX_API_KEY' };
|
|
86
98
|
}
|
|
87
99
|
const llm = getMinimax();
|
|
88
|
-
const content = await documentReader.read(
|
|
89
|
-
const improved = await llm.summarize(content.text + '\n\n改进要求: ' +
|
|
100
|
+
const content = await documentReader.read(path);
|
|
101
|
+
const improved = await llm.summarize(content.text + '\n\n改进要求: ' + requirements, undefined);
|
|
90
102
|
return {
|
|
91
103
|
success: true,
|
|
92
104
|
output: `✅ 改进完成\n质量评分: ${(improved.qualityScore * 10).toFixed(1)}/10\n${improved.summary ? '\n改进内容:\n' + improved.summary.substring(0, 500) + '...' : ''}`
|
package/dist/cli/loading-tui.js
CHANGED
|
@@ -7,34 +7,101 @@ const GREEN = '\x1b[32m';
|
|
|
7
7
|
const RED = '\x1b[31m';
|
|
8
8
|
const GRAY = '\x1b[90m';
|
|
9
9
|
const RESET = '\x1b[0m';
|
|
10
|
+
const STEP_SYMBOL = {
|
|
11
|
+
pending: `${GRAY}○${RESET}`,
|
|
12
|
+
active: `${YELLOW}⠹${RESET}`,
|
|
13
|
+
ok: `${GREEN}✓${RESET}`,
|
|
14
|
+
warn: `${YELLOW}⚠${RESET}`,
|
|
15
|
+
error: `${RED}✗${RESET}`,
|
|
16
|
+
};
|
|
10
17
|
export class LoadingTUI {
|
|
11
18
|
write;
|
|
12
19
|
timer = null;
|
|
13
|
-
|
|
20
|
+
frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
21
|
+
frameIdx = 0;
|
|
22
|
+
steps = [];
|
|
23
|
+
currentLabel = 'Bolloon loading...';
|
|
24
|
+
lastRenderedStepCount = 0;
|
|
25
|
+
finished = false;
|
|
26
|
+
ok = true;
|
|
14
27
|
constructor() {
|
|
15
28
|
this.write = process.stdout.write.bind(process.stdout);
|
|
16
29
|
}
|
|
30
|
+
setSteps(steps) {
|
|
31
|
+
this.steps = steps.map(label => ({ label, status: 'pending' }));
|
|
32
|
+
this.drawAll();
|
|
33
|
+
}
|
|
34
|
+
startStep(index, label) {
|
|
35
|
+
if (index < 0 || index >= this.steps.length)
|
|
36
|
+
return;
|
|
37
|
+
this.steps[index].status = 'active';
|
|
38
|
+
if (label !== undefined)
|
|
39
|
+
this.steps[index].label = label;
|
|
40
|
+
this.drawAll();
|
|
41
|
+
}
|
|
42
|
+
completeStep(index, status = 'ok', label) {
|
|
43
|
+
if (index < 0 || index >= this.steps.length)
|
|
44
|
+
return;
|
|
45
|
+
this.steps[index].status = status;
|
|
46
|
+
if (label !== undefined)
|
|
47
|
+
this.steps[index].label = label;
|
|
48
|
+
this.drawAll();
|
|
49
|
+
}
|
|
50
|
+
setMessage(msg) {
|
|
51
|
+
this.currentLabel = msg;
|
|
52
|
+
}
|
|
17
53
|
start(msg = 'Bolloon loading...') {
|
|
18
|
-
this.
|
|
54
|
+
if (this.timer)
|
|
55
|
+
return;
|
|
56
|
+
this.currentLabel = msg;
|
|
19
57
|
this.write(HIDE);
|
|
20
|
-
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
21
|
-
let i = 0;
|
|
22
58
|
this.timer = setInterval(() => {
|
|
23
|
-
if (
|
|
59
|
+
if (this.finished)
|
|
24
60
|
return;
|
|
25
|
-
this.write(`\r${CLEAR}\r ${YELLOW}${frames[
|
|
61
|
+
this.write(`\r${CLEAR}\r ${YELLOW}${this.frames[this.frameIdx++ % this.frames.length]}${RESET} ${this.currentLabel}`);
|
|
26
62
|
}, 100);
|
|
27
63
|
}
|
|
64
|
+
drawAll() {
|
|
65
|
+
if (!this.timer || this.finished)
|
|
66
|
+
return;
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const step of this.steps) {
|
|
69
|
+
const prefix = step.status === 'active' ? YELLOW : '';
|
|
70
|
+
out.push(` ${STEP_SYMBOL[step.status]} ${prefix}${step.label}${RESET}\n`);
|
|
71
|
+
}
|
|
72
|
+
this.lastRenderedStepCount = this.steps.length;
|
|
73
|
+
this.write(out.join(''));
|
|
74
|
+
this.write(`\x1b[${this.steps.length}A`);
|
|
75
|
+
this.write(`\r${CLEAR}\r ${YELLOW}${this.frames[this.frameIdx % this.frames.length]}${RESET} ${this.currentLabel}`);
|
|
76
|
+
}
|
|
28
77
|
stop(ok = true) {
|
|
29
|
-
this.
|
|
30
|
-
|
|
78
|
+
this.finished = true;
|
|
79
|
+
this.ok = ok;
|
|
80
|
+
if (this.timer) {
|
|
31
81
|
clearInterval(this.timer);
|
|
32
|
-
|
|
82
|
+
this.timer = null;
|
|
83
|
+
}
|
|
84
|
+
if (this.lastRenderedStepCount > 0) {
|
|
85
|
+
this.write(`\x1b[${this.lastRenderedStepCount}B`);
|
|
86
|
+
}
|
|
33
87
|
this.write(`\r${CLEAR}\r`);
|
|
34
|
-
if (
|
|
88
|
+
if (this.steps.length > 0) {
|
|
89
|
+
for (const step of this.steps) {
|
|
90
|
+
this.write(` ${STEP_SYMBOL[step.status]} ${step.label}\n`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (this.ok) {
|
|
35
94
|
this.write(` ${GREEN}✓${RESET} ${CYAN}Bolloon${RESET} ${GRAY}ready${RESET}\n`);
|
|
36
|
-
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
37
97
|
this.write(` ${RED}✗${RESET} ${CYAN}Bolloon${RESET} ${GRAY}startup failed${RESET}\n`);
|
|
98
|
+
}
|
|
38
99
|
this.write(SHOW);
|
|
39
100
|
}
|
|
101
|
+
isFinished() {
|
|
102
|
+
return this.finished;
|
|
103
|
+
}
|
|
104
|
+
wasOk() {
|
|
105
|
+
return this.ok;
|
|
106
|
+
}
|
|
40
107
|
}
|
package/dist/documents/reader.js
CHANGED
|
@@ -4,6 +4,9 @@ import pdfParse from 'pdf-parse';
|
|
|
4
4
|
import mammoth from 'mammoth';
|
|
5
5
|
export class DocumentReader {
|
|
6
6
|
async read(filePath) {
|
|
7
|
+
if (!filePath || typeof filePath !== 'string' || !filePath.trim()) {
|
|
8
|
+
throw new Error('read(filePath): filePath 必填且必须是非空字符串');
|
|
9
|
+
}
|
|
7
10
|
const stats = await fs.stat(filePath);
|
|
8
11
|
const ext = path.extname(filePath).toLowerCase();
|
|
9
12
|
const filename = path.basename(filePath);
|
package/dist/index.js
CHANGED
|
@@ -1819,7 +1819,16 @@ async function main() {
|
|
|
1819
1819
|
const isCLIInteractive = mode === 'cli' && !isNonInteractive;
|
|
1820
1820
|
loading = isCLIInteractive ? new LoadingTUI() : null;
|
|
1821
1821
|
if (loading) {
|
|
1822
|
-
loading.
|
|
1822
|
+
loading.setSteps([
|
|
1823
|
+
'LLM provider 检测',
|
|
1824
|
+
'DIAP 身份生成',
|
|
1825
|
+
'DID 发布到 IPFS',
|
|
1826
|
+
'P2P 网络启动',
|
|
1827
|
+
'iroh transport 启动',
|
|
1828
|
+
'Bolloon 上下文 bootstrap',
|
|
1829
|
+
'Web 服务启动',
|
|
1830
|
+
]);
|
|
1831
|
+
loading.start('启动中...');
|
|
1823
1832
|
console.log = () => { };
|
|
1824
1833
|
console.info = () => { };
|
|
1825
1834
|
process.stdout.write = () => true;
|
|
@@ -1873,22 +1882,31 @@ async function main() {
|
|
|
1873
1882
|
hasQwen ? 'Qwen' : null;
|
|
1874
1883
|
if (llmProvider) {
|
|
1875
1884
|
s.step(0, 4, `LLM: ${llmProvider}`, 'ok');
|
|
1885
|
+
loading?.completeStep(0, 'ok', `LLM: ${llmProvider}`);
|
|
1876
1886
|
initMinimax({ provider: llmProvider.toLowerCase() });
|
|
1877
1887
|
}
|
|
1878
1888
|
else {
|
|
1879
1889
|
s.step(0, 4, 'LLM: 未配置', 'warn');
|
|
1890
|
+
loading?.completeStep(0, 'warn', 'LLM: 未配置');
|
|
1880
1891
|
if (isNonInteractive) {
|
|
1881
1892
|
s.warn('未设置任何 LLM API Key,功能受限');
|
|
1882
1893
|
}
|
|
1883
1894
|
}
|
|
1895
|
+
loading?.startStep(1, '生成 DIAP 身份...');
|
|
1884
1896
|
const { keypair, did, name } = await bootstrapIdentity();
|
|
1885
1897
|
agentIdentity = { did, name, publicKey: Buffer.from(keypair.publicKey).toString('hex') };
|
|
1898
|
+
loading?.completeStep(1, 'ok', `身份 ${name}`);
|
|
1899
|
+
loading?.startStep(2, '发布 DID 到 IPFS...');
|
|
1886
1900
|
publishDID(name, keypair).then(({ cid, ipnsName }) => {
|
|
1887
1901
|
if (cid)
|
|
1888
1902
|
agentIdentity.cid = cid;
|
|
1889
1903
|
if (ipnsName)
|
|
1890
1904
|
agentIdentity.ipnsName = ipnsName;
|
|
1891
|
-
|
|
1905
|
+
loading?.completeStep(2, cid ? 'ok' : 'warn', cid ? 'DID 已发布' : 'DID 本地模式');
|
|
1906
|
+
}).catch(() => {
|
|
1907
|
+
loading?.completeStep(2, 'warn', 'DID 本地模式');
|
|
1908
|
+
});
|
|
1909
|
+
loading?.startStep(3, '启动 P2P 网络...');
|
|
1892
1910
|
const verifier = createVerificationManager();
|
|
1893
1911
|
let comm = null;
|
|
1894
1912
|
try {
|
|
@@ -1900,7 +1918,9 @@ async function main() {
|
|
|
1900
1918
|
agentIdentity.peerId = connections[0].publicKey;
|
|
1901
1919
|
agentIdentity.p2pChannel = 'bolloon-agent-harness';
|
|
1902
1920
|
}
|
|
1921
|
+
loading?.completeStep(3, 'ok', 'P2P 已连接');
|
|
1903
1922
|
}).catch(err => {
|
|
1923
|
+
loading?.completeStep(3, 'warn', 'P2P Web 模式启动失败');
|
|
1904
1924
|
s.warn(`P2P Web 模式启动失败: ${err.message}`);
|
|
1905
1925
|
});
|
|
1906
1926
|
}
|
|
@@ -1911,23 +1931,30 @@ async function main() {
|
|
|
1911
1931
|
agentIdentity.peerId = connections[0].publicKey;
|
|
1912
1932
|
agentIdentity.p2pChannel = 'bolloon-agent-harness';
|
|
1913
1933
|
}
|
|
1934
|
+
loading?.completeStep(3, 'ok', 'P2P 已连接');
|
|
1914
1935
|
}
|
|
1915
1936
|
}
|
|
1916
1937
|
catch (err) {
|
|
1917
1938
|
s.warn(`P2P 初始化失败: ${err.message}`);
|
|
1918
1939
|
s.warn('将使用无 P2P 模式运行');
|
|
1940
|
+
loading?.completeStep(3, 'error', 'P2P 初始化失败');
|
|
1919
1941
|
}
|
|
1942
|
+
loading?.startStep(4, '启动 iroh transport...');
|
|
1920
1943
|
await bootstrapIroh(keypair, name);
|
|
1944
|
+
loading?.completeStep(4, 'ok', 'iroh 已就绪');
|
|
1921
1945
|
// Bolloon Bootstrap: 启动扫描 + Context 收集 + 挂定时任务
|
|
1922
1946
|
// 失败静默 (主流程不被阻塞)
|
|
1947
|
+
loading?.startStep(5, '正在 bootstrap bolloon 上下文...');
|
|
1923
1948
|
try {
|
|
1924
1949
|
const { bootstrapBolloon } = await import('./pi-ecosystem-judgment/human-value-pipeline.js');
|
|
1925
1950
|
s.info('正在 bootstrap bolloon 上下文...');
|
|
1926
1951
|
const bs = await bootstrapBolloon({ cwd: process.cwd() });
|
|
1927
1952
|
s.info(`Bootstrap 完成 (${bs.durationMs}ms, ${bs.errors.length} 个非致命错误)`);
|
|
1953
|
+
loading?.completeStep(5, 'ok', `Bootstrap 完成 (${bs.durationMs}ms)`);
|
|
1928
1954
|
}
|
|
1929
1955
|
catch (err) {
|
|
1930
1956
|
s.warn(`Bootstrap 失败 (非致命, 主流程继续): ${err.message}`);
|
|
1957
|
+
loading?.completeStep(5, 'warn', 'Bootstrap 失败 (已跳过)');
|
|
1931
1958
|
}
|
|
1932
1959
|
s.divider();
|
|
1933
1960
|
if (mode === 'web') {
|
|
@@ -1939,10 +1966,12 @@ async function main() {
|
|
|
1939
1966
|
console.log('[startup] BOLLOON_DEV_MODE=1, 开发者模式: 自迭代已启用');
|
|
1940
1967
|
}
|
|
1941
1968
|
const { createWebServer, openBrowser } = await import('./web/server.js');
|
|
1969
|
+
loading?.startStep(6, `启动 Web 服务端口 ${port}...`);
|
|
1942
1970
|
s.info(`启动 Web 服务端口 ${port}...`);
|
|
1943
1971
|
// 2026-06-24: CLI 默认 loopback bind (安全), LAN 访问需 BOLLOON_HOST=0.0.0.0
|
|
1944
1972
|
const bindHost = process.env.BOLLOON_HOST;
|
|
1945
1973
|
const { port: actualPort } = await createWebServer(port, { selfImprove, ...(bindHost ? { host: bindHost } : {}) });
|
|
1974
|
+
loading?.completeStep(6, 'ok', `Web 服务 :${actualPort}`);
|
|
1946
1975
|
const displayHost = bindHost ?? '127.0.0.1';
|
|
1947
1976
|
s.success(`浏览器已打开 → http://${displayHost}:${actualPort}`);
|
|
1948
1977
|
openBrowser(`http://${displayHost}:${actualPort}`);
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* judgeness · auto-add.ts — Channel-based Auto-add (反攻期 O3)
|
|
3
|
+
*
|
|
4
|
+
* 用户原话: "传播智能体的时候, 智能体可根据内容频道选择其他用户的 Id 自动添加"
|
|
5
|
+
*
|
|
6
|
+
* 流程:
|
|
7
|
+
* 1. POST /api/hearth/channel-autoadd { channelTopic, sourceChannelOwnerPk? }
|
|
8
|
+
* 2. 闸 2 (allowlist gate) 校验 sourceChannelOwnerPk
|
|
9
|
+
* 3. 扫描 ~/.bolloon/judgeness/descriptions/, 找出 scope.topics 含 channelTopic 且 openState='open' 的 description
|
|
10
|
+
* 4. 对每个 description 的 owner pk 调用 p2p-direct.joinTopic
|
|
11
|
+
* 5. 全部进 ~/.bolloon/human-values/counterfactual-audit.jsonl
|
|
12
|
+
* 6. 频次限制 (defense=无; 反攻期 = 每分钟 5 次; 单 peer pk 24h 内最多 10 次)
|
|
13
|
+
*
|
|
14
|
+
* 反攻期接 src/network/p2p-direct.ts 的 joinTopic; 防御期 stub.
|
|
15
|
+
* 反攻期接 src/judgeness/protocol.ts 的 sendAutoaddInvite.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import * as os from 'os';
|
|
20
|
+
const DEFENSE_FREQ_LIMIT_PER_HOUR = 5; // 防御期更严
|
|
21
|
+
const ROLLING_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
|
22
|
+
export async function performAutoAdd(req, opts = {}) {
|
|
23
|
+
if (!req.channelTopic)
|
|
24
|
+
throw new Error('channelTopic required');
|
|
25
|
+
const now = opts.nowMs ?? Date.now();
|
|
26
|
+
// ---- 频次限制 (读 audit log last hour 统计) ----
|
|
27
|
+
const auditLog = await readAutoaddAudit();
|
|
28
|
+
const recent = auditLog.filter((l) => now - l.ts < ROLLING_WINDOW_MS);
|
|
29
|
+
if (recent.length >= DEFENSE_FREQ_LIMIT_PER_HOUR) {
|
|
30
|
+
return {
|
|
31
|
+
channelTopic: req.channelTopic,
|
|
32
|
+
matched: 0,
|
|
33
|
+
joined: 0,
|
|
34
|
+
skipped: 0,
|
|
35
|
+
auditLines: [],
|
|
36
|
+
frequencyLimited: true,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// ---- 扫描 descriptions 找 matches ----
|
|
40
|
+
const { listDescriptions } = await import('./store.js');
|
|
41
|
+
const descs = await listDescriptions();
|
|
42
|
+
const matched = descs.filter((d) => {
|
|
43
|
+
const open = d.openState === 'open';
|
|
44
|
+
const topicMatch = (d.scope.topics ?? []).includes(req.channelTopic);
|
|
45
|
+
return open && topicMatch;
|
|
46
|
+
});
|
|
47
|
+
// ---- join (defense=stub) ----
|
|
48
|
+
const result = {
|
|
49
|
+
channelTopic: req.channelTopic,
|
|
50
|
+
matched: matched.length,
|
|
51
|
+
joined: 0,
|
|
52
|
+
skipped: 0,
|
|
53
|
+
auditLines: [],
|
|
54
|
+
frequencyLimited: false,
|
|
55
|
+
};
|
|
56
|
+
// 每次请求都写一条 audit line (不论 matched), 这样 frequency limit 才能工作
|
|
57
|
+
result.auditLines.push(JSON.stringify({
|
|
58
|
+
ts: now,
|
|
59
|
+
kind: 'autoadd_request',
|
|
60
|
+
channelTopic: req.channelTopic,
|
|
61
|
+
by: undefined,
|
|
62
|
+
matched: matched.length,
|
|
63
|
+
}));
|
|
64
|
+
for (const d of matched) {
|
|
65
|
+
const ownerPk = d.byAgentId ?? '__no-pk__';
|
|
66
|
+
if (!opts.joinTopic) {
|
|
67
|
+
// defense: 仅 audit, 不调用 joinTopic
|
|
68
|
+
result.skipped += 1;
|
|
69
|
+
const line = JSON.stringify({
|
|
70
|
+
ts: now,
|
|
71
|
+
kind: 'autoadd_skipped',
|
|
72
|
+
channelTopic: req.channelTopic,
|
|
73
|
+
descriptionId: d.descriptionId,
|
|
74
|
+
ownerPk,
|
|
75
|
+
reason: 'defense stub',
|
|
76
|
+
});
|
|
77
|
+
result.auditLines.push(line);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const r = await opts.joinTopic(req.channelTopic, ownerPk);
|
|
81
|
+
if (r.ok) {
|
|
82
|
+
result.joined += 1;
|
|
83
|
+
result.auditLines.push(JSON.stringify({
|
|
84
|
+
ts: now,
|
|
85
|
+
kind: 'autoadd_joined',
|
|
86
|
+
channelTopic: req.channelTopic,
|
|
87
|
+
descriptionId: d.descriptionId,
|
|
88
|
+
ownerPk,
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
result.skipped += 1;
|
|
93
|
+
result.auditLines.push(JSON.stringify({
|
|
94
|
+
ts: now,
|
|
95
|
+
kind: 'autoadd_join_failed',
|
|
96
|
+
channelTopic: req.channelTopic,
|
|
97
|
+
descriptionId: d.descriptionId,
|
|
98
|
+
ownerPk,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ---- 写 audit log ----
|
|
103
|
+
await appendCounterfactualAudit(result.auditLines);
|
|
104
|
+
return result;
|
|
105
|
+
}
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// audit 读写 helpers
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
async function readAutoaddAudit() {
|
|
110
|
+
const auditPath = await auditPathResolved();
|
|
111
|
+
try {
|
|
112
|
+
const raw = await fs.readFile(auditPath, 'utf-8');
|
|
113
|
+
return raw.split('\n').filter(Boolean).map((l) => {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(l);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}).filter((x) => !!x);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function appendCounterfactualAudit(lines) {
|
|
127
|
+
if (lines.length === 0)
|
|
128
|
+
return;
|
|
129
|
+
const auditPath = await auditPathResolved();
|
|
130
|
+
const dir = path.dirname(auditPath);
|
|
131
|
+
await fs.mkdir(dir, { recursive: true });
|
|
132
|
+
await fs.appendFile(auditPath, lines.join('\n') + '\n', 'utf-8');
|
|
133
|
+
}
|
|
134
|
+
let _auditPathCache = null;
|
|
135
|
+
async function auditPathResolved() {
|
|
136
|
+
if (_auditPathCache)
|
|
137
|
+
return _auditPathCache;
|
|
138
|
+
const home = process.env.BOLLOON_HOME || path.join(os.homedir(), '.bolloon');
|
|
139
|
+
_auditPathCache = path.join(home, 'human-values', 'counterfactual-audit.jsonl');
|
|
140
|
+
return _auditPathCache;
|
|
141
|
+
}
|
|
142
|
+
// 工具: 复位 cache (测试用)
|
|
143
|
+
export function _resetAuditPathCacheForTest() {
|
|
144
|
+
_auditPathCache = null;
|
|
145
|
+
}
|