@bolloon/bolloon-agent 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +7 -2
  2. package/dist/agents/pi-sdk.js +10 -1
  3. package/dist/bollharness-integration/index.js +8 -1
  4. package/dist/heartbeat/Watchdog.js +9 -1
  5. package/dist/llm/audio-config-store.js +199 -0
  6. package/dist/llm/config-store.js +20 -10
  7. package/dist/llm/pi-ai.js +2 -2
  8. package/dist/llm/video-config-store.js +31 -1
  9. package/dist/network/p2p-direct.js +59 -2
  10. package/dist/pi-ecosystem/index.js +10 -7
  11. package/dist/pi-ecosystem-judgment/decision.js +5 -2
  12. package/dist/social/heartbeat.js +19 -2
  13. package/dist/web/api-config.html +16 -4
  14. package/dist/web/client.js +1017 -137
  15. package/dist/web/index.html +10 -27
  16. package/dist/web/server.js +865 -52
  17. package/dist/web/style.css +370 -0
  18. package/package.json +2 -1
  19. package/src/agents/pi-sdk.ts +9 -1
  20. package/src/bollharness-integration/index.ts +8 -32
  21. package/src/heartbeat/Watchdog.ts +9 -1
  22. package/src/llm/audio-config-store.ts +6 -1
  23. package/src/llm/config-store.ts +21 -11
  24. package/src/llm/pi-ai.ts +2 -2
  25. package/src/llm/video-config-store.ts +7 -1
  26. package/src/network/p2p-direct.ts +59 -3
  27. package/src/social/ant-colony/index.js +19 -0
  28. package/src/social/heartbeat.ts +18 -2
  29. package/src/web/api-config.html +16 -4
  30. package/src/web/client.js +1017 -137
  31. package/src/web/index.html +10 -27
  32. package/src/web/server.ts +810 -47
  33. package/src/web/style.css +370 -0
  34. package/src/social/ant-colony/AdaptiveHeartbeat.ts +0 -131
  35. package/src/social/ant-colony/PheromoneEngine.ts +0 -302
  36. package/src/social/ant-colony/index.ts +0 -18
  37. package/src/social/ant-colony/types.ts +0 -94
@@ -4083,3 +4083,373 @@ body:has(> .api-config-page) {
4083
4083
  cursor: pointer;
4084
4084
  accent-color: var(--accent);
4085
4085
  }
4086
+
4087
+ /* ============================================================
4088
+ * 2026-06-10: 远程聊天 modal — 完全对齐本地聊天风格
4089
+ * 用 CSS 变量, 字体继承全局 JetBrains Mono, 支持深/浅色主题切换
4090
+ * ============================================================ */
4091
+ .remote-chat-overlay {
4092
+ position: fixed;
4093
+ inset: 0;
4094
+ background: rgba(0, 0, 0, 0.55);
4095
+ z-index: 10002;
4096
+ display: flex;
4097
+ align-items: center;
4098
+ justify-content: center;
4099
+ }
4100
+ .remote-chat-shell {
4101
+ background: var(--bg-main);
4102
+ color: var(--text);
4103
+ border: 1px solid var(--border);
4104
+ border-radius: 8px;
4105
+ width: 720px;
4106
+ max-width: 92vw;
4107
+ height: 80vh;
4108
+ max-height: 80vh;
4109
+ display: flex;
4110
+ flex-direction: column;
4111
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
4112
+ font-family: inherit;
4113
+ }
4114
+ .remote-chat-header {
4115
+ padding: 12px 16px;
4116
+ border-bottom: 1px solid var(--border);
4117
+ display: flex;
4118
+ align-items: center;
4119
+ justify-content: space-between;
4120
+ gap: 8px;
4121
+ background: var(--bg-sidebar);
4122
+ border-radius: 8px 8px 0 0;
4123
+ }
4124
+ .remote-chat-title {
4125
+ font-size: 15px;
4126
+ font-weight: 600;
4127
+ color: var(--text);
4128
+ }
4129
+ .remote-chat-meta {
4130
+ font-size: 11px;
4131
+ color: var(--text-muted);
4132
+ margin-top: 2px;
4133
+ overflow: hidden;
4134
+ text-overflow: ellipsis;
4135
+ white-space: nowrap;
4136
+ }
4137
+ .remote-chat-btn-secondary {
4138
+ background: var(--bg-hover);
4139
+ border: 1px solid var(--border);
4140
+ color: var(--text-secondary);
4141
+ cursor: pointer;
4142
+ padding: 4px 10px;
4143
+ border-radius: 4px;
4144
+ font-size: 11px;
4145
+ font-family: inherit;
4146
+ }
4147
+ .remote-chat-btn-secondary:hover {
4148
+ background: var(--bg-active);
4149
+ color: var(--text);
4150
+ }
4151
+ .remote-chat-btn-close {
4152
+ background: none;
4153
+ border: none;
4154
+ font-size: 20px;
4155
+ color: var(--text-muted);
4156
+ cursor: pointer;
4157
+ padding: 0 8px;
4158
+ line-height: 1;
4159
+ }
4160
+ .remote-chat-btn-close:hover {
4161
+ color: var(--text);
4162
+ }
4163
+ .remote-chat-thinking {
4164
+ padding: 8px 16px;
4165
+ background: var(--bg-hover);
4166
+ color: var(--text-secondary);
4167
+ font-size: 12px;
4168
+ border-bottom: 1px solid var(--border);
4169
+ }
4170
+ .remote-chat-log {
4171
+ flex: 1;
4172
+ overflow-y: auto;
4173
+ padding: 12px 16px;
4174
+ background: var(--bg-main);
4175
+ }
4176
+ .remote-chat-input-row {
4177
+ padding: 10px 12px;
4178
+ border-top: 1px solid var(--border);
4179
+ display: flex;
4180
+ gap: 6px;
4181
+ background: var(--bg-sidebar);
4182
+ border-radius: 0 0 8px 8px;
4183
+ }
4184
+ .remote-chat-input {
4185
+ flex: 1;
4186
+ padding: 8px 10px;
4187
+ border: 1px solid var(--border);
4188
+ border-radius: 4px;
4189
+ font-size: 13px;
4190
+ background: var(--bg-main);
4191
+ color: var(--text);
4192
+ font-family: inherit;
4193
+ }
4194
+ .remote-chat-input:focus {
4195
+ outline: none;
4196
+ border-color: var(--accent, #a4b630);
4197
+ }
4198
+ .remote-chat-btn-send {
4199
+ padding: 8px 14px;
4200
+ background: var(--user-bg);
4201
+ color: var(--user-text);
4202
+ border: none;
4203
+ border-radius: 4px;
4204
+ cursor: pointer;
4205
+ font-size: 13px;
4206
+ font-weight: 600;
4207
+ font-family: inherit;
4208
+ }
4209
+ .remote-chat-btn-send:hover:not(:disabled) {
4210
+ filter: brightness(1.1);
4211
+ }
4212
+ .remote-chat-btn-send:disabled {
4213
+ opacity: 0.5;
4214
+ cursor: wait;
4215
+ }
4216
+ .remote-chat-sysmsg {
4217
+ margin: 6px 0;
4218
+ padding: 6px 10px;
4219
+ border-radius: 4px;
4220
+ font-size: 11px;
4221
+ text-align: center;
4222
+ border: 1px solid var(--border);
4223
+ }
4224
+ .remote-chat-sysmsg-info {
4225
+ background: var(--bg-hover);
4226
+ color: var(--text-secondary);
4227
+ }
4228
+ .remote-chat-sysmsg-warn {
4229
+ background: var(--bg-hover);
4230
+ color: var(--text);
4231
+ border-color: var(--border-light);
4232
+ }
4233
+ .remote-chat-sysmsg-error {
4234
+ background: rgba(220, 38, 38, 0.12);
4235
+ color: #fca5a5;
4236
+ border-color: rgba(220, 38, 38, 0.3);
4237
+ }
4238
+ .remote-chat-judgments {
4239
+ margin: 0 0 8px;
4240
+ padding: 8px 10px;
4241
+ background: var(--bg-hover);
4242
+ border-left: 3px solid var(--accent, #a4b630);
4243
+ border-radius: 4px;
4244
+ font-size: 12px;
4245
+ }
4246
+ .remote-chat-judgments-title {
4247
+ font-weight: 600;
4248
+ color: var(--text);
4249
+ margin-bottom: 4px;
4250
+ }
4251
+ .remote-chat-judgment-item {
4252
+ margin: 3px 0;
4253
+ padding-left: 8px;
4254
+ color: var(--text);
4255
+ }
4256
+ .remote-chat-judgment-tag {
4257
+ color: var(--text-muted);
4258
+ font-size: 10px;
4259
+ }
4260
+ .remote-chat-judgment-reason {
4261
+ color: var(--text-secondary);
4262
+ font-size: 11px;
4263
+ }
4264
+ .remote-chat-judgments-foot {
4265
+ margin-top: 6px;
4266
+ color: var(--text-muted);
4267
+ font-size: 11px;
4268
+ }
4269
+
4270
+ /* ============================================================
4271
+ * 2026-06-10: 好友申请 modal — 对齐本地风格 + CSS 变量
4272
+ * ============================================================ */
4273
+ .friend-req-overlay {
4274
+ position: fixed;
4275
+ inset: 0;
4276
+ background: rgba(0, 0, 0, 0.55);
4277
+ z-index: 10004;
4278
+ display: flex;
4279
+ align-items: center;
4280
+ justify-content: center;
4281
+ }
4282
+ .friend-req-shell {
4283
+ background: var(--bg-main);
4284
+ color: var(--text);
4285
+ border: 1px solid var(--border);
4286
+ border-radius: 8px;
4287
+ width: 460px;
4288
+ max-width: 92vw;
4289
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
4290
+ font-family: inherit;
4291
+ }
4292
+ .friend-req-header {
4293
+ padding: 14px 18px;
4294
+ border-bottom: 1px solid var(--border);
4295
+ background: var(--bg-sidebar);
4296
+ display: flex;
4297
+ align-items: center;
4298
+ gap: 10px;
4299
+ border-radius: 8px 8px 0 0;
4300
+ }
4301
+ .friend-req-title {
4302
+ font-size: 15px;
4303
+ font-weight: 600;
4304
+ color: var(--text);
4305
+ }
4306
+ .friend-req-meta {
4307
+ font-size: 11px;
4308
+ color: var(--text-muted);
4309
+ margin-top: 2px;
4310
+ overflow: hidden;
4311
+ text-overflow: ellipsis;
4312
+ white-space: nowrap;
4313
+ }
4314
+ .friend-req-body {
4315
+ padding: 14px 18px;
4316
+ font-size: 13px;
4317
+ color: var(--text);
4318
+ line-height: 1.5;
4319
+ }
4320
+ .friend-req-actions {
4321
+ padding: 10px 18px;
4322
+ border-top: 1px solid var(--border);
4323
+ display: flex;
4324
+ gap: 8px;
4325
+ justify-content: flex-end;
4326
+ background: var(--bg-sidebar);
4327
+ border-radius: 0 0 8px 8px;
4328
+ }
4329
+ .friend-req-btn-deny {
4330
+ padding: 6px 14px;
4331
+ background: var(--bg-hover);
4332
+ color: var(--text-secondary);
4333
+ border: 1px solid var(--border);
4334
+ border-radius: 4px;
4335
+ cursor: pointer;
4336
+ font-size: 13px;
4337
+ font-family: inherit;
4338
+ }
4339
+ .friend-req-btn-deny:hover {
4340
+ background: var(--bg-active);
4341
+ color: var(--text);
4342
+ }
4343
+ .friend-req-btn-accept {
4344
+ padding: 6px 14px;
4345
+ background: var(--user-bg);
4346
+ color: var(--user-text);
4347
+ border: none;
4348
+ border-radius: 4px;
4349
+ cursor: pointer;
4350
+ font-size: 13px;
4351
+ font-weight: 600;
4352
+ font-family: inherit;
4353
+ }
4354
+ .friend-req-btn-accept:hover {
4355
+ filter: brightness(1.1);
4356
+ }
4357
+
4358
+ /* ============================================================
4359
+ * 2026-06-10: Toast 入场动画 (simple-toast 用)
4360
+ * ============================================================ */
4361
+ @keyframes toast-in {
4362
+ from {
4363
+ opacity: 0;
4364
+ transform: translateX(20px);
4365
+ }
4366
+ to {
4367
+ opacity: 1;
4368
+ transform: translateX(0);
4369
+ }
4370
+ }
4371
+
4372
+ /* ============================================================
4373
+ * 2026-06-10: P2P peer-group 可折叠 — 复用 #remote-agents-section.collapsed 模式
4374
+ * peer header 加 .peer-caret, 点击切 .remote-peer-group.collapsed
4375
+ * 折叠后隐藏 .remote-peer-channels, caret 旋转 -90deg
4376
+ * ============================================================ */
4377
+ .remote-peer-group .peer-caret {
4378
+ display: inline-block;
4379
+ font-size: 10px;
4380
+ color: var(--text-muted);
4381
+ transition: transform 0.15s ease;
4382
+ user-select: none;
4383
+ width: 12px;
4384
+ text-align: center;
4385
+ }
4386
+ .remote-peer-group.collapsed .peer-caret {
4387
+ transform: rotate(-90deg);
4388
+ }
4389
+ .remote-peer-group.collapsed > .remote-peer-channels {
4390
+ display: none;
4391
+ }
4392
+ .remote-peer-group .remote-peer-header {
4393
+ /* 让 caret 单独成可点区域时不撑破 header */
4394
+ user-select: none;
4395
+ }
4396
+
4397
+ /* ============================================================
4398
+ * 2026-06-11: 分享 channel modal (P2P peer 分享) — 用 friend-req-shell 风格
4399
+ * ============================================================ */
4400
+ .share-modal-shell {
4401
+ width: 520px;
4402
+ max-height: 80vh;
4403
+ }
4404
+ .share-modal-hint {
4405
+ padding: 8px 18px;
4406
+ font-size: 12px;
4407
+ color: var(--text-muted);
4408
+ background: var(--bg-sidebar);
4409
+ border-bottom: 1px solid var(--border);
4410
+ }
4411
+ .share-modal-list {
4412
+ flex: 1;
4413
+ overflow-y: auto;
4414
+ padding: 4px 18px;
4415
+ max-height: 50vh;
4416
+ }
4417
+ .share-modal-empty {
4418
+ color: var(--text-muted);
4419
+ padding: 20px;
4420
+ text-align: center;
4421
+ }
4422
+ .share-modal-row {
4423
+ display: flex;
4424
+ align-items: flex-start;
4425
+ gap: 10px;
4426
+ padding: 8px 4px;
4427
+ cursor: pointer;
4428
+ border-bottom: 1px solid var(--border);
4429
+ font-family: inherit;
4430
+ }
4431
+ .share-modal-row:hover {
4432
+ background: var(--bg-hover);
4433
+ }
4434
+ .share-modal-cb {
4435
+ margin-top: 4px;
4436
+ cursor: pointer;
4437
+ width: 16px;
4438
+ height: 16px;
4439
+ accent-color: var(--accent);
4440
+ }
4441
+ .share-modal-row-info {
4442
+ flex: 1;
4443
+ min-width: 0;
4444
+ }
4445
+ .share-modal-row-name {
4446
+ font-size: 13px;
4447
+ font-weight: 500;
4448
+ color: var(--text);
4449
+ }
4450
+ .share-modal-row-meta {
4451
+ font-size: 10px;
4452
+ color: var(--text-muted);
4453
+ margin-top: 2px;
4454
+ font-family: 'JetBrains Mono', monospace;
4455
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli.js",
@@ -60,6 +60,7 @@
60
60
  "viem": "^2.52.0"
61
61
  },
62
62
  "devDependencies": {
63
+ "@playwright/test": "^1.60.0",
63
64
  "@types/express": "^5.0.6",
64
65
  "@types/node": "^25.9.1",
65
66
  "@types/pdf-parse": "^1.1.5",
@@ -1178,6 +1178,7 @@ ${toolDefs}
1178
1178
  const reply = response.reply.trim();
1179
1179
 
1180
1180
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
1181
+ console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
1181
1182
 
1182
1183
  // 通知前端:收到 LLM 回复
1183
1184
  if (onStream) {
@@ -1450,7 +1451,14 @@ Workspace root folder: ${this.cwd}
1450
1451
  const marker = '<final gen>';
1451
1452
  const markerIndex = content.indexOf(marker);
1452
1453
  if (markerIndex !== -1) {
1453
- content = content.substring(markerIndex + marker.length).trim();
1454
+ const after = content.substring(markerIndex + marker.length).trim();
1455
+ // v3 修复: 如果 <final gen> 之后是空, fallback 用 marker 之前的内容 (去掉 marker)
1456
+ // 否则 LLM 写了 <final gen> 在末尾时, 用户看到空回复 + error
1457
+ if (after) {
1458
+ content = after;
1459
+ } else {
1460
+ content = content.substring(0, markerIndex).trim();
1461
+ }
1454
1462
  }
1455
1463
  // 移除任何 tool call 标记
1456
1464
  let cleaned = content
@@ -96,38 +96,14 @@ export {
96
96
  type SubagentResult,
97
97
  } from '../pi-ecosystem-subagents/index.js';
98
98
 
99
- export {
100
- registerAnt,
101
- antScouting,
102
- antWorking,
103
- antReviewing,
104
- antComplete,
105
- antFail,
106
- antAbort,
107
- antTick,
108
- createTask,
109
- dispatchTask,
110
- recordResult,
111
- getAnt,
112
- listAnts,
113
- listAntsByRole,
114
- listAntsBySignal,
115
- getActiveAnts,
116
- getTask,
117
- listTasks,
118
- getSignalHistory,
119
- getColonyStatus,
120
- getColonyDump,
121
- persistColony,
122
- loadColony,
123
- onColonyEvent,
124
- offColonyEvent,
125
- type Ant,
126
- type ColonyTask,
127
- type ColonySignalEvent,
128
- type ColonySignal,
129
- type AntRole,
130
- } from '../pi-ecosystem-colony/index.js';
99
+ // 2026-06-11: 蚁群模块 (pi-ecosystem-colony) 已被用户删除, 移除对应 re-export 防止启动失败
100
+ // export {
101
+ // registerAnt, antScouting, antWorking, antReviewing, antComplete, antFail, antAbort, antTick,
102
+ // createTask, dispatchTask, recordResult, getAnt, listAnts, listAntsByRole, listAntsBySignal,
103
+ // getActiveAnts, getTask, listTasks, getSignalHistory, getColonyStatus, getColonyDump,
104
+ // persistColony, loadColony, onColonyEvent, offColonyEvent,
105
+ // type Ant, type ColonyTask, type ColonySignalEvent, type ColonySignal, type AntRole,
106
+ // } from '../pi-ecosystem-colony/index.js';
131
107
 
132
108
  // Judgment exports
133
109
  export {
@@ -42,11 +42,19 @@ export class Watchdog {
42
42
 
43
43
  /**
44
44
  * 记录活动(调用后更新 lastActivityTime)
45
+ * 2026-06-10: 加 5s 去抖, 防 broadcast / SSE 高频路径炸 log + CPU
46
+ * - lastActivityTime 始终更新 (cheap)
47
+ * - 但 onLog 回调只在距上次 ≥5s 时触发, 避免每秒几十次 console.log
45
48
  */
49
+ private _lastLogAt = 0;
46
50
  recordActivity(component?: string): void {
47
51
  this.state.lastActivityTime = Date.now();
48
52
  if (component) {
49
- this.callbacks.onLog?.(`[Watchdog] Activity from ${component}`);
53
+ const now = Date.now();
54
+ if (now - this._lastLogAt >= 5000) {
55
+ this._lastLogAt = now;
56
+ this.callbacks.onLog?.(`[Watchdog] Activity from ${component}`);
57
+ }
50
58
  }
51
59
  }
52
60
 
@@ -222,9 +222,14 @@ class AudioConfigStore {
222
222
  return { success: true, latency };
223
223
  } else {
224
224
  const errorText = await response.text().catch(() => 'Unknown error');
225
+ const hint = response.status === 401
226
+ ? '(请确认是 MiniMax 的 API Key)'
227
+ : response.status === 404
228
+ ? '(端点不存在 — 请检查 baseUrl)'
229
+ : '';
225
230
  return {
226
231
  success: false,
227
- error: `HTTP ${response.status}: ${errorText.substring(0, 200)}`,
232
+ error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
228
233
  latency
229
234
  };
230
235
  }
@@ -77,7 +77,7 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
77
77
  enabled: false,
78
78
  apiKey: '',
79
79
  baseUrl: 'https://api.minimaxi.com/v1',
80
- model: 'MiniMax-M2',
80
+ model: 'MiniMax-M3',
81
81
  temperature: 0.7,
82
82
  maxTokens: 4096,
83
83
  requiresApiKey: true
@@ -129,17 +129,22 @@ export const DEFAULT_PROVIDER_CONFIGS: Record<ModelProvider, ProviderConfig> = {
129
129
  }
130
130
  };
131
131
 
132
- export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: string; requiresApiKey: boolean }> = {
133
- openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true },
134
- anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true },
132
+ export const PROVIDER_INFO: Record<ModelProvider, { name: string; description: string; requiresApiKey: boolean; models?: string[] }> = {
133
+ openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true, models: ['gpt-4', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'] },
134
+ anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true, models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'] },
135
135
  openrouter: { name: 'OpenRouter', description: '聚合多个 AI 供应商', requiresApiKey: true },
136
- gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true },
136
+ gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true, models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'] },
137
137
  ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
138
- minimax: { name: 'MiniMax', description: '国产大模型服务', requiresApiKey: true },
139
- deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true },
140
- kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true },
141
- glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true },
142
- qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true },
138
+ minimax: {
139
+ name: 'MiniMax',
140
+ description: '国产大模型服务',
141
+ requiresApiKey: true,
142
+ models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2', 'MiniMax-M2.1-highspeed', 'MiniMax-M2.7-highspeed']
143
+ },
144
+ deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true, models: ['deepseek-chat', 'deepseek-reasoner'] },
145
+ kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true, models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'] },
146
+ glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true, models: ['glm-4-flash', 'glm-4', 'glm-4-plus', 'glm-4-air', 'glm-4-airx'] },
147
+ qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen-plus', 'qwen-max', 'qwen-turbo', 'qwen-long'] },
143
148
  local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
144
149
  };
145
150
 
@@ -323,7 +328,12 @@ class LLMConfigStore {
323
328
  return { success: true, latency };
324
329
  } else {
325
330
  const errorText = await response.text().catch(() => 'Unknown error');
326
- return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 100)}`, latency };
331
+ const hint = response.status === 401
332
+ ? '(API Key 无效或不匹配该供应商 — 请检查是否复制完整、有无多余空格)'
333
+ : response.status === 404
334
+ ? '(端点不存在 — 请检查 baseUrl)'
335
+ : '';
336
+ return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`, latency };
327
337
  }
328
338
  } catch (error: any) {
329
339
  return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
package/src/llm/pi-ai.ts CHANGED
@@ -176,7 +176,7 @@ export class PiAIModel {
176
176
  ollama: this.config.model || 'llama3.2',
177
177
  openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
178
178
  gemini: this.config.model || 'gemini-2.0-flash',
179
- minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
179
+ minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M3',
180
180
  deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-chat',
181
181
  kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'moonshot-v1-8k',
182
182
  glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-4-flash',
@@ -482,7 +482,7 @@ function detectModel(provider: ModelProvider): string {
482
482
  ollama: 'llama3.2',
483
483
  openrouter: 'anthropic/claude-3.5-sonnet',
484
484
  gemini: 'gemini-2.0-flash',
485
- minimax: 'MiniMax-M2.7',
485
+ minimax: 'MiniMax-M3',
486
486
  deepseek: 'deepseek-chat',
487
487
  kimi: 'moonshot-v1-8k',
488
488
  glm: 'glm-4-flash',
@@ -232,9 +232,15 @@ class VideoConfigStore {
232
232
  return { success: true, latency };
233
233
  } else {
234
234
  const errorText = await response.text().catch(() => 'Unknown error');
235
+ // 401 通常是 key 错误,给出针对性提示
236
+ const hint = response.status === 401
237
+ ? '(请确认是火山方舟 ARK 的 API Key,不是 MiniMax / 其他平台)'
238
+ : response.status === 404
239
+ ? '(端点不存在 — 火山方舟可能没有 /models,请检查 baseUrl)'
240
+ : '';
235
241
  return {
236
242
  success: false,
237
- error: `HTTP ${response.status}: ${errorText.substring(0, 200)}`,
243
+ error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
238
244
  latency
239
245
  };
240
246
  }
@@ -88,10 +88,9 @@ export class P2PDirect extends EventEmitter {
88
88
  // 双向记录 (inbound + outbound 都能拿到)
89
89
  this.conns.set(remotePubKeyHex, conn);
90
90
 
91
- // v3: 触发 'connection' 事件, 上层 (web server) 可以主动给新连接发消息
92
- this.emit('connection', { remotePublicKey: remotePubKeyHex, conn });
93
-
94
91
  // 收到数据时 → 触发 'data' 事件
92
+ // 注意: data 监听器必须在 emit('connection') 之前注册,
93
+ // 否则 server 的 connection handler 发送消息后, 对端回复可能在 data 监听器就绪前到达
95
94
  conn.on('data', (chunk: Buffer | Uint8Array) => {
96
95
  const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
97
96
  console.log(`[P2PDirect:${this.name}] 收到数据 from ${remotePubKeyHex.substring(0,12)}... (${buf.length} bytes)`);
@@ -108,6 +107,10 @@ export class P2PDirect extends EventEmitter {
108
107
  conn.on('close', () => {
109
108
  this.conns.delete(remotePubKeyHex);
110
109
  });
110
+
111
+ // v3: 触发 'connection' 事件, 上层 (web server) 可以主动给新连接发消息
112
+ // 注意: 放在 data/error/close 监听器之后, 确保 server 的 connection handler 不会先于 data 就绪
113
+ this.emit('connection', { remotePublicKey: remotePubKeyHex, conn });
111
114
  });
112
115
 
113
116
  await this.swarm.listen(); // server 模式
@@ -159,6 +162,59 @@ export class P2PDirect extends EventEmitter {
159
162
  }
160
163
  }
161
164
 
165
+ /**
166
+ * 2026-06-10: 真"主动发, 等握手完成"版本 — 修复好友申请 fire-and-forget bug.
167
+ *
168
+ * 之前的问题: server.ts:2914 `await swarm.joinPeer(...)` 只触发握手, conn 还没 push 进 this.conns,
169
+ * 立即调 sendTo 找不到 conn → 静默返回 false → 消息扔进虚空.
170
+ *
171
+ * 现在: sendToWithWait 监听 'connection' 事件, 等到 targetPublicKey 真正出现在 this.conns,
172
+ * 才 write; 超时返回 NO_CONN; 写失败返回 WRITE_FAIL; 成功返回 SENT.
173
+ *
174
+ * 上层调用: const r = await p2p.sendToWithWait(pk, rpc, 5000);
175
+ * if (r !== 'SENT') return 502 给前端.
176
+ */
177
+ async sendToWithWait(
178
+ publicKeyHex: string,
179
+ data: Buffer | string,
180
+ timeoutMs: number = 5000
181
+ ): Promise<'SENT' | 'NO_CONN' | 'WRITE_FAIL'> {
182
+ // 2026-06-11: 先主动触发 joinPeer, 否则 DHT 上对面可能没 push conn
183
+ if (this.swarm) {
184
+ try { await this.swarm.joinPeer(Buffer.from(publicKeyHex, 'hex')); } catch {}
185
+ }
186
+ // 1) 已有 conn → 立即试
187
+ let conn = this.conns.get(publicKeyHex);
188
+ if (!conn || conn.destroyed) {
189
+ // 2) 等 'connection' 事件 (this.emit('connection', { remotePublicKey, conn }))
190
+ const waitResult = await new Promise<'READY' | 'TIMEOUT'>((resolve) => {
191
+ const timer = setTimeout(() => {
192
+ this.off('connection', onConn);
193
+ resolve('TIMEOUT');
194
+ }, timeoutMs);
195
+ const onConn = (evt: { remotePublicKey: string; conn: any }) => {
196
+ if (evt.remotePublicKey === publicKeyHex) {
197
+ clearTimeout(timer);
198
+ this.off('connection', onConn);
199
+ resolve('READY');
200
+ }
201
+ };
202
+ this.on('connection', onConn);
203
+ });
204
+ if (waitResult === 'TIMEOUT') return 'NO_CONN';
205
+ conn = this.conns.get(publicKeyHex);
206
+ if (!conn || conn.destroyed) return 'NO_CONN'; // 双保险
207
+ }
208
+ const buf = typeof data === 'string' ? Buffer.from(data) : data;
209
+ try {
210
+ conn.write(buf);
211
+ return 'SENT';
212
+ } catch (err) {
213
+ console.error(`[P2PDirect:${this.name}] sendToWithWait 写失败 (${publicKeyHex.substring(0,12)}...):`, (err as Error).message);
214
+ return 'WRITE_FAIL';
215
+ }
216
+ }
217
+
162
218
  getPublicKey(): string {
163
219
  if (!this.swarm) return '';
164
220
  return b4a.toString(this.swarm.keyPair.publicKey, 'hex');