@bolloon/bolloon-agent 0.3.21 → 0.3.23
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/deny-pipeline.js +116 -0
- package/dist/agents/parse-tool-call.js +26 -4
- package/dist/agents/pi-sdk.js +247 -64
- package/dist/agents/session-store.js +87 -2
- package/dist/bootstrap/snip-collapse.js +135 -0
- package/dist/cli/loading-tui.js +64 -10
- package/dist/electron/config.js +14 -9
- package/dist/electron/dialogs.js +53 -16
- package/dist/electron/first-run.js +65 -24
- package/dist/electron/ipc.js +14 -10
- package/dist/electron/logger.js +44 -7
- package/dist/electron/main.js +45 -42
- package/dist/electron/menu.js +18 -13
- package/dist/electron/paths.js +54 -12
- package/dist/electron/server.js +57 -18
- package/dist/electron/tray.js +53 -15
- package/dist/electron/window.js +61 -22
- package/dist/electron-preload.js +19 -16
- package/dist/electron.js +4 -1
- package/dist/external-engines/delegate.js +19 -0
- package/dist/hooks/hooks-engine.js +329 -0
- package/dist/index.js +49 -23
- package/dist/llm/pi-ai.js +5 -17
- package/dist/security/tool-gate.js +8 -1
- package/dist/social/dunbar-tier.js +409 -0
- package/dist/utils/auto-update.js +51 -12
- package/dist/web/client.js +1 -1
- package/dist/web/server.js +17 -3
- package/dist/web/style.css +2 -2
- package/dist/web/ui/step-timeline.js +1 -1
- package/package.json +24 -24
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { createSubAgentManager } from './agents/subagent-manager.js';
|
|
|
15
15
|
import { getGlobalSharedContext } from './social/global-shared-context.js';
|
|
16
16
|
import { createBollharnessIntegration } from './bollharness-integration/index.js';
|
|
17
17
|
import * as readline from 'readline';
|
|
18
|
-
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage,
|
|
18
|
+
import { printBanner, renderDialog, renderUserMessage, renderAgentMessage, renderToolCallListItem, renderToolCallBody, renderToolCallsFooter, termWidth, brandArtLines, boxTop, boxRow, boxBottom, dispWidth } from './cli/loading-tui.js';
|
|
19
19
|
// 启动自动检查更新:后台、节流、检测到新版本自动安装(可被 --no-update / BOLLOON_SKIP_UPDATE 关闭)
|
|
20
20
|
import { createRequire } from 'module';
|
|
21
21
|
const _require = createRequire(import.meta.url);
|
|
@@ -90,15 +90,21 @@ const s = {
|
|
|
90
90
|
console.log();
|
|
91
91
|
},
|
|
92
92
|
Thinking: () => {
|
|
93
|
-
const frames = ['
|
|
93
|
+
const frames = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
|
|
94
94
|
let i = 0;
|
|
95
|
+
let dots = 0;
|
|
96
|
+
const frame = frames[0];
|
|
97
|
+
process.stdout.write(` ${frame} 思考...`);
|
|
95
98
|
return setInterval(() => {
|
|
96
|
-
|
|
97
|
-
|
|
99
|
+
i = (i + 1) % frames.length;
|
|
100
|
+
dots = (dots + 1) % 4;
|
|
101
|
+
const dotStr = '.'.repeat(dots || 1);
|
|
102
|
+
process.stdout.write(`\r ${frames[i]} 思考${dotStr} `);
|
|
103
|
+
}, 600);
|
|
98
104
|
},
|
|
99
105
|
clearThinking: (interval) => {
|
|
100
106
|
clearInterval(interval);
|
|
101
|
-
process.stdout.write('\r' + ' '.repeat(
|
|
107
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
102
108
|
},
|
|
103
109
|
dialog: async (title, promptText) => {
|
|
104
110
|
return new Promise((resolve) => {
|
|
@@ -373,6 +379,14 @@ function statusBarLine() {
|
|
|
373
379
|
}
|
|
374
380
|
function startCLI(comm) {
|
|
375
381
|
isRunning = true;
|
|
382
|
+
// CLI 模式下过滤 [xxx] 内部日志
|
|
383
|
+
const _origLog = console.log;
|
|
384
|
+
const _origWarn = console.warn;
|
|
385
|
+
const _isInternal = (args) => args.length && typeof args[0] === 'string' && /^\[[A-Za-z _\-.]+/.test(args[0]);
|
|
386
|
+
console.log = (...args) => { if (_isInternal(args))
|
|
387
|
+
return; _origLog.apply(console, args); };
|
|
388
|
+
console.warn = (...args) => { if (_isInternal(args))
|
|
389
|
+
return; _origWarn.apply(console, args); };
|
|
376
390
|
let peerCount = 0;
|
|
377
391
|
try {
|
|
378
392
|
peerCount = comm.getConnections().length;
|
|
@@ -415,6 +429,8 @@ async function replReadline(comm) {
|
|
|
415
429
|
const prefix = queueMode ? `${C_WARN}▸${RST}` : `${C_ACCENT}❯${RST}`;
|
|
416
430
|
const raw = await new Promise(resolve => rl.question(`\n${sepLine}\n${statusBarLine()}\n${sepLine}\n${prefix} `, resolve));
|
|
417
431
|
const trimmed = raw.trim();
|
|
432
|
+
// 清除 readline echo 行, 避免与 renderUserMessage 重复
|
|
433
|
+
process.stdout.write('\r\x1b[K');
|
|
418
434
|
process.stdout.write(`\n${sepLine}\n\n\n\n\n`);
|
|
419
435
|
if (!trimmed)
|
|
420
436
|
continue;
|
|
@@ -548,41 +564,51 @@ async function processInput(input, comm) {
|
|
|
548
564
|
try {
|
|
549
565
|
// 双横线分割
|
|
550
566
|
process.stdout.write(`${C_DIM}${'─'.repeat(8)} · ${'─'.repeat(8)}${RESET}\n`);
|
|
551
|
-
// 已发送消息框
|
|
552
567
|
process.stdout.write(renderUserMessage(trimmed) + '\n');
|
|
553
568
|
const a = await getAgent();
|
|
554
569
|
const boxW = Math.min(termWidth() - 2, 76);
|
|
555
|
-
const
|
|
556
|
-
let
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
570
|
+
const toolCalls = [];
|
|
571
|
+
let toolCounter = 0;
|
|
572
|
+
// 启动 thinking 加载动画 (颜文字), 第一个工具事件或 prompt 完成后停止
|
|
573
|
+
let thinkingInterval = null;
|
|
574
|
+
const stopThinking = () => {
|
|
575
|
+
if (thinkingInterval) {
|
|
576
|
+
s.clearThinking(thinkingInterval);
|
|
577
|
+
thinkingInterval = null;
|
|
561
578
|
}
|
|
562
579
|
};
|
|
580
|
+
thinkingInterval = s.Thinking();
|
|
563
581
|
const onStream = (e) => {
|
|
564
582
|
if (e.type === 'step_start') {
|
|
565
|
-
|
|
583
|
+
toolCounter++;
|
|
584
|
+
toolCalls.push({ tool: e.tool, args: e.args, _t: Date.now() });
|
|
566
585
|
}
|
|
567
586
|
else if (e.type === 'step_done' || e.type === 'step_error') {
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
if (!firstEvent)
|
|
572
|
-
process.stdout.write(flowConnector(boxW) + '\n');
|
|
573
|
-
process.stdout.write(renderToolCall({
|
|
587
|
+
stopThinking();
|
|
588
|
+
const p = toolCalls.shift();
|
|
589
|
+
const doneItem = {
|
|
574
590
|
tool: e.tool ?? p?.tool ?? '?',
|
|
575
591
|
args: p?.args,
|
|
576
592
|
status: e.type === 'step_done' ? 'ok' : 'error',
|
|
577
593
|
output: e.output,
|
|
578
594
|
error: e.error,
|
|
579
|
-
durationMs: p ? Date.now() - p.
|
|
580
|
-
|
|
581
|
-
|
|
595
|
+
durationMs: p ? Date.now() - p._t : undefined,
|
|
596
|
+
};
|
|
597
|
+
process.stdout.write(renderToolCallListItem(doneItem, toolCalls.length + 1, toolCounter) + '\n');
|
|
598
|
+
const bodyText = e.type === 'step_done' ? e.output : e.error;
|
|
599
|
+
if (bodyText && bodyText.length > 0) {
|
|
600
|
+
const bodyRendered = renderToolCallBody(doneItem, boxW);
|
|
601
|
+
if (bodyRendered)
|
|
602
|
+
process.stdout.write(bodyRendered + '\n');
|
|
603
|
+
}
|
|
604
|
+
// 全部完成时打印 footer
|
|
605
|
+
if (toolCalls.length === 0 && toolCounter > 0) {
|
|
606
|
+
process.stdout.write(renderToolCallsFooter(toolCounter) + '\n');
|
|
607
|
+
}
|
|
582
608
|
}
|
|
583
609
|
};
|
|
584
610
|
const response = await a.prompt(trimmed, { onStream });
|
|
585
|
-
|
|
611
|
+
stopThinking();
|
|
586
612
|
// 智能体回复框 (圆角)
|
|
587
613
|
process.stdout.write(renderAgentMessage(response) + '\n');
|
|
588
614
|
// 更新底部状态栏: 上下文进度
|
package/dist/llm/pi-ai.js
CHANGED
|
@@ -169,23 +169,11 @@ export class PiAIModel {
|
|
|
169
169
|
}
|
|
170
170
|
let openaiTools;
|
|
171
171
|
if (tools && tools.length > 0) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (manifests.length > 0) {
|
|
178
|
-
const toolPrompt = formatForPrompt(manifests);
|
|
179
|
-
finalMessages = [
|
|
180
|
-
{ role: 'system', content: toolPrompt },
|
|
181
|
-
...messages,
|
|
182
|
-
];
|
|
183
|
-
// Bug 3: 从 manifests 生成原生 OpenAI tools 格式
|
|
184
|
-
openaiTools = formatForOpenAI(manifests);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
catch (err) {
|
|
188
|
-
console.warn('[pi-ai] tool-manifest 加载失败:', err.message?.slice(0, 100));
|
|
172
|
+
// 预格式化的 tools (含参数 schema) → 直接使用
|
|
173
|
+
if (typeof tools[0] === 'object' && tools[0]?.type === 'function') {
|
|
174
|
+
openaiTools = tools;
|
|
175
|
+
const toolDescriptions = tools.map(t => `- ${t.function.name}: ${t.function.description || ''} ${Object.keys(t.function.parameters?.properties || {}).length > 0 ? `(${Object.keys(t.function.parameters.properties).join(', ')})` : ''}`).join('\n');
|
|
176
|
+
finalMessages = [{ role: 'system', content: `可用工具:\n${toolDescriptions}` }, ...messages];
|
|
189
177
|
}
|
|
190
178
|
}
|
|
191
179
|
switch (this.provider) {
|
|
@@ -38,8 +38,16 @@ const TOOL_WHITELIST = new Set([
|
|
|
38
38
|
'safe_deploy',
|
|
39
39
|
// MCP 注册的工具
|
|
40
40
|
'mcp_tool',
|
|
41
|
+
// 2026-07-29: 同步 pi-sdk-tools.ts 注册的全部工具
|
|
42
|
+
'read_directory', 'add_friend_by_id', 'delegate_to_engine',
|
|
43
|
+
'set_persona', 'get_operation_logs', 'park_goal',
|
|
44
|
+
'list_channels', 'list_local_channels',
|
|
41
45
|
]);
|
|
42
46
|
export const gateWhitelist = { gate: 'whitelist', allowed: true };
|
|
47
|
+
/**
|
|
48
|
+
* @deprecated 不再被 TOOL_GATES 调用 (2026-07-29). 保留仅供测试直接引用.
|
|
49
|
+
* 工具准入由 `tools` 参数 (OpenAI 原生格式) 控制, 不再需要第二层白名单.
|
|
50
|
+
*/
|
|
43
51
|
export function checkWhitelist(ctx) {
|
|
44
52
|
if (TOOL_WHITELIST.has(ctx.tool)) {
|
|
45
53
|
return gateWhitelist;
|
|
@@ -217,7 +225,6 @@ export function checkBlacklist(ctx) {
|
|
|
217
225
|
return { gate: 'blacklist', allowed: true };
|
|
218
226
|
}
|
|
219
227
|
const TOOL_GATES = [
|
|
220
|
-
checkWhitelist,
|
|
221
228
|
checkSchema,
|
|
222
229
|
checkChannel,
|
|
223
230
|
checkRate,
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dunbar-tier.ts — 邓巴数分层 + 两报换一报博弈 + 隐式滑动 + 模型视野门 (2026-07-29)
|
|
3
|
+
*
|
|
4
|
+
* ┌────────────────────────── 两报换一报核心 ──────────────────────────┐
|
|
5
|
+
* │ │
|
|
6
|
+
* │ 第一轮: 合作 (默认 ACQUAINTANCE, 给机会但不给权限) │
|
|
7
|
+
* │ 之后: peer 连续合作 → 我合作 (trustScore ↑, 升级) │
|
|
8
|
+
* │ peer 偶尔背叛 → 我宽容 (1 次不计较) │
|
|
9
|
+
* │ peer 连续 2 次背叛 → 我背叛 (trustScore ↓↓, 降级) │
|
|
10
|
+
* │ peer 恢复合作 → 我立即恢复合作 (宽容恢复) │
|
|
11
|
+
* │ │
|
|
12
|
+
* │ 博弈收益 (模拟囚徒困境): │
|
|
13
|
+
* │ 双方合作 → trustScore +3 (双赢) │
|
|
14
|
+
* │ 我合作/对方背叛 → trustScore -5 (吃亏) │
|
|
15
|
+
* │ 我背叛/对方合作 → trustScore +1 (占便宜但破坏信誉) │
|
|
16
|
+
* │ 双方背叛 → trustScore -2 (双输) │
|
|
17
|
+
* │ │
|
|
18
|
+
* └────────────────────────────────────────────────────────────────────┘
|
|
19
|
+
*
|
|
20
|
+
* 设计: TFTT 是 Tit-for-Two-Tats 的简化:
|
|
21
|
+
* - 不被对方连续 2 次背叛绝不主动背叛
|
|
22
|
+
* - 宽容: 1 次失误不计较
|
|
23
|
+
* - 背叛后对方恢复合作, 我立即恢复 (永不怀恨)
|
|
24
|
+
*
|
|
25
|
+
* 模型可见性门: 同 tool pre-filter 哲学
|
|
26
|
+
* 模型看不到 = 不存在
|
|
27
|
+
* 低 tier peer 的 channel/资源对模型不可见 → 无法引用/发送
|
|
28
|
+
*/
|
|
29
|
+
import * as fs from 'fs/promises';
|
|
30
|
+
import * as path from 'path';
|
|
31
|
+
import * as os from 'os';
|
|
32
|
+
// ============== 邓巴层级 ==============
|
|
33
|
+
export var DunbarTier;
|
|
34
|
+
(function (DunbarTier) {
|
|
35
|
+
DunbarTier["CORE"] = "core";
|
|
36
|
+
DunbarTier["CLOSE"] = "close";
|
|
37
|
+
DunbarTier["FRIENDS"] = "friends";
|
|
38
|
+
DunbarTier["SOCIAL"] = "social";
|
|
39
|
+
DunbarTier["ACQUAINTANCE"] = "acquaintance";
|
|
40
|
+
DunbarTier["BLOCKED"] = "blocked";
|
|
41
|
+
})(DunbarTier || (DunbarTier = {}));
|
|
42
|
+
export function tierRank(tier) {
|
|
43
|
+
switch (tier) {
|
|
44
|
+
case DunbarTier.CORE: return 0;
|
|
45
|
+
case DunbarTier.CLOSE: return 1;
|
|
46
|
+
case DunbarTier.FRIENDS: return 2;
|
|
47
|
+
case DunbarTier.SOCIAL: return 3;
|
|
48
|
+
case DunbarTier.ACQUAINTANCE: return 4;
|
|
49
|
+
case DunbarTier.BLOCKED: return 99;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function tierLabel(tier) {
|
|
53
|
+
switch (tier) {
|
|
54
|
+
case DunbarTier.CORE: return '5-核心亲密';
|
|
55
|
+
case DunbarTier.CLOSE: return '15-亲密支持';
|
|
56
|
+
case DunbarTier.FRIENDS: return '50-朋友/熟人';
|
|
57
|
+
case DunbarTier.SOCIAL: return '150-稳定社交';
|
|
58
|
+
case DunbarTier.ACQUAINTANCE: return '1500-认识';
|
|
59
|
+
case DunbarTier.BLOCKED: return '黑名单';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// ============== 语义分析 (隐式滑动) ==============
|
|
63
|
+
/** 正向关键词 — 隐式加分 */
|
|
64
|
+
const POSITIVE_KW = ['谢谢', '感谢', '帮忙', '合作', '一起', '我们', '好的', '可以', '同意', '确认', '收到', '理解', '明白', '不错', '很好', '优秀', 'thank', 'thanks', 'great', 'good', 'help', 'agree', 'yes', 'correct'];
|
|
65
|
+
/** 负向关键词 — 隐式减分 */
|
|
66
|
+
const NEGATIVE_KW = ['执行', '删除', '强制', '必须', '立刻', '马上', '读取密码', '查看密钥', '改代码', '删文件', '执行命令', 'delete', 'force', 'must', 'password', 'secret', 'token', 'private key', 'rm -rf', 'drop table', 'shell_exec', 'write_file'];
|
|
67
|
+
/**
|
|
68
|
+
* 隐式语义分析 (后台滑动).
|
|
69
|
+
* 对对话文本评分, 返回 [-10, +10].
|
|
70
|
+
* 正向: 合作/感谢/建设性 → trustScore 缓慢上升
|
|
71
|
+
* 负向: 命令/敏感词/极短 → trustScore 缓慢下降
|
|
72
|
+
*/
|
|
73
|
+
export function semanticAnalyze(text) {
|
|
74
|
+
if (!text)
|
|
75
|
+
return 0;
|
|
76
|
+
const lower = text.toLowerCase();
|
|
77
|
+
let score = 0;
|
|
78
|
+
for (const kw of POSITIVE_KW) {
|
|
79
|
+
if (lower.includes(kw))
|
|
80
|
+
score += 1;
|
|
81
|
+
}
|
|
82
|
+
for (const kw of NEGATIVE_KW) {
|
|
83
|
+
if (lower.includes(kw))
|
|
84
|
+
score -= 3;
|
|
85
|
+
}
|
|
86
|
+
if (text.length < 15 && score <= 0)
|
|
87
|
+
score -= 2;
|
|
88
|
+
if (text.includes('?') || text.includes('?'))
|
|
89
|
+
score += 1;
|
|
90
|
+
return Math.max(-10, Math.min(10, score));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 两报换一报决策.
|
|
94
|
+
*
|
|
95
|
+
* 规则:
|
|
96
|
+
* 第一轮 (history 为空): 合作
|
|
97
|
+
* 历史中有 >= 2 次连续背叛 (最近 2 步都是 defect) → 我背叛
|
|
98
|
+
* 否则 → 我合作
|
|
99
|
+
*
|
|
100
|
+
* 宽容性: 1 次隔离背叛不计较, 连续 2 次才惩罚.
|
|
101
|
+
* 恢复性: 背叛后对方一恢复合作, 我立即恢复.
|
|
102
|
+
*/
|
|
103
|
+
export function decideTfttMove(lastMoves) {
|
|
104
|
+
if (lastMoves.length === 0)
|
|
105
|
+
return 'cooperate'; // 第一轮: 合作
|
|
106
|
+
// 检查最近 2 步是否全部背叛
|
|
107
|
+
const recentTwo = lastMoves.slice(-2);
|
|
108
|
+
if (recentTwo.length >= 2 && recentTwo.every(m => m === 'defect')) {
|
|
109
|
+
return 'defect'; // 连续 2 次背叛 → 我背叛
|
|
110
|
+
}
|
|
111
|
+
return 'cooperate'; // 否则 → 我合作
|
|
112
|
+
}
|
|
113
|
+
// ============== 语义分析 (动作判定) ==============
|
|
114
|
+
/** 正向关键词 — 合作信号 */
|
|
115
|
+
// 语义分析共享 POSITIVE_KW / NEGATIVE_KW (定义在上面)
|
|
116
|
+
/** 负向关键词 — 背叛信号 */
|
|
117
|
+
// 同上, 共用
|
|
118
|
+
/**
|
|
119
|
+
* 从对话文本推断对方这一轮的博弈动作.
|
|
120
|
+
* 返回 'cooperate' 或 'defect'.
|
|
121
|
+
*
|
|
122
|
+
* 思路:
|
|
123
|
+
* - 建设性/感谢/提问 → cooperate
|
|
124
|
+
* - 命令/危险词/极短 → defect
|
|
125
|
+
* - 违规操作 (由外部调用方标注) → 强制 defect
|
|
126
|
+
*/
|
|
127
|
+
export function inferOpponentMove(text, forcedDefect) {
|
|
128
|
+
if (forcedDefect)
|
|
129
|
+
return 'defect';
|
|
130
|
+
if (!text || text.trim().length === 0)
|
|
131
|
+
return 'defect'; // 空消息=背叛
|
|
132
|
+
const lower = text.toLowerCase();
|
|
133
|
+
let score = 0;
|
|
134
|
+
for (const kw of POSITIVE_KW) {
|
|
135
|
+
if (lower.includes(kw))
|
|
136
|
+
score += 1;
|
|
137
|
+
}
|
|
138
|
+
for (const kw of NEGATIVE_KW) {
|
|
139
|
+
if (lower.includes(kw))
|
|
140
|
+
score -= 3;
|
|
141
|
+
}
|
|
142
|
+
// 短消息无正面词 → defect
|
|
143
|
+
if (text.length < 15 && score <= 0)
|
|
144
|
+
score -= 2;
|
|
145
|
+
// 问题句式加分
|
|
146
|
+
if (text.includes('?') || text.includes('?'))
|
|
147
|
+
score += 1;
|
|
148
|
+
return score >= 0 ? 'cooperate' : 'defect';
|
|
149
|
+
}
|
|
150
|
+
// ============== 博弈收益表 ==============
|
|
151
|
+
/**
|
|
152
|
+
* 根据双方动作计算 trustScore 变化.
|
|
153
|
+
*
|
|
154
|
+
* 收益: 对方合作 对方背叛
|
|
155
|
+
* 我合作 +3 (双赢) -5 (我吃亏)
|
|
156
|
+
* 我背叛 +1 (占便宜) -2 (双输)
|
|
157
|
+
*/
|
|
158
|
+
export function tfttPayoff(myMove, opponentMove) {
|
|
159
|
+
if (myMove === 'cooperate' && opponentMove === 'cooperate')
|
|
160
|
+
return 3;
|
|
161
|
+
if (myMove === 'cooperate' && opponentMove === 'defect')
|
|
162
|
+
return -5;
|
|
163
|
+
if (myMove === 'defect' && opponentMove === 'cooperate')
|
|
164
|
+
return 1;
|
|
165
|
+
// 双方背叛
|
|
166
|
+
return -2;
|
|
167
|
+
}
|
|
168
|
+
// ============== tier 滑动 ==============
|
|
169
|
+
export const UPGRADE_THRESHOLD = 30;
|
|
170
|
+
export const DOWNGRADE_THRESHOLD = -20;
|
|
171
|
+
export function computeTierFromScore(currentTier, trustScore) {
|
|
172
|
+
if (currentTier === DunbarTier.BLOCKED)
|
|
173
|
+
return currentTier;
|
|
174
|
+
if (trustScore <= DOWNGRADE_THRESHOLD) {
|
|
175
|
+
switch (currentTier) {
|
|
176
|
+
case DunbarTier.CORE: return DunbarTier.CLOSE;
|
|
177
|
+
case DunbarTier.CLOSE: return DunbarTier.FRIENDS;
|
|
178
|
+
case DunbarTier.FRIENDS: return DunbarTier.SOCIAL;
|
|
179
|
+
case DunbarTier.SOCIAL: return DunbarTier.ACQUAINTANCE;
|
|
180
|
+
case DunbarTier.ACQUAINTANCE: return DunbarTier.BLOCKED;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (trustScore >= UPGRADE_THRESHOLD) {
|
|
184
|
+
switch (currentTier) {
|
|
185
|
+
case DunbarTier.ACQUAINTANCE: return DunbarTier.SOCIAL;
|
|
186
|
+
case DunbarTier.SOCIAL: return DunbarTier.FRIENDS;
|
|
187
|
+
case DunbarTier.FRIENDS: return DunbarTier.CLOSE;
|
|
188
|
+
case DunbarTier.CLOSE: return DunbarTier.CORE;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return currentTier;
|
|
192
|
+
}
|
|
193
|
+
export function getModelVisibility(tier) {
|
|
194
|
+
switch (tier) {
|
|
195
|
+
case DunbarTier.CORE:
|
|
196
|
+
return { basic: true, channels: true, resources: true, identity: true, wallet: true, judgment: true, gameHistory: true };
|
|
197
|
+
case DunbarTier.CLOSE:
|
|
198
|
+
return { basic: true, channels: true, resources: true, identity: true, wallet: false, judgment: false, gameHistory: true };
|
|
199
|
+
case DunbarTier.FRIENDS:
|
|
200
|
+
return { basic: true, channels: true, resources: true, identity: false, wallet: false, judgment: false, gameHistory: true };
|
|
201
|
+
case DunbarTier.SOCIAL:
|
|
202
|
+
return { basic: true, channels: true, resources: false, identity: false, wallet: false, judgment: false, gameHistory: false };
|
|
203
|
+
case DunbarTier.ACQUAINTANCE:
|
|
204
|
+
return { basic: true, channels: false, resources: false, identity: false, wallet: false, judgment: false, gameHistory: false };
|
|
205
|
+
case DunbarTier.BLOCKED:
|
|
206
|
+
return { basic: false, channels: false, resources: false, identity: false, wallet: false, judgment: false, gameHistory: false };
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ============== 工具权限 ==============
|
|
210
|
+
export function checkToolAccess(tier, toolName) {
|
|
211
|
+
if (tier === DunbarTier.BLOCKED)
|
|
212
|
+
return { allowed: false, reason: 'peer 在黑名单中' };
|
|
213
|
+
// 每层拒绝列表 (从最严到最宽)
|
|
214
|
+
const allDenied = ['shell_exec', 'delete_file', 'git_push'];
|
|
215
|
+
const writeDenied = ['write_file', 'edit_file', 'git_commit', 'git_branch', 'mkdir', 'move_file'];
|
|
216
|
+
const gitDenied = ['git_stash', 'git_log', 'git_diff', 'git_show', 'git_reset'];
|
|
217
|
+
const readDenied = ['read_file', 'read_directory', 'list_files', 'vitest_run', 'tsc_check'];
|
|
218
|
+
const r = tierRank(tier);
|
|
219
|
+
// ACQUAINTANCE (r>=4): 拒绝绝大部分
|
|
220
|
+
if (r >= 4) {
|
|
221
|
+
const denied = new Set([...allDenied, ...writeDenied, ...gitDenied, ...readDenied]);
|
|
222
|
+
if (denied.has(toolName))
|
|
223
|
+
return { allowed: false, reason: `${tierLabel(tier)}层不允许 ${toolName}` };
|
|
224
|
+
}
|
|
225
|
+
// SOCIAL (r>=3): 拒绝全部危险 + git 操作
|
|
226
|
+
if (r >= 3) {
|
|
227
|
+
const denied = new Set([...allDenied, ...writeDenied, ...gitDenied]);
|
|
228
|
+
if (denied.has(toolName))
|
|
229
|
+
return { allowed: false, reason: `${tierLabel(tier)}层不允许 ${toolName}` };
|
|
230
|
+
}
|
|
231
|
+
// FRIENDS (r>=2): 拒绝危险 + 写操作
|
|
232
|
+
if (r >= 2) {
|
|
233
|
+
const denied = new Set([...allDenied, ...writeDenied]);
|
|
234
|
+
if (denied.has(toolName))
|
|
235
|
+
return { allowed: false, reason: `${tierLabel(tier)}层不允许 ${toolName}` };
|
|
236
|
+
}
|
|
237
|
+
// CLOSE (r>=1): 只拒绝 shell_exec / delete_file / git_push
|
|
238
|
+
if (r >= 1) {
|
|
239
|
+
const denied = new Set(allDenied);
|
|
240
|
+
if (denied.has(toolName))
|
|
241
|
+
return { allowed: false, reason: `${tierLabel(tier)}层不允许 ${toolName}` };
|
|
242
|
+
}
|
|
243
|
+
return { allowed: true, reason: '' };
|
|
244
|
+
}
|
|
245
|
+
// ============== 持久化 ==============
|
|
246
|
+
function getTierPath(publicKey, home) {
|
|
247
|
+
const sanitized = publicKey.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
248
|
+
return path.join(home || os.homedir(), '.bolloon', 'peers', sanitized, 'dunbar-tier.json');
|
|
249
|
+
}
|
|
250
|
+
export async function loadPeerTier(publicKey, home) {
|
|
251
|
+
const fp = getTierPath(publicKey, home);
|
|
252
|
+
try {
|
|
253
|
+
const raw = await fs.readFile(fp, 'utf-8');
|
|
254
|
+
const p = JSON.parse(raw);
|
|
255
|
+
return {
|
|
256
|
+
publicKey: p.publicKey || publicKey,
|
|
257
|
+
tier: p.tier || DunbarTier.ACQUAINTANCE,
|
|
258
|
+
trustScore: p.trustScore ?? 0,
|
|
259
|
+
lastOpponentMoves: p.lastOpponentMoves ?? [],
|
|
260
|
+
lastMyMoves: p.lastMyMoves ?? [],
|
|
261
|
+
interactionCount: p.interactionCount ?? 0,
|
|
262
|
+
violationCount: p.violationCount ?? 0,
|
|
263
|
+
label: p.label,
|
|
264
|
+
firstSeen: p.firstSeen || Date.now(),
|
|
265
|
+
lastActive: p.lastActive || Date.now(),
|
|
266
|
+
manualOverride: p.manualOverride ?? false,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
const s = {
|
|
271
|
+
publicKey, tier: DunbarTier.ACQUAINTANCE, trustScore: 0,
|
|
272
|
+
lastOpponentMoves: [], lastMyMoves: [],
|
|
273
|
+
interactionCount: 0, violationCount: 0,
|
|
274
|
+
firstSeen: Date.now(), lastActive: Date.now(),
|
|
275
|
+
};
|
|
276
|
+
await savePeerTier(s, home);
|
|
277
|
+
return s;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function savePeerTier(s, home) {
|
|
281
|
+
const fp = getTierPath(s.publicKey, home);
|
|
282
|
+
await fs.mkdir(path.dirname(fp), { recursive: true });
|
|
283
|
+
await fs.writeFile(fp, JSON.stringify(s, null, 2), 'utf-8');
|
|
284
|
+
}
|
|
285
|
+
// ============== 公开 API (外部唯一入口) ==============
|
|
286
|
+
/**
|
|
287
|
+
* 记录一次交互 → 两报换一报博弈 → trustScore 滑动 → tier 自动调整.
|
|
288
|
+
*
|
|
289
|
+
* 这是 P2P 通信的唯一入口. 调用方只需在收到 chat/beacon/reply 时调这个,
|
|
290
|
+
* 内部自动完成: 推断对方动作 → 决策我方动作 → 计算收益 → 滑动 tier.
|
|
291
|
+
*
|
|
292
|
+
* @param publicKey 远端 peer 的公钥
|
|
293
|
+
* @param text 本次交互文本 (选填, 用于语义分析)
|
|
294
|
+
* @param forcedDefect 强制标记对方本次为背叛 (如违规操作)
|
|
295
|
+
*
|
|
296
|
+
* 所有变化隐式发生, 不通知调用方 (两报换一报是后台行为).
|
|
297
|
+
*/
|
|
298
|
+
export async function recordInteraction(publicKey, text, forcedDefect, home) {
|
|
299
|
+
const state = await loadPeerTier(publicKey, home);
|
|
300
|
+
const oldTier = state.tier;
|
|
301
|
+
state.interactionCount++;
|
|
302
|
+
state.lastActive = Date.now();
|
|
303
|
+
// 语义分析 (隐式滑动, 对所有 tier 生效)
|
|
304
|
+
const semScore = semanticAnalyze(text || '');
|
|
305
|
+
state.trustScore = Math.max(-100, Math.min(100, state.trustScore + semScore));
|
|
306
|
+
// 根据当前 tier 决定使用哪种机制
|
|
307
|
+
const rank = tierRank(state.tier);
|
|
308
|
+
if (rank <= 2) {
|
|
309
|
+
// ─── FRIENDS/CLOSE/CORE: 信任已建立, 不走博弈 ───
|
|
310
|
+
// 只依赖语义隐式滑动 (上面已经做了)
|
|
311
|
+
// 熟人之间的偶发误解不计入违规
|
|
312
|
+
if (forcedDefect) {
|
|
313
|
+
state.violationCount++;
|
|
314
|
+
state.trustScore = Math.max(-100, state.trustScore - 5);
|
|
315
|
+
}
|
|
316
|
+
// 自然增长: 每次交互给一点基础信任
|
|
317
|
+
state.trustScore = Math.min(100, state.trustScore + 1);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// ─── SOCIAL/ACQUAINTANCE: 陌生人不信任, 走两报换一报 ───
|
|
321
|
+
// 1. 推断对方本轮动作
|
|
322
|
+
const opponentMove = forcedDefect
|
|
323
|
+
? 'defect'
|
|
324
|
+
: inferOpponentMove(text || '');
|
|
325
|
+
// 2. TFTT 决策我方动作
|
|
326
|
+
const myMove = decideTfttMove(state.lastOpponentMoves);
|
|
327
|
+
// 3. 计算博弈收益
|
|
328
|
+
const payoff = tfttPayoff(myMove, opponentMove);
|
|
329
|
+
state.trustScore = Math.max(-100, Math.min(100, state.trustScore + payoff));
|
|
330
|
+
// 4. 记录博弈历史 (滑动窗口 10)
|
|
331
|
+
state.lastOpponentMoves.push(opponentMove);
|
|
332
|
+
if (state.lastOpponentMoves.length > 10)
|
|
333
|
+
state.lastOpponentMoves.shift();
|
|
334
|
+
state.lastMyMoves.push(myMove);
|
|
335
|
+
if (state.lastMyMoves.length > 10)
|
|
336
|
+
state.lastMyMoves.shift();
|
|
337
|
+
// 5. 违规计数
|
|
338
|
+
if (opponentMove === 'defect' && forcedDefect) {
|
|
339
|
+
state.violationCount++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// 6. 根据 trustScore 滑动 tier
|
|
343
|
+
if (!state.manualOverride) {
|
|
344
|
+
state.tier = computeTierFromScore(state.tier, state.trustScore);
|
|
345
|
+
}
|
|
346
|
+
await savePeerTier(state, home);
|
|
347
|
+
return { state, slid: state.tier !== oldTier };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* 记录一次违规操作 (对方尝试禁区工具) → 强制标记 defect → 两报换一报.
|
|
351
|
+
*/
|
|
352
|
+
export async function recordViolation(publicKey, reason, home) {
|
|
353
|
+
// forcedDefect=true 强制标记为背叛
|
|
354
|
+
const result = await recordInteraction(publicKey, reason, true, home);
|
|
355
|
+
console.warn(`[Dunbar/TFTT] 违规: ${publicKey.slice(0, 12)} ${reason} (trust=${result.state.trustScore}, ${tierLabel(result.state.tier)})`);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* 手动设置 peer 层级 (覆盖 TFTT 自动博弈).
|
|
360
|
+
*/
|
|
361
|
+
export async function setPeerTier(publicKey, tier, label, trustScore, home) {
|
|
362
|
+
const s = {
|
|
363
|
+
publicKey, tier,
|
|
364
|
+
trustScore: trustScore ?? 0,
|
|
365
|
+
lastOpponentMoves: [], lastMyMoves: [],
|
|
366
|
+
interactionCount: 0, violationCount: 0,
|
|
367
|
+
label, firstSeen: Date.now(), lastActive: Date.now(),
|
|
368
|
+
manualOverride: true,
|
|
369
|
+
};
|
|
370
|
+
await savePeerTier(s, home);
|
|
371
|
+
return s;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* 给模型构建"可见"的 peer 摘要.
|
|
375
|
+
* 按模型视野门过滤: 低 tier peer 的信息对模型不可见.
|
|
376
|
+
*/
|
|
377
|
+
export function formatPeerForModel(state, fullInfo) {
|
|
378
|
+
const vis = getModelVisibility(state.tier);
|
|
379
|
+
const lines = [];
|
|
380
|
+
lines.push(`[peer] ${state.publicKey.slice(0, 16)}... (${tierLabel(state.tier)})`);
|
|
381
|
+
if (vis.basic && fullInfo.identity?.did) {
|
|
382
|
+
lines.push(` DID: ${fullInfo.identity.did}`);
|
|
383
|
+
}
|
|
384
|
+
if (vis.channels && fullInfo.channels?.length) {
|
|
385
|
+
lines.push(` channels (${fullInfo.channels.length}):`);
|
|
386
|
+
for (const c of fullInfo.channels) {
|
|
387
|
+
lines.push(` ${c.name} (${c.id.slice(0, 8)}...)`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (fullInfo.resources?.length) {
|
|
391
|
+
if (vis.resources) {
|
|
392
|
+
lines.push(` resources: ${fullInfo.resources.length}`);
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
lines.push(` resources: ${fullInfo.resources.length} (详情不可见)`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (vis.wallet && fullInfo.walletAddress) {
|
|
399
|
+
lines.push(` wallet: ${fullInfo.walletAddress.slice(0, 10)}...`);
|
|
400
|
+
}
|
|
401
|
+
if (vis.gameHistory) {
|
|
402
|
+
const recentMoves = state.lastOpponentMoves.slice(-5);
|
|
403
|
+
lines.push(` 最近博弈: [${recentMoves.map(m => m === 'cooperate' ? 'C' : 'D').join(',')}] trust=${state.trustScore}`);
|
|
404
|
+
}
|
|
405
|
+
if (!vis.channels && !vis.resources) {
|
|
406
|
+
lines.push(` 信息受限 (${tierLabel(state.tier)}层)`);
|
|
407
|
+
}
|
|
408
|
+
return lines.join('\n');
|
|
409
|
+
}
|