@bolloon/bolloon-agent 0.2.13 → 0.2.15
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/cli-entry.js +1 -1
- package/dist/llm/config-store.js +100 -16
- package/dist/llm/pi-ai.js +2 -0
- package/dist/web/api-config.html +12 -2
- package/dist/web/routes-llm-config.js +23 -13
- package/package.json +2 -2
package/dist/cli-entry.js
CHANGED
|
@@ -23,7 +23,7 @@ const YELLOW = '\x1b[33m';
|
|
|
23
23
|
const GREEN = '\x1b[32m';
|
|
24
24
|
const MAGENTA = '\x1b[35m';
|
|
25
25
|
// 版本信息 — 与 package.json:version 同步, 否则 banner 会显示过时版本误导用户
|
|
26
|
-
const VERSION = '0.2.
|
|
26
|
+
const VERSION = '0.2.15';
|
|
27
27
|
function log(msg, color = RESET) {
|
|
28
28
|
console.log(`${color}${msg}${RESET}`);
|
|
29
29
|
}
|
package/dist/llm/config-store.js
CHANGED
|
@@ -38,7 +38,7 @@ export const DEFAULT_PROVIDER_CONFIGS = {
|
|
|
38
38
|
enabled: false,
|
|
39
39
|
apiKey: '',
|
|
40
40
|
baseUrl: 'https://generativelanguage.googleapis.com/v1beta',
|
|
41
|
-
model: 'gemini-
|
|
41
|
+
model: 'gemini-2.5-pro',
|
|
42
42
|
temperature: 0.7,
|
|
43
43
|
maxTokens: 4096,
|
|
44
44
|
requiresApiKey: true
|
|
@@ -121,7 +121,12 @@ export const PROVIDER_INFO = {
|
|
|
121
121
|
openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true, models: ['gpt-4.1', 'gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'gpt-3.5-turbo'] },
|
|
122
122
|
anthropic: { name: 'Anthropic', description: 'Claude 3.5+ 系列模型', requiresApiKey: true, models: ['claude-sonnet-4-5-20250929', 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'] },
|
|
123
123
|
openrouter: { name: 'OpenRouter', description: '聚合多个 AI 供应商', requiresApiKey: true, models: ['anthropic/claude-sonnet-4.5', 'anthropic/claude-3.5-sonnet'] },
|
|
124
|
-
gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true, models: [
|
|
124
|
+
gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true, models: [
|
|
125
|
+
'gemini-3.5-flash', // 2026-06 官方推荐 stable 旗舰 (https://ai.google.dev/gemini-api/docs/models)
|
|
126
|
+
'gemini-2.5-pro', // 高级推理, 仍为 GA
|
|
127
|
+
'gemini-3.1-flash-lite', // 成本敏感场景
|
|
128
|
+
'gemini-flash-latest', // 滚动 alias → 当前 stable Flash
|
|
129
|
+
] },
|
|
125
130
|
ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
|
|
126
131
|
minimax: {
|
|
127
132
|
name: 'MiniMax',
|
|
@@ -209,6 +214,18 @@ function getDefaultConfig() {
|
|
|
209
214
|
class LLMConfigStore {
|
|
210
215
|
config = null;
|
|
211
216
|
initialized = false;
|
|
217
|
+
// v0.2.15: single-flight lock around read-modify-write of `~/.bolloon/llm-config.json`.
|
|
218
|
+
// Prevents concurrent save() calls from clobbering each other when the user
|
|
219
|
+
// configures two providers back-to-back (e.g. saving gemini, then anthropic, in
|
|
220
|
+
// quick succession). One operation at a time, in call order.
|
|
221
|
+
writeChain = Promise.resolve();
|
|
222
|
+
async withWriteLock(fn) {
|
|
223
|
+
// Chain the new op after the previous one; swallow the previous op's
|
|
224
|
+
// rejection so a single failed save does not poison subsequent writes.
|
|
225
|
+
const next = this.writeChain.then(fn, fn);
|
|
226
|
+
this.writeChain = next.then(() => undefined, () => undefined);
|
|
227
|
+
return next;
|
|
228
|
+
}
|
|
212
229
|
async initialize() {
|
|
213
230
|
if (this.initialized)
|
|
214
231
|
return;
|
|
@@ -270,19 +287,23 @@ class LLMConfigStore {
|
|
|
270
287
|
if (providerConfig.requiresApiKey && !providerConfig.apiKey) {
|
|
271
288
|
throw new Error(`${provider} requires an API key but none is configured`);
|
|
272
289
|
}
|
|
273
|
-
this.
|
|
274
|
-
|
|
290
|
+
await this.withWriteLock(async () => {
|
|
291
|
+
this.config.activeProvider = provider;
|
|
292
|
+
await this.save();
|
|
293
|
+
});
|
|
275
294
|
}
|
|
276
295
|
async updateProvider(provider, updates) {
|
|
277
296
|
await this.initialize();
|
|
278
297
|
if (!this.config?.providers[provider]) {
|
|
279
298
|
throw new Error(`Unknown provider: ${provider}`);
|
|
280
299
|
}
|
|
281
|
-
this.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
300
|
+
await this.withWriteLock(async () => {
|
|
301
|
+
this.config.providers[provider] = {
|
|
302
|
+
...this.config.providers[provider],
|
|
303
|
+
...updates
|
|
304
|
+
};
|
|
305
|
+
await this.save();
|
|
306
|
+
});
|
|
286
307
|
}
|
|
287
308
|
async testProvider(provider) {
|
|
288
309
|
await this.initialize();
|
|
@@ -295,10 +316,16 @@ class LLMConfigStore {
|
|
|
295
316
|
}
|
|
296
317
|
const startTime = Date.now();
|
|
297
318
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
319
|
+
// v0.2.15: per-provider test endpoint. The old unified `GET baseUrl/models`
|
|
320
|
+
// is wrong on at least two providers:
|
|
321
|
+
// - Anthropic has no GET /v1/models endpoint -> always 404.
|
|
322
|
+
// - Google's GET /v1beta/models returns the public model catalog
|
|
323
|
+
// without auth -> always 200, even with a wrong/expired key, so
|
|
324
|
+
// the user saw "connected" while chat requests still failed.
|
|
325
|
+
// The per-provider branch below uses an endpoint that actually
|
|
326
|
+
// gates on the key.
|
|
327
|
+
const { url, init } = this.buildTestRequest(provider, config);
|
|
328
|
+
const response = await fetch(url, init);
|
|
302
329
|
const latency = Date.now() - startTime;
|
|
303
330
|
if (response.ok) {
|
|
304
331
|
return { success: true, latency };
|
|
@@ -307,9 +334,13 @@ class LLMConfigStore {
|
|
|
307
334
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
308
335
|
const hint = response.status === 401
|
|
309
336
|
? '(API Key 无效或不匹配该供应商 — 请检查是否复制完整、有无多余空格)'
|
|
310
|
-
: response.status ===
|
|
311
|
-
? '
|
|
312
|
-
:
|
|
337
|
+
: response.status === 403
|
|
338
|
+
? '(API Key 没有调用此端点的权限 — 请检查 key scope 或供应商 endpoint)'
|
|
339
|
+
: response.status === 404
|
|
340
|
+
? '(端点不存在 — 请检查 baseUrl)'
|
|
341
|
+
: response.status === 429
|
|
342
|
+
? '(供应商限流中 — 稍候再试)'
|
|
343
|
+
: '';
|
|
313
344
|
return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`, latency };
|
|
314
345
|
}
|
|
315
346
|
}
|
|
@@ -317,6 +348,59 @@ class LLMConfigStore {
|
|
|
317
348
|
return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
|
|
318
349
|
}
|
|
319
350
|
}
|
|
351
|
+
/**
|
|
352
|
+
* Build the lightest "is the key + baseUrl healthy?" probe for the given
|
|
353
|
+
* provider. Each branch targets an endpoint that *actually* validates the
|
|
354
|
+
* credentials (as opposed to a public catalog or non-existent route).
|
|
355
|
+
*/
|
|
356
|
+
buildTestRequest(provider, config) {
|
|
357
|
+
switch (provider) {
|
|
358
|
+
case 'anthropic':
|
|
359
|
+
// No GET /v1/models. Use a minimal /messages ping that fails fast
|
|
360
|
+
// on bad keys (401) and rate-limits (429) without burning quota.
|
|
361
|
+
return {
|
|
362
|
+
url: `${config.baseUrl}/messages`,
|
|
363
|
+
init: {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: this.buildHeaders(provider, config),
|
|
366
|
+
body: JSON.stringify({
|
|
367
|
+
model: config.model || 'claude-sonnet-4-5-20250929',
|
|
368
|
+
max_tokens: 1,
|
|
369
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
370
|
+
}),
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
case 'gemini':
|
|
374
|
+
// listModels with the key in the query string returns 400 for
|
|
375
|
+
// an invalid key, 200 for a valid one. This is the only "light"
|
|
376
|
+
// Gemini endpoint that gates on auth (generateContent would
|
|
377
|
+
// burn quota on a real prompt).
|
|
378
|
+
return {
|
|
379
|
+
url: `${config.baseUrl}/models?key=${encodeURIComponent(config.apiKey)}`,
|
|
380
|
+
init: { method: 'GET' },
|
|
381
|
+
};
|
|
382
|
+
case 'ollama':
|
|
383
|
+
return { url: `${config.baseUrl}/api/tags`, init: { method: 'GET' } };
|
|
384
|
+
case 'openai':
|
|
385
|
+
case 'openrouter':
|
|
386
|
+
case 'deepseek':
|
|
387
|
+
case 'kimi':
|
|
388
|
+
case 'glm':
|
|
389
|
+
case 'qwen':
|
|
390
|
+
case 'mimo':
|
|
391
|
+
case 'minimax':
|
|
392
|
+
case 'local':
|
|
393
|
+
return {
|
|
394
|
+
url: `${config.baseUrl}/models`,
|
|
395
|
+
init: { method: 'GET', headers: this.buildHeaders(provider, config) },
|
|
396
|
+
};
|
|
397
|
+
default:
|
|
398
|
+
return {
|
|
399
|
+
url: `${config.baseUrl}/models`,
|
|
400
|
+
init: { method: 'GET', headers: this.buildHeaders(provider, config) },
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
320
404
|
buildHeaders(provider, config) {
|
|
321
405
|
const headers = { 'Content-Type': 'application/json' };
|
|
322
406
|
switch (provider) {
|
package/dist/llm/pi-ai.js
CHANGED
|
@@ -255,6 +255,8 @@ export class PiAIModel {
|
|
|
255
255
|
anthropic: this.config.model || 'claude-sonnet-4-5-20250929',
|
|
256
256
|
ollama: this.config.model || 'llama3.2',
|
|
257
257
|
openrouter: this.config.model || 'anthropic/claude-sonnet-4.5',
|
|
258
|
+
// Pinned to 2.5-pro: the only `-pro` model that is GA per Google docs.
|
|
259
|
+
// The 3.x line ships as `-flash` only — there is no `gemini-3.x-pro`.
|
|
258
260
|
gemini: this.config.model || 'gemini-2.5-pro',
|
|
259
261
|
minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M3',
|
|
260
262
|
deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-chat',
|
package/dist/web/api-config.html
CHANGED
|
@@ -421,7 +421,15 @@
|
|
|
421
421
|
const result = document.getElementById('testResult');
|
|
422
422
|
|
|
423
423
|
btn.disabled = true;
|
|
424
|
-
|
|
424
|
+
// v0.2.15: defer label mutation to the next frame. Synchronously
|
|
425
|
+
// replacing the button's innerHTML in its own click handler cancels
|
|
426
|
+
// the click event in several Chromium-derived engines — the user
|
|
427
|
+
// observed the first click "doing nothing" until they closed and
|
|
428
|
+
// reopened the modal. Wrapping the mutation in requestAnimationFrame
|
|
429
|
+
// lets the click event finish dispatching first.
|
|
430
|
+
requestAnimationFrame(() => {
|
|
431
|
+
btn.textContent = '⚡ 测试中...';
|
|
432
|
+
});
|
|
425
433
|
result.style.display = 'none';
|
|
426
434
|
|
|
427
435
|
const endpoint = currentProviderType === 'llm' ? '/api/llm-test' :
|
|
@@ -447,7 +455,9 @@
|
|
|
447
455
|
}
|
|
448
456
|
|
|
449
457
|
btn.disabled = false;
|
|
450
|
-
|
|
458
|
+
requestAnimationFrame(() => {
|
|
459
|
+
btn.textContent = '⚡ 测试连接';
|
|
460
|
+
});
|
|
451
461
|
}
|
|
452
462
|
|
|
453
463
|
// ==================== 保存 ====================
|
|
@@ -43,20 +43,30 @@ export function registerLlmConfigRoutes(app) {
|
|
|
43
43
|
config.apiKey = currentConfig.apiKey;
|
|
44
44
|
}
|
|
45
45
|
await llmConfigStore.updateProvider(provider, config);
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
46
|
+
// v0.2.15: 当用户保存一个 LLM 配置为 enabled + 有 key 时,自动把它切为
|
|
47
|
+
// activeProvider 并 rebind runtime singleton。修原来的两个真问题:
|
|
48
|
+
// 1) frontend Save 从不调 /api/llm-provider,于是 activeProvider 一直
|
|
49
|
+
// 卡在 process 启动时的第一个值,新配置的 provider 永远不接管 chat
|
|
50
|
+
// 2) 即使用户手动 active,updateProvider 不替换 modelInstance(只有
|
|
51
|
+
// 当前 active 命中才 rebind),所以保存非 active 那个 provider 之后
|
|
52
|
+
// runtime 还是指向旧 provider + 旧 key
|
|
53
|
+
// 现在:用户每保存一个 enabled 的 LLM 配置,bolloon 立即把这个 provider
|
|
54
|
+
// 设成 active + 重新 init MinLLM/Pi SDK,让"配置 + 立刻能跑"成立。
|
|
55
|
+
// 用户想保持旧 active,可以显式调 /api/llm-provider 改回去。
|
|
56
|
+
const newConfig = await llmConfigStore.getProvider(provider);
|
|
57
|
+
const shouldAutoActivate = newConfig?.enabled === true &&
|
|
58
|
+
// 如果 provider 不需要 key (如 ollama) 或者已经给了真 key,才激活
|
|
59
|
+
(newConfig.apiKey || !newConfig.requiresApiKey);
|
|
60
|
+
if (shouldAutoActivate) {
|
|
61
|
+
await llmConfigStore.setActiveProvider(provider);
|
|
62
|
+
initMinimax({
|
|
63
|
+
provider: provider,
|
|
64
|
+
apiKey: newConfig.apiKey || undefined,
|
|
65
|
+
baseUrl: newConfig.baseUrl || undefined,
|
|
66
|
+
model: newConfig.model || undefined
|
|
67
|
+
});
|
|
58
68
|
}
|
|
59
|
-
res.json({ ok: true });
|
|
69
|
+
res.json({ ok: true, autoActivated: shouldAutoActivate });
|
|
60
70
|
}
|
|
61
71
|
catch (err) {
|
|
62
72
|
res.status(500).json({ error: err.message });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bolloon/bolloon-agent",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
|
|
6
6
|
"main": "dist/cli-entry.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"src/constraint-runtime"
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@bolloon/bolloon-agent": "^0.2.
|
|
51
|
+
"@bolloon/bolloon-agent": "^0.2.15",
|
|
52
52
|
"@bolloon/constraint-runtime": "0.1.0",
|
|
53
53
|
"@capacitor/core": "^8.4.1",
|
|
54
54
|
"@capacitor/ios": "^8.4.1",
|