@bolloon/bolloon-agent 0.1.30 → 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.
- package/dist/agents/pi-sdk.js +10 -1
- package/dist/index.js +13 -1
- package/dist/llm/audio-config-store.js +199 -0
- package/dist/llm/config-store.js +80 -6
- package/dist/llm/pi-ai.js +30 -2
- package/dist/llm/video-config-store.js +201 -0
- package/dist/pi-ecosystem/index.js +1 -1
- package/dist/web/api-config.html +308 -53
- package/dist/web/client.js +375 -8
- package/dist/web/server.js +324 -5
- package/dist/web/style.css +83 -0
- package/package.json +1 -1
- package/src/agents/pi-sdk.ts +9 -1
- package/src/index.ts +13 -1
- package/src/llm/audio-config-store.ts +246 -0
- package/src/llm/config-store.ts +21 -11
- package/src/llm/pi-ai.ts +22 -2
- package/src/llm/video-config-store.ts +257 -0
- package/src/web/api-config.html +308 -53
- package/src/web/client.js +375 -8
- package/src/web/server.ts +354 -5
- package/src/web/style.css +83 -0
package/dist/agents/pi-sdk.js
CHANGED
|
@@ -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
|
-
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -1310,6 +1310,10 @@ function printHelp() {
|
|
|
1310
1310
|
|
|
1311
1311
|
环境变量:
|
|
1312
1312
|
MINIMAX_API_KEY MiniMax API 密钥
|
|
1313
|
+
DEEPSEEK_API_KEY DeepSeek API 密钥
|
|
1314
|
+
KIMI_API_KEY / MOONSHOT_API_KEY Kimi/Moonshot API 密钥
|
|
1315
|
+
GLM_API_KEY / ZHIPU_API_KEY 智谱 GLM API 密钥
|
|
1316
|
+
QWEN_API_KEY / DASHSCOPE_API_KEY 通义千问 API 密钥
|
|
1313
1317
|
OPENAI_API_KEY OpenAI API 密钥(Pi SDK)
|
|
1314
1318
|
ANTHROPIC_API_KEY Anthropic API 密钥(Pi SDK)
|
|
1315
1319
|
PORT Web 服务端口(默认 54188)
|
|
@@ -1365,6 +1369,10 @@ async function main() {
|
|
|
1365
1369
|
const hasOpenAI = !!process.env.OPENAI_API_KEY;
|
|
1366
1370
|
const hasAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
|
1367
1371
|
const hasMinimax = !!process.env.MINIMAX_API_KEY;
|
|
1372
|
+
const hasDeepSeek = !!process.env.DEEPSEEK_API_KEY;
|
|
1373
|
+
const hasKimi = !!(process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY);
|
|
1374
|
+
const hasGlm = !!(process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY);
|
|
1375
|
+
const hasQwen = !!(process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY);
|
|
1368
1376
|
const hasOpenRouter = !!process.env.OPENROUTER_API_KEY;
|
|
1369
1377
|
const hasGemini = !!process.env.GEMINI_API_KEY;
|
|
1370
1378
|
const hasOllama = !!process.env.OLLAMA_BASE_URL;
|
|
@@ -1373,7 +1381,11 @@ async function main() {
|
|
|
1373
1381
|
hasOpenRouter ? 'OpenRouter' :
|
|
1374
1382
|
hasGemini ? 'Gemini' :
|
|
1375
1383
|
hasOllama ? 'Ollama' :
|
|
1376
|
-
hasMinimax ? 'MiniMax' :
|
|
1384
|
+
hasMinimax ? 'MiniMax' :
|
|
1385
|
+
hasDeepSeek ? 'DeepSeek' :
|
|
1386
|
+
hasKimi ? 'Kimi' :
|
|
1387
|
+
hasGlm ? 'GLM' :
|
|
1388
|
+
hasQwen ? 'Qwen' : null;
|
|
1377
1389
|
if (llmProvider) {
|
|
1378
1390
|
s.step(0, 4, `LLM: ${llmProvider}`, 'ok');
|
|
1379
1391
|
initMinimax({ provider: llmProvider.toLowerCase() });
|
|
@@ -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();
|
package/dist/llm/config-store.js
CHANGED
|
@@ -56,7 +56,43 @@ export const DEFAULT_PROVIDER_CONFIGS = {
|
|
|
56
56
|
enabled: false,
|
|
57
57
|
apiKey: '',
|
|
58
58
|
baseUrl: 'https://api.minimaxi.com/v1',
|
|
59
|
-
model: 'MiniMax-
|
|
59
|
+
model: 'MiniMax-M3',
|
|
60
|
+
temperature: 0.7,
|
|
61
|
+
maxTokens: 4096,
|
|
62
|
+
requiresApiKey: true
|
|
63
|
+
},
|
|
64
|
+
deepseek: {
|
|
65
|
+
enabled: false,
|
|
66
|
+
apiKey: '',
|
|
67
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
68
|
+
model: 'deepseek-chat',
|
|
69
|
+
temperature: 0.7,
|
|
70
|
+
maxTokens: 4096,
|
|
71
|
+
requiresApiKey: true
|
|
72
|
+
},
|
|
73
|
+
kimi: {
|
|
74
|
+
enabled: false,
|
|
75
|
+
apiKey: '',
|
|
76
|
+
baseUrl: 'https://api.moonshot.cn/v1',
|
|
77
|
+
model: 'moonshot-v1-8k',
|
|
78
|
+
temperature: 0.7,
|
|
79
|
+
maxTokens: 4096,
|
|
80
|
+
requiresApiKey: true
|
|
81
|
+
},
|
|
82
|
+
glm: {
|
|
83
|
+
enabled: false,
|
|
84
|
+
apiKey: '',
|
|
85
|
+
baseUrl: 'https://open.bigmodel.cn/api/paas/v4',
|
|
86
|
+
model: 'glm-4-flash',
|
|
87
|
+
temperature: 0.7,
|
|
88
|
+
maxTokens: 4096,
|
|
89
|
+
requiresApiKey: true
|
|
90
|
+
},
|
|
91
|
+
qwen: {
|
|
92
|
+
enabled: false,
|
|
93
|
+
apiKey: '',
|
|
94
|
+
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
95
|
+
model: 'qwen-plus',
|
|
60
96
|
temperature: 0.7,
|
|
61
97
|
maxTokens: 4096,
|
|
62
98
|
requiresApiKey: true
|
|
@@ -72,12 +108,21 @@ export const DEFAULT_PROVIDER_CONFIGS = {
|
|
|
72
108
|
}
|
|
73
109
|
};
|
|
74
110
|
export const PROVIDER_INFO = {
|
|
75
|
-
openai: { name: 'OpenAI', description: 'GPT-4, GPT-3.5 等模型', requiresApiKey: true },
|
|
76
|
-
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'] },
|
|
77
113
|
openrouter: { name: 'OpenRouter', description: '聚合多个 AI 供应商', requiresApiKey: true },
|
|
78
|
-
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'] },
|
|
79
115
|
ollama: { name: 'Ollama', description: '本地 LLM 运行框架', requiresApiKey: false },
|
|
80
|
-
minimax: {
|
|
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'] },
|
|
81
126
|
local: { name: '本地模型', description: '本地部署的模型服务', requiresApiKey: false }
|
|
82
127
|
};
|
|
83
128
|
function getDefaultConfig() {
|
|
@@ -97,6 +142,18 @@ function getDefaultConfig() {
|
|
|
97
142
|
if (process.env.MINIMAX_API_KEY) {
|
|
98
143
|
envConfigs.minimax = { ...DEFAULT_PROVIDER_CONFIGS.minimax, enabled: true, apiKey: process.env.MINIMAX_API_KEY };
|
|
99
144
|
}
|
|
145
|
+
if (process.env.DEEPSEEK_API_KEY) {
|
|
146
|
+
envConfigs.deepseek = { ...DEFAULT_PROVIDER_CONFIGS.deepseek, enabled: true, apiKey: process.env.DEEPSEEK_API_KEY };
|
|
147
|
+
}
|
|
148
|
+
if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY) {
|
|
149
|
+
envConfigs.kimi = { ...DEFAULT_PROVIDER_CONFIGS.kimi, enabled: true, apiKey: process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || '' };
|
|
150
|
+
}
|
|
151
|
+
if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY) {
|
|
152
|
+
envConfigs.glm = { ...DEFAULT_PROVIDER_CONFIGS.glm, enabled: true, apiKey: process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY || '' };
|
|
153
|
+
}
|
|
154
|
+
if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY) {
|
|
155
|
+
envConfigs.qwen = { ...DEFAULT_PROVIDER_CONFIGS.qwen, enabled: true, apiKey: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '' };
|
|
156
|
+
}
|
|
100
157
|
if (process.env.OLLAMA_BASE_URL) {
|
|
101
158
|
envConfigs.ollama = { ...DEFAULT_PROVIDER_CONFIGS.ollama, enabled: true, baseUrl: process.env.OLLAMA_BASE_URL };
|
|
102
159
|
}
|
|
@@ -111,6 +168,14 @@ function getDefaultConfig() {
|
|
|
111
168
|
activeProvider = 'gemini';
|
|
112
169
|
else if (process.env.MINIMAX_API_KEY)
|
|
113
170
|
activeProvider = 'minimax';
|
|
171
|
+
else if (process.env.DEEPSEEK_API_KEY)
|
|
172
|
+
activeProvider = 'deepseek';
|
|
173
|
+
else if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY)
|
|
174
|
+
activeProvider = 'kimi';
|
|
175
|
+
else if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY)
|
|
176
|
+
activeProvider = 'glm';
|
|
177
|
+
else if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY)
|
|
178
|
+
activeProvider = 'qwen';
|
|
114
179
|
else if (process.env.OLLAMA_BASE_URL)
|
|
115
180
|
activeProvider = 'ollama';
|
|
116
181
|
const providers = { ...DEFAULT_PROVIDER_CONFIGS };
|
|
@@ -224,7 +289,12 @@ class LLMConfigStore {
|
|
|
224
289
|
}
|
|
225
290
|
else {
|
|
226
291
|
const errorText = await response.text().catch(() => 'Unknown error');
|
|
227
|
-
|
|
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 };
|
|
228
298
|
}
|
|
229
299
|
}
|
|
230
300
|
catch (error) {
|
|
@@ -237,6 +307,10 @@ class LLMConfigStore {
|
|
|
237
307
|
case 'openai':
|
|
238
308
|
case 'openrouter':
|
|
239
309
|
case 'minimax':
|
|
310
|
+
case 'deepseek':
|
|
311
|
+
case 'kimi':
|
|
312
|
+
case 'glm':
|
|
313
|
+
case 'qwen':
|
|
240
314
|
if (config.apiKey)
|
|
241
315
|
headers['Authorization'] = `Bearer ${config.apiKey}`;
|
|
242
316
|
break;
|
package/dist/llm/pi-ai.js
CHANGED
|
@@ -68,6 +68,10 @@ export class PiAIModel {
|
|
|
68
68
|
switch (this.provider) {
|
|
69
69
|
case 'openai':
|
|
70
70
|
case 'minimax':
|
|
71
|
+
case 'deepseek':
|
|
72
|
+
case 'kimi':
|
|
73
|
+
case 'glm':
|
|
74
|
+
case 'qwen':
|
|
71
75
|
return this.callOpenAI(messages, temperature, maxTokens);
|
|
72
76
|
case 'anthropic':
|
|
73
77
|
return this.callAnthropic(messages, temperature, maxTokens);
|
|
@@ -94,6 +98,10 @@ export class PiAIModel {
|
|
|
94
98
|
openrouter: process.env.OPENROUTER_API_KEY || '',
|
|
95
99
|
gemini: process.env.GEMINI_API_KEY || '',
|
|
96
100
|
minimax: process.env.MINIMAX_API_KEY || '',
|
|
101
|
+
deepseek: process.env.DEEPSEEK_API_KEY || '',
|
|
102
|
+
kimi: process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY || '',
|
|
103
|
+
glm: process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY || '',
|
|
104
|
+
qwen: process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY || '',
|
|
97
105
|
local: ''
|
|
98
106
|
};
|
|
99
107
|
return envVars[this.provider] || '';
|
|
@@ -110,6 +118,10 @@ export class PiAIModel {
|
|
|
110
118
|
openrouter: process.env.OPENROUTER_BASE_URL || 'https://openrouter.ai/api/v1',
|
|
111
119
|
gemini: 'https://generativelanguage.googleapis.com/v1beta',
|
|
112
120
|
minimax: process.env.MINIMAX_BASE_URL || 'https://api.minimaxi.com/v1',
|
|
121
|
+
deepseek: process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/v1',
|
|
122
|
+
kimi: process.env.KIMI_BASE_URL || process.env.MOONSHOT_BASE_URL || 'https://api.moonshot.cn/v1',
|
|
123
|
+
glm: process.env.GLM_BASE_URL || process.env.ZHIPU_BASE_URL || 'https://open.bigmodel.cn/api/paas/v4',
|
|
124
|
+
qwen: process.env.QWEN_BASE_URL || process.env.DASHSCOPE_BASE_URL || 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
113
125
|
local: 'http://localhost:11434'
|
|
114
126
|
};
|
|
115
127
|
return baseUrls[this.provider];
|
|
@@ -121,7 +133,11 @@ export class PiAIModel {
|
|
|
121
133
|
ollama: this.config.model || 'llama3.2',
|
|
122
134
|
openrouter: this.config.model || 'anthropic/claude-3.5-sonnet',
|
|
123
135
|
gemini: this.config.model || 'gemini-2.0-flash',
|
|
124
|
-
minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-
|
|
136
|
+
minimax: this.config.model || process.env.MINIMAX_MODEL || 'MiniMax-M3',
|
|
137
|
+
deepseek: this.config.model || process.env.DEEPSEEK_MODEL || 'deepseek-chat',
|
|
138
|
+
kimi: this.config.model || process.env.KIMI_MODEL || process.env.MOONSHOT_MODEL || 'moonshot-v1-8k',
|
|
139
|
+
glm: this.config.model || process.env.GLM_MODEL || process.env.ZHIPU_MODEL || 'glm-4-flash',
|
|
140
|
+
qwen: this.config.model || process.env.QWEN_MODEL || process.env.DASHSCOPE_MODEL || 'qwen-plus',
|
|
125
141
|
local: this.config.model || 'llama3.2'
|
|
126
142
|
};
|
|
127
143
|
return modelMap[this.provider];
|
|
@@ -368,6 +384,14 @@ function detectProvider() {
|
|
|
368
384
|
return 'ollama';
|
|
369
385
|
if (process.env.MINIMAX_API_KEY)
|
|
370
386
|
return 'minimax';
|
|
387
|
+
if (process.env.DEEPSEEK_API_KEY)
|
|
388
|
+
return 'deepseek';
|
|
389
|
+
if (process.env.KIMI_API_KEY || process.env.MOONSHOT_API_KEY)
|
|
390
|
+
return 'kimi';
|
|
391
|
+
if (process.env.GLM_API_KEY || process.env.ZHIPU_API_KEY)
|
|
392
|
+
return 'glm';
|
|
393
|
+
if (process.env.QWEN_API_KEY || process.env.DASHSCOPE_API_KEY)
|
|
394
|
+
return 'qwen';
|
|
371
395
|
return 'openai';
|
|
372
396
|
}
|
|
373
397
|
function detectModel(provider) {
|
|
@@ -377,7 +401,11 @@ function detectModel(provider) {
|
|
|
377
401
|
ollama: 'llama3.2',
|
|
378
402
|
openrouter: 'anthropic/claude-3.5-sonnet',
|
|
379
403
|
gemini: 'gemini-2.0-flash',
|
|
380
|
-
minimax: 'MiniMax-
|
|
404
|
+
minimax: 'MiniMax-M3',
|
|
405
|
+
deepseek: 'deepseek-chat',
|
|
406
|
+
kimi: 'moonshot-v1-8k',
|
|
407
|
+
glm: 'glm-4-flash',
|
|
408
|
+
qwen: 'qwen-plus',
|
|
381
409
|
local: 'llama3.2'
|
|
382
410
|
};
|
|
383
411
|
return defaults[provider];
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Video Generation API Configuration Store
|
|
3
|
+
*
|
|
4
|
+
* 视频生成模型配置(与 LLM 完全独立)。当前内置 Seedance(火山引擎 ARK)。
|
|
5
|
+
* Seedance 任务流: POST /contents/generations/tasks → 轮询 GET /contents/generations/tasks/{id}
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
const CONFIG_DIR = path.join(process.env.HOME || '/tmp', '.bolloon');
|
|
10
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'video-config.json');
|
|
11
|
+
export const DEFAULT_VIDEO_PROVIDER_CONFIGS = {
|
|
12
|
+
seedance: {
|
|
13
|
+
enabled: false,
|
|
14
|
+
apiKey: '',
|
|
15
|
+
baseUrl: 'https://ark.cn-beijing.volces.com/api/v3',
|
|
16
|
+
// 文生视频 lite 版(也支持图生视频,加 --resolution 等参数)
|
|
17
|
+
model: 'doubao-seedance-1-0-lite-t2v-250428',
|
|
18
|
+
resolution: '720p',
|
|
19
|
+
duration: 5,
|
|
20
|
+
ratio: '16:9',
|
|
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
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
export const VIDEO_PROVIDER_INFO = {
|
|
35
|
+
seedance: {
|
|
36
|
+
name: 'Seedance (火山方舟)',
|
|
37
|
+
description: '字节跳动文生视频 / 图生视频模型',
|
|
38
|
+
requiresApiKey: true,
|
|
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'
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function getDefaultConfig() {
|
|
49
|
+
const envConfigs = {};
|
|
50
|
+
if (process.env.SEEDANCE_API_KEY || process.env.ARK_API_KEY) {
|
|
51
|
+
envConfigs.seedance = {
|
|
52
|
+
...DEFAULT_VIDEO_PROVIDER_CONFIGS.seedance,
|
|
53
|
+
enabled: true,
|
|
54
|
+
apiKey: process.env.SEEDANCE_API_KEY || process.env.ARK_API_KEY || ''
|
|
55
|
+
};
|
|
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
|
+
}
|
|
65
|
+
const activeProvider = 'seedance';
|
|
66
|
+
const providers = { ...DEFAULT_VIDEO_PROVIDER_CONFIGS };
|
|
67
|
+
for (const [provider, config] of Object.entries(envConfigs)) {
|
|
68
|
+
if (config) {
|
|
69
|
+
providers[provider] = config;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
activeProvider,
|
|
74
|
+
providers,
|
|
75
|
+
updatedAt: new Date().toISOString()
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
class VideoConfigStore {
|
|
79
|
+
config = null;
|
|
80
|
+
initialized = false;
|
|
81
|
+
async initialize() {
|
|
82
|
+
if (this.initialized)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
86
|
+
const data = await fs.readFile(CONFIG_PATH, 'utf-8');
|
|
87
|
+
const loadedConfig = JSON.parse(data);
|
|
88
|
+
// 确保加载的配置包含所有默认供应商
|
|
89
|
+
const defaultProviders = Object.keys(DEFAULT_VIDEO_PROVIDER_CONFIGS);
|
|
90
|
+
for (const provider of defaultProviders) {
|
|
91
|
+
if (!loadedConfig.providers[provider]) {
|
|
92
|
+
loadedConfig.providers[provider] = { ...DEFAULT_VIDEO_PROVIDER_CONFIGS[provider] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const activeProvider = loadedConfig.activeProvider;
|
|
96
|
+
if (!activeProvider || !DEFAULT_VIDEO_PROVIDER_CONFIGS[activeProvider]) {
|
|
97
|
+
loadedConfig.activeProvider = 'seedance';
|
|
98
|
+
}
|
|
99
|
+
this.config = loadedConfig;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
this.config = getDefaultConfig();
|
|
103
|
+
await this.save();
|
|
104
|
+
}
|
|
105
|
+
this.initialized = true;
|
|
106
|
+
}
|
|
107
|
+
async save() {
|
|
108
|
+
if (!this.config)
|
|
109
|
+
return;
|
|
110
|
+
this.config.updatedAt = new Date().toISOString();
|
|
111
|
+
await fs.writeFile(CONFIG_PATH, JSON.stringify(this.config, null, 2));
|
|
112
|
+
}
|
|
113
|
+
async getConfig() {
|
|
114
|
+
await this.initialize();
|
|
115
|
+
return { ...this.config };
|
|
116
|
+
}
|
|
117
|
+
async getProvider(provider) {
|
|
118
|
+
await this.initialize();
|
|
119
|
+
return this.config?.providers[provider] || null;
|
|
120
|
+
}
|
|
121
|
+
async getActiveProvider() {
|
|
122
|
+
await this.initialize();
|
|
123
|
+
return this.config?.activeProvider || 'seedance';
|
|
124
|
+
}
|
|
125
|
+
async getActiveProviderConfig() {
|
|
126
|
+
await this.initialize();
|
|
127
|
+
const provider = this.config?.activeProvider;
|
|
128
|
+
if (!provider)
|
|
129
|
+
return null;
|
|
130
|
+
return this.config?.providers[provider] || null;
|
|
131
|
+
}
|
|
132
|
+
async setActiveProvider(provider) {
|
|
133
|
+
await this.initialize();
|
|
134
|
+
if (!this.config?.providers[provider]) {
|
|
135
|
+
throw new Error(`Unknown video provider: ${provider}`);
|
|
136
|
+
}
|
|
137
|
+
this.config.activeProvider = provider;
|
|
138
|
+
await this.save();
|
|
139
|
+
}
|
|
140
|
+
async updateProvider(provider, updates) {
|
|
141
|
+
await this.initialize();
|
|
142
|
+
if (!this.config?.providers[provider]) {
|
|
143
|
+
throw new Error(`Unknown video provider: ${provider}`);
|
|
144
|
+
}
|
|
145
|
+
this.config.providers[provider] = {
|
|
146
|
+
...this.config.providers[provider],
|
|
147
|
+
...updates
|
|
148
|
+
};
|
|
149
|
+
await this.save();
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* 测试连接:只校验 API key 是否能被 ARK 接受(创建任务失败不算连接失败,
|
|
153
|
+
* 返回 401/403 才算失败)。
|
|
154
|
+
*/
|
|
155
|
+
async testProvider(provider) {
|
|
156
|
+
await this.initialize();
|
|
157
|
+
const config = this.config?.providers[provider];
|
|
158
|
+
if (!config) {
|
|
159
|
+
return { success: false, error: 'Provider not configured' };
|
|
160
|
+
}
|
|
161
|
+
if (!config.enabled) {
|
|
162
|
+
return { success: false, error: 'Provider is not enabled' };
|
|
163
|
+
}
|
|
164
|
+
if (config.requiresApiKey && !config.apiKey) {
|
|
165
|
+
return { success: false, error: 'API key is required' };
|
|
166
|
+
}
|
|
167
|
+
const startTime = Date.now();
|
|
168
|
+
try {
|
|
169
|
+
// 列出可用模型(轻量级健康检查)。ARK 端点:GET /models
|
|
170
|
+
const response = await fetch(`${config.baseUrl.replace(/\/$/, '')}/models`, {
|
|
171
|
+
method: 'GET',
|
|
172
|
+
headers: { 'Authorization': `Bearer ${config.apiKey}` }
|
|
173
|
+
});
|
|
174
|
+
const latency = Date.now() - startTime;
|
|
175
|
+
if (response.ok) {
|
|
176
|
+
return { success: true, latency };
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
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
|
+
: '';
|
|
186
|
+
return {
|
|
187
|
+
success: false,
|
|
188
|
+
error: `HTTP ${response.status}: ${errorText.substring(0, 500)}${hint ? ' ' + hint : ''}`,
|
|
189
|
+
latency
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
return { success: false, error: error.message || 'Connection failed', latency: Date.now() - startTime };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
getAllProviderInfo() {
|
|
198
|
+
return VIDEO_PROVIDER_INFO;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export const videoConfigStore = new VideoConfigStore();
|
|
@@ -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-
|
|
44
|
+
model: process.env.MINIMAX_MODEL || 'MiniMax-M3',
|
|
45
45
|
extensions: [
|
|
46
46
|
'safe-guard',
|
|
47
47
|
'git-guard',
|