@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/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, renderToolCall, flowConnector, termWidth, brandArtLines, boxTop, boxRow, boxBottom, dispWidth } from './cli/loading-tui.js';
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
- process.stdout.write(`\r ${YELLOW}${frames[i++ % frames.length]} 思考中...${RESET} `);
97
- }, 80);
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(30) + '\r');
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 pending = [];
556
- let firstEvent = true;
557
- const clearThinking = () => {
558
- if (firstEvent) {
559
- process.stdout.write('\r' + ' '.repeat(30) + '\r');
560
- firstEvent = false;
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
- pending.push({ tool: e.tool, args: e.args, t0: Date.now() });
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
- const p = pending.shift();
569
- clearThinking();
570
- // 连接线只在第 2 个及之后的工具框前出现
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.t0 : undefined,
580
- width: boxW,
581
- }) + '\n');
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
- clearThinking();
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
- try {
173
- const { getToolManifest, formatForPrompt, formatForOpenAI } = await import('./tool-manifest/index.js');
174
- const manifests = tools
175
- .map((id) => getToolManifest(id))
176
- .filter((m) => m !== undefined);
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
+ }