@bolloon/bolloon-agent 0.1.32 → 0.1.33

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.
@@ -914,6 +914,7 @@ ${toolDefs}
914
914
  const response = await llm.chat(context, systemPrompt);
915
915
  const reply = response.reply.trim();
916
916
  console.log(`[PiAgent] LLM 回复长度: ${reply.length}, 内容预览: "${reply.substring(0, 80)}..."`);
917
+ console.log(`[PiAgent] LLM 完整回复:\n${reply}`);
917
918
  // 通知前端:收到 LLM 回复
918
919
  if (onStream) {
919
920
  onStream({ type: 'token', content: reply.substring(0, 100) });
@@ -1159,7 +1160,15 @@ Workspace root folder: ${this.cwd}
1159
1160
  const marker = '<final gen>';
1160
1161
  const markerIndex = content.indexOf(marker);
1161
1162
  if (markerIndex !== -1) {
1162
- content = content.substring(markerIndex + marker.length).trim();
1163
+ const after = content.substring(markerIndex + marker.length).trim();
1164
+ // v3 修复: 如果 <final gen> 之后是空, fallback 用 marker 之前的内容 (去掉 marker)
1165
+ // 否则 LLM 写了 <final gen> 在末尾时, 用户看到空回复 + error
1166
+ if (after) {
1167
+ content = after;
1168
+ }
1169
+ else {
1170
+ content = content.substring(0, markerIndex).trim();
1171
+ }
1163
1172
  }
1164
1173
  // 移除任何 tool call 标记
1165
1174
  let cleaned = content
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Audio Generation API Configuration Store
3
+ *
4
+ * 音频模型配置:MiniMax 提供的 Speech(TTS+ASR)与 Music(文生音乐)。
5
+ * 与 LLM / 视频配置完全独立,持久化到 ~/.bolloon/audio-config.json。
6
+ *
7
+ * 复用 LLM 那一套 MINIMAX_API_KEY 即可(同源)。
8
+ * - TTS: POST /audio/speech (OpenAI 兼容,body 含 model/voice/input)
9
+ * - ASR: POST /audio/transcriptions
10
+ * - Music: POST /music_generation (MiniMax 自有端点)
11
+ */
12
+ import * as fs from 'fs/promises';
13
+ import * as path from 'path';
14
+ const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.bolloon');
15
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'audio-config.json');
16
+ export const DEFAULT_AUDIO_PROVIDER_CONFIGS = {
17
+ 'minimax-speech': {
18
+ enabled: false,
19
+ apiKey: '',
20
+ baseUrl: 'https://api.minimaxi.com/v1',
21
+ model: 'speech-01',
22
+ voice: 'male-qn-jingying',
23
+ speed: 1.0,
24
+ format: 'mp3',
25
+ requiresApiKey: true
26
+ },
27
+ 'minimax-music': {
28
+ enabled: false,
29
+ apiKey: '',
30
+ baseUrl: 'https://api.minimaxi.com/v1',
31
+ model: 'music-01',
32
+ mode: 'instrumental',
33
+ duration: 30,
34
+ requiresApiKey: true
35
+ }
36
+ };
37
+ export const AUDIO_PROVIDER_INFO = {
38
+ 'minimax-speech': {
39
+ name: 'MiniMax Speech',
40
+ description: 'TTS 文生语音 / ASR 语音转写',
41
+ requiresApiKey: true,
42
+ docs: 'https://platform.minimaxi.com/document/T2A%20V2',
43
+ kind: 'speech'
44
+ },
45
+ 'minimax-music': {
46
+ name: 'MiniMax Music',
47
+ description: '文生音乐 (纯音乐 / 带歌词)',
48
+ requiresApiKey: true,
49
+ docs: 'https://platform.minimaxi.com/document/Music%20Generation',
50
+ kind: 'music'
51
+ }
52
+ };
53
+ function getDefaultConfig() {
54
+ const envConfigs = {};
55
+ const sharedKey = process.env.MINIMAX_API_KEY || '';
56
+ if (sharedKey) {
57
+ envConfigs['minimax-speech'] = {
58
+ ...DEFAULT_AUDIO_PROVIDER_CONFIGS['minimax-speech'],
59
+ enabled: true,
60
+ apiKey: sharedKey
61
+ };
62
+ envConfigs['minimax-music'] = {
63
+ ...DEFAULT_AUDIO_PROVIDER_CONFIGS['minimax-music'],
64
+ enabled: true,
65
+ apiKey: sharedKey
66
+ };
67
+ }
68
+ const activeProvider = 'minimax-speech';
69
+ const providers = { ...DEFAULT_AUDIO_PROVIDER_CONFIGS };
70
+ for (const [provider, config] of Object.entries(envConfigs)) {
71
+ if (config) {
72
+ providers[provider] = config;
73
+ }
74
+ }
75
+ return {
76
+ activeProvider,
77
+ providers,
78
+ updatedAt: new Date().toISOString()
79
+ };
80
+ }
81
+ class AudioConfigStore {
82
+ config = null;
83
+ initialized = false;
84
+ async initialize() {
85
+ if (this.initialized)
86
+ return;
87
+ try {
88
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
89
+ const data = await fs.readFile(CONFIG_PATH, 'utf-8');
90
+ const loadedConfig = JSON.parse(data);
91
+ // 补齐缺失的供应商
92
+ const defaultProviders = Object.keys(DEFAULT_AUDIO_PROVIDER_CONFIGS);
93
+ for (const provider of defaultProviders) {
94
+ if (!loadedConfig.providers[provider]) {
95
+ loadedConfig.providers[provider] = { ...DEFAULT_AUDIO_PROVIDER_CONFIGS[provider] };
96
+ }
97
+ }
98
+ const activeProvider = loadedConfig.activeProvider;
99
+ if (!activeProvider || !DEFAULT_AUDIO_PROVIDER_CONFIGS[activeProvider]) {
100
+ loadedConfig.activeProvider = 'minimax-speech';
101
+ }
102
+ this.config = loadedConfig;
103
+ }
104
+ catch {
105
+ this.config = getDefaultConfig();
106
+ await this.save();
107
+ }
108
+ this.initialized = true;
109
+ }
110
+ async save() {
111
+ if (!this.config)
112
+ return;
113
+ this.config.updatedAt = new Date().toISOString();
114
+ await fs.writeFile(CONFIG_PATH, JSON.stringify(this.config, null, 2));
115
+ }
116
+ async getConfig() {
117
+ await this.initialize();
118
+ return { ...this.config };
119
+ }
120
+ async getProvider(provider) {
121
+ await this.initialize();
122
+ return this.config?.providers[provider] || null;
123
+ }
124
+ async getActiveProvider() {
125
+ await this.initialize();
126
+ return this.config?.activeProvider || 'minimax-speech';
127
+ }
128
+ async getActiveProviderConfig() {
129
+ await this.initialize();
130
+ const provider = this.config?.activeProvider;
131
+ if (!provider)
132
+ return null;
133
+ return this.config?.providers[provider] || null;
134
+ }
135
+ async setActiveProvider(provider) {
136
+ await this.initialize();
137
+ if (!this.config?.providers[provider]) {
138
+ throw new Error(`Unknown audio provider: ${provider}`);
139
+ }
140
+ this.config.activeProvider = provider;
141
+ await this.save();
142
+ }
143
+ async updateProvider(provider, updates) {
144
+ await this.initialize();
145
+ if (!this.config?.providers[provider]) {
146
+ throw new Error(`Unknown audio provider: ${provider}`);
147
+ }
148
+ this.config.providers[provider] = {
149
+ ...this.config.providers[provider],
150
+ ...updates
151
+ };
152
+ await this.save();
153
+ }
154
+ /**
155
+ * 测试连接:探测 /models 端点。
156
+ */
157
+ async testProvider(provider) {
158
+ await this.initialize();
159
+ const config = this.config?.providers[provider];
160
+ if (!config)
161
+ return { success: false, error: 'Provider not configured' };
162
+ if (!config.enabled)
163
+ return { success: false, error: 'Provider is not enabled' };
164
+ if (config.requiresApiKey && !config.apiKey) {
165
+ return { success: false, error: 'API key is required' };
166
+ }
167
+ const startTime = Date.now();
168
+ try {
169
+ const response = await fetch(`${config.baseUrl.replace(/\/$/, '')}/models`, {
170
+ method: 'GET',
171
+ headers: { 'Authorization': `Bearer ${config.apiKey}` }
172
+ });
173
+ const latency = Date.now() - startTime;
174
+ if (response.ok) {
175
+ return { success: true, latency };
176
+ }
177
+ else {
178
+ const errorText = await response.text().catch(() => 'Unknown error');
179
+ const hint = response.status === 401
180
+ ? '(请确认是 MiniMax 的 API Key)'
181
+ : response.status === 404
182
+ ? '(端点不存在 — 请检查 baseUrl)'
183
+ : '';
184
+ return {
185
+ success: false,
186
+ error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
187
+ latency
188
+ };
189
+ }
190
+ }
191
+ catch (error) {
192
+ return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
193
+ }
194
+ }
195
+ getAllProviderInfo() {
196
+ return AUDIO_PROVIDER_INFO;
197
+ }
198
+ }
199
+ export const audioConfigStore = new AudioConfigStore();
@@ -56,7 +56,7 @@ export const DEFAULT_PROVIDER_CONFIGS = {
56
56
  enabled: false,
57
57
  apiKey: '',
58
58
  baseUrl: 'https://api.minimaxi.com/v1',
59
- model: 'MiniMax-M2',
59
+ model: 'MiniMax-M3',
60
60
  temperature: 0.7,
61
61
  maxTokens: 4096,
62
62
  requiresApiKey: true
@@ -108,16 +108,21 @@ export const DEFAULT_PROVIDER_CONFIGS = {
108
108
  }
109
109
  };
110
110
  export const PROVIDER_INFO = {
111
- openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true },
112
- anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true },
111
+ 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'] },
112
+ anthropic: { name: 'Anthropic', description: 'Claude 3.5 系列模型', requiresApiKey: true, models: ['claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229'] },
113
113
  openrouter: { name: 'OpenRouter', description: '聚合多个 AI 供应商', requiresApiKey: true },
114
- gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true },
114
+ gemini: { name: 'Google Gemini', description: 'Gemini 系列模型', requiresApiKey: true, models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'] },
115
115
  ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
116
- minimax: { name: 'MiniMax', description: '国产大模型服务', requiresApiKey: true },
117
- deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true },
118
- kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true },
119
- glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true },
120
- qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true },
116
+ minimax: {
117
+ name: 'MiniMax',
118
+ description: '国产大模型服务',
119
+ requiresApiKey: true,
120
+ models: ['MiniMax-M3', 'MiniMax-M2.7', 'MiniMax-M2', 'MiniMax-M2.1-highspeed', 'MiniMax-M2.7-highspeed']
121
+ },
122
+ deepseek: { name: 'DeepSeek', description: '深度求索大模型', requiresApiKey: true, models: ['deepseek-chat', 'deepseek-reasoner'] },
123
+ kimi: { name: 'Kimi (月之暗面)', description: 'Moonshot 长上下文模型', requiresApiKey: true, models: ['moonshot-v1-8k', 'moonshot-v1-32k', 'moonshot-v1-128k'] },
124
+ glm: { name: 'GLM (智谱)', description: '智谱 ChatGLM 系列模型', requiresApiKey: true, models: ['glm-4-flash', 'glm-4', 'glm-4-plus', 'glm-4-air', 'glm-4-airx'] },
125
+ qwen: { name: 'Qwen (通义千问)', description: '阿里云通义千问系列', requiresApiKey: true, models: ['qwen-plus', 'qwen-max', 'qwen-turbo', 'qwen-long'] },
121
126
  local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
122
127
  };
123
128
  function getDefaultConfig() {
@@ -284,7 +289,12 @@ class LLMConfigStore {
284
289
  }
285
290
  else {
286
291
  const errorText = await response.text().catch(() => 'Unknown error');
287
- return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 100)}`, latency };
292
+ const hint = response.status === 401
293
+ ? '(API Key 无效或不匹配该供应商 — 请检查是否复制完整、有无多余空格)'
294
+ : response.status === 404
295
+ ? '(端点不存在 — 请检查 baseUrl)'
296
+ : '';
297
+ return { success: false, error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`, latency };
288
298
  }
289
299
  }
290
300
  catch (error) {
package/dist/llm/pi-ai.js CHANGED
@@ -133,7 +133,7 @@ export class PiAIModel {
133
133
  ollama: this.config.model || 'llama3.2',
134
134
  openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
135
135
  gemini: this.config.model || 'gemini-2.0-flash',
136
- minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
136
+ minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M3',
137
137
  deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-chat',
138
138
  kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'moonshot-v1-8k',
139
139
  glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-4-flash',
@@ -401,7 +401,7 @@ function detectModel(provider) {
401
401
  ollama: 'llama3.2',
402
402
  openrouter: 'anthropic/claude-3.5-sonnet',
403
403
  gemini: 'gemini-2.0-flash',
404
- minimax: 'MiniMax-M2.7',
404
+ minimax: 'MiniMax-M3',
405
405
  deepseek: 'deepseek-chat',
406
406
  kimi: 'moonshot-v1-8k',
407
407
  glm: 'glm-4-flash',
@@ -19,6 +19,16 @@ export const DEFAULT_VIDEO_PROVIDER_CONFIGS = {
19
19
  duration: 5,
20
20
  ratio: '16:9',
21
21
  requiresApiKey: true
22
+ },
23
+ 'minimax-video': {
24
+ enabled: false,
25
+ apiKey: '',
26
+ baseUrl: 'https://api.minimaxi.com/v1',
27
+ model: 'MiniMax-video-01',
28
+ resolution: '720p',
29
+ duration: 6,
30
+ ratio: '16:9',
31
+ requiresApiKey: true
22
32
  }
23
33
  };
24
34
  export const VIDEO_PROVIDER_INFO = {
@@ -27,6 +37,12 @@ export const VIDEO_PROVIDER_INFO = {
27
37
  description: '字节跳动文生视频 / 图生视频模型',
28
38
  requiresApiKey: true,
29
39
  docs: 'https://www.volcengine.com/docs/82379'
40
+ },
41
+ 'minimax-video': {
42
+ name: 'MiniMax Video',
43
+ description: 'MiniMax 文生视频 (Video-01)',
44
+ requiresApiKey: true,
45
+ docs: 'https://platform.minimaxi.com/document/Video%20Generation'
30
46
  }
31
47
  };
32
48
  function getDefaultConfig() {
@@ -38,6 +54,14 @@ function getDefaultConfig() {
38
54
  apiKey: process.env.SEEDANCE_API_KEY || process.env.ARK_API_KEY || ''
39
55
  };
40
56
  }
57
+ const sharedKey = process.env.MINIMAX_API_KEY || '';
58
+ if (sharedKey) {
59
+ envConfigs['minimax-video'] = {
60
+ ...DEFAULT_VIDEO_PROVIDER_CONFIGS['minimax-video'],
61
+ enabled: true,
62
+ apiKey: sharedKey
63
+ };
64
+ }
41
65
  const activeProvider = 'seedance';
42
66
  const providers = { ...DEFAULT_VIDEO_PROVIDER_CONFIGS };
43
67
  for (const [provider, config] of Object.entries(envConfigs)) {
@@ -153,9 +177,15 @@ class VideoConfigStore {
153
177
  }
154
178
  else {
155
179
  const errorText = await response.text().catch(() => 'Unknown error');
180
+ // 401 通常是 key 错误,给出针对性提示
181
+ const hint = response.status === 401
182
+ ? '(请确认是火山方舟 ARK 的 API Key,不是 MiniMax / 其他平台)'
183
+ : response.status === 404
184
+ ? '(端点不存在 — 火山方舟可能没有 /models,请检查 baseUrl)'
185
+ : '';
156
186
  return {
157
187
  success: false,
158
- error: `HTTP ${response.status}: ${errorText.substring(0, 200)}`,
188
+ error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
159
189
  latency
160
190
  };
161
191
  }
@@ -41,7 +41,7 @@ const CONFIG_FILE = path.join(PI_ECOSYSTEM_DIR, 'config.json');
41
41
  * Default Pi ecosystem configuration (mirrors oh-pi)
42
42
  */
43
43
  const DEFAULT_CONFIG = {
44
- model: process.env.MINIMAX_MODEL || 'MiniMax-M2.7',
44
+ model: process.env.MINIMAX_MODEL || 'MiniMax-M3',
45
45
  extensions: [
46
46
  'safe-guard',
47
47
  'git-guard',
@@ -92,7 +92,8 @@
92
92
 
93
93
  <div class="form-group">
94
94
  <label>模型</label>
95
- <input type="text" id="modelInput" placeholder="如 gpt-4">
95
+ <input type="text" id="modelInput" placeholder="如 gpt-4" list="modelSuggestList">
96
+ <datalist id="modelSuggestList"></datalist>
96
97
  <p class="form-hint" id="modelHint"></p>
97
98
  </div>
98
99
 
@@ -358,6 +359,17 @@
358
359
  document.getElementById('baseUrlInput').value = p.baseUrl || '';
359
360
  document.getElementById('modelInput').value = p.model || '';
360
361
 
362
+ // 填充模型下拉建议(仅 LLM 类型有 providerInfo.models)
363
+ const datalist = document.getElementById('modelSuggestList');
364
+ datalist.innerHTML = '';
365
+ if (type === 'llm' && i.models && Array.isArray(i.models)) {
366
+ for (const m of i.models) {
367
+ const opt = document.createElement('option');
368
+ opt.value = m;
369
+ datalist.appendChild(opt);
370
+ }
371
+ }
372
+
361
373
  // 视频/音频/LLM 专用字段显隐
362
374
  const isVideo = type === 'video';
363
375
  const isAudio = type === 'audio';