imtoagent 0.2.0
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/README.md +234 -0
- package/bin/imtoagent +453 -0
- package/index.ts +1129 -0
- package/modules/agent/claude-adapter.ts +258 -0
- package/modules/agent/claude.ts +160 -0
- package/modules/agent/codex-adapter.ts +232 -0
- package/modules/agent/codex-exec-server.ts +513 -0
- package/modules/agent/codex.ts +275 -0
- package/modules/agent/opencode-adapter.ts +308 -0
- package/modules/agent/opencode.ts +247 -0
- package/modules/bot-context.ts +26 -0
- package/modules/capabilities.ts +189 -0
- package/modules/cli/setup.ts +424 -0
- package/modules/core/config.ts +275 -0
- package/modules/core/error.ts +124 -0
- package/modules/core/index.ts +39 -0
- package/modules/core/runtime.ts +282 -0
- package/modules/core/session.ts +256 -0
- package/modules/core/stats.ts +92 -0
- package/modules/core/types.ts +250 -0
- package/modules/im/feishu.ts +731 -0
- package/modules/im/telegram.ts +639 -0
- package/modules/im/wechat.ts +1094 -0
- package/modules/im/wecom.ts +603 -0
- package/modules/media/feishu-inbound-adapter.ts +108 -0
- package/modules/media/index.ts +27 -0
- package/modules/media/media-store.ts +273 -0
- package/modules/media/resolver.ts +178 -0
- package/modules/media/telegram-inbound-adapter.ts +124 -0
- package/modules/media/types.ts +76 -0
- package/modules/prompt-builder.ts +123 -0
- package/modules/proxy/anthropic-proxy.ts +1083 -0
- package/modules/proxy/codex-proxy.ts +657 -0
- package/modules/rate-limiter.ts +58 -0
- package/modules/types.ts +144 -0
- package/modules/utils/backend-check.ts +121 -0
- package/modules/utils/paths.ts +218 -0
- package/package.json +53 -0
- package/scripts/postinstall.ts +70 -0
- package/templates/config.template.json +57 -0
- package/templates/opencode.template.json +28 -0
- package/templates/providers.template.json +19 -0
- package/templates/soul.template/identity.md +6 -0
- package/templates/soul.template/profile.md +11 -0
- package/templates/soul.template/rules.md +7 -0
- package/templates/soul.template/skills.md +3 -0
- package/templates/soul.template/workspace.md +4 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// setup.ts — 交互式配置向导
|
|
3
|
+
// ================================================================
|
|
4
|
+
// 零依赖,使用 Bun 原生 prompt()
|
|
5
|
+
// 通过 `imtoagent setup` 调用
|
|
6
|
+
// ================================================================
|
|
7
|
+
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
import * as os from 'os';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as readline from 'readline';
|
|
12
|
+
import { getDataDir, getPkgDir, getTemplatePath, getTemplateSoulPath, getSoulDir } from '../utils/paths';
|
|
13
|
+
import { checkAllBackends, formatBackendStatus, checkBackend } from '../utils/backend-check';
|
|
14
|
+
|
|
15
|
+
// ================================================================
|
|
16
|
+
// 主流程
|
|
17
|
+
// ================================================================
|
|
18
|
+
export async function runSetupWizard(): Promise<void> {
|
|
19
|
+
const dataDir = getDataDir();
|
|
20
|
+
const configPath = path.join(dataDir, 'config.json');
|
|
21
|
+
const providersPath = path.join(dataDir, 'providers.json');
|
|
22
|
+
const opencodePath = path.join(dataDir, 'opencode.json');
|
|
23
|
+
|
|
24
|
+
console.log('\n╔══════════════════════════════════════════════╗');
|
|
25
|
+
console.log('║ 🚀 imtoagent 配置向导 ║');
|
|
26
|
+
console.log('╚══════════════════════════════════════════════╝\n');
|
|
27
|
+
console.log(`数据目录: ${dataDir}\n`);
|
|
28
|
+
|
|
29
|
+
// ===== Step 1: 检测已有配置 =====
|
|
30
|
+
let existingConfig: any = null;
|
|
31
|
+
if (fs.existsSync(configPath)) {
|
|
32
|
+
console.log('📋 检测到已有配置。');
|
|
33
|
+
console.log(' [1] 覆盖现有配置');
|
|
34
|
+
console.log(' [2] 合并(保留现有 bot,添加新的)');
|
|
35
|
+
console.log(' [3] 退出');
|
|
36
|
+
|
|
37
|
+
const choice = await prompt('请选择 (1/2/3): ');
|
|
38
|
+
if (choice === '3' || choice.toLowerCase() === 'exit') {
|
|
39
|
+
console.log('👋 已取消');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
45
|
+
} catch {
|
|
46
|
+
console.log('⚠️ 现有配置文件解析失败,将重新生成');
|
|
47
|
+
existingConfig = null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ===== Step 1.5: 检测后端安装状态 =====
|
|
52
|
+
console.log('📌 检测后端 Agent...\n');
|
|
53
|
+
const backendStatus = checkAllBackends();
|
|
54
|
+
console.log(formatBackendStatus(backendStatus));
|
|
55
|
+
|
|
56
|
+
const installedBackends = backendStatus.filter(b => b.installed);
|
|
57
|
+
if (installedBackends.length === 0) {
|
|
58
|
+
console.log('\n⚠️ 未检测到任何后端 Agent。');
|
|
59
|
+
console.log('你需要先安装至少一个后端 Agent 才能使用 imtoagent。');
|
|
60
|
+
console.log('\n推荐安装:');
|
|
61
|
+
console.log(' npm install -g @anthropic-ai/claude-agent-sdk # Claude Code');
|
|
62
|
+
console.log(' npm install -g @openai/codex # Codex');
|
|
63
|
+
console.log(' npm install -g opencode # OpenCode');
|
|
64
|
+
const proceed = await promptChoice('暂不配置 Bot,先退出?', ['Y', 'N']);
|
|
65
|
+
if (proceed === 'Y') {
|
|
66
|
+
console.log('\n👋 安装后端后请重新运行 "imtoagent setup"');
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
console.log('\n⚠️ 你可以继续配置 Bot,但启动网关后发消息会报错,直到后端安装完成。\n');
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`\n✅ 已安装 ${installedBackends.length} 个后端: ${installedBackends.map(b => b.label).join(', ')}\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ===== Step 2: 配置 Bot =====
|
|
75
|
+
console.log('📌 Step 2: 配置 Bot\n');
|
|
76
|
+
|
|
77
|
+
const bots: any[] = existingConfig?.bots && (await promptChoice('保留现有 Bot?', ['Y', 'N'])) !== 'N'
|
|
78
|
+
? [...existingConfig.bots]
|
|
79
|
+
: [];
|
|
80
|
+
|
|
81
|
+
let addMore = true;
|
|
82
|
+
while (addMore) {
|
|
83
|
+
console.log(`\n--- 添加新 Bot (已有 ${bots.length} 个) ---`);
|
|
84
|
+
|
|
85
|
+
const name = await prompt('Bot 名称 (如 ClaudeBot): ');
|
|
86
|
+
if (!name) { addMore = false; continue; }
|
|
87
|
+
|
|
88
|
+
const imType = await promptChoice('IM 平台', ['feishu', 'telegram']);
|
|
89
|
+
|
|
90
|
+
// 后端选项:优先推荐已安装的,未安装的标 ⚠️
|
|
91
|
+
const backendOptions = backendStatus.map(b => {
|
|
92
|
+
const label = b.installed ? `${b.label} (v${b.version})` : `${b.label} ⚠️ 未安装`;
|
|
93
|
+
return { value: b.type, label };
|
|
94
|
+
});
|
|
95
|
+
const backendLabels = backendOptions.map(o => o.label);
|
|
96
|
+
const backendChoice = await promptChoice('后端', backendLabels);
|
|
97
|
+
const backend = backendOptions.find(o => o.label === backendChoice)?.value || 'claude';
|
|
98
|
+
const isBackendInstalled = backendStatus.find(b => b.type === backend)?.installed;
|
|
99
|
+
|
|
100
|
+
if (!isBackendInstalled) {
|
|
101
|
+
const installCmd = backendStatus.find(b => b.type === backend)?.installHint || '';
|
|
102
|
+
console.log(`\n⚠️ ${backend} 未安装。`);
|
|
103
|
+
console.log(` 安装提示: ${installCmd}\n`);
|
|
104
|
+
|
|
105
|
+
const rl = readline.createInterface({
|
|
106
|
+
input: process.stdin,
|
|
107
|
+
output: process.stdout,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const answer = await new Promise<string>((resolve) => {
|
|
111
|
+
rl.question(`🔧 是否现在自动安装?[Y/n]: `, resolve);
|
|
112
|
+
});
|
|
113
|
+
rl.close();
|
|
114
|
+
|
|
115
|
+
if (answer.trim().toLowerCase() !== 'n') {
|
|
116
|
+
const { installBackend } = await import('../utils/backend-check');
|
|
117
|
+
const ok = await installBackend(backend as 'claude' | 'codex' | 'opencode');
|
|
118
|
+
if (!ok) {
|
|
119
|
+
console.log(`\n⚠️ 安装失败,你可以稍后手动运行: ${installCmd}`);
|
|
120
|
+
const rl2 = readline.createInterface({
|
|
121
|
+
input: process.stdin,
|
|
122
|
+
output: process.stdout,
|
|
123
|
+
});
|
|
124
|
+
const retry = await new Promise<string>((resolve) => {
|
|
125
|
+
rl2.question('是否仍然选择此 Bot?[y/N]: ', resolve);
|
|
126
|
+
});
|
|
127
|
+
rl2.close();
|
|
128
|
+
if (retry.trim().toLowerCase() !== 'y') {
|
|
129
|
+
addMore = (await promptChoice('继续添加其他 Bot?', ['Y', 'N'])) !== 'Y';
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`✅ ${backend} 已安装,继续配置。\n`);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.log(`跳过安装。你可以稍后手动运行: ${installCmd}\n`);
|
|
137
|
+
const confirm = await promptChoice('仍要继续配置此 Bot?', ['Y', 'N']);
|
|
138
|
+
if (confirm !== 'Y') {
|
|
139
|
+
addMore = (await promptChoice('继续添加其他 Bot?', ['Y', 'N'])) !== 'Y';
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let appId = '', appSecret = '', proxy = '', cwd = '';
|
|
146
|
+
|
|
147
|
+
if (imType === 'feishu') {
|
|
148
|
+
appId = await prompt('飞书 App ID (cli_...): ');
|
|
149
|
+
appSecret = await prompt('飞书 App Secret: ');
|
|
150
|
+
} else {
|
|
151
|
+
appId = await prompt('Telegram Bot Token: ');
|
|
152
|
+
proxy = await prompt('代理地址 (留空不使用代理): ') || '';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
cwd = await prompt('工作目录 (如 /Users/keyi/Desktop,留空用默认): ') || os.homedir();
|
|
156
|
+
|
|
157
|
+
const bot: any = {
|
|
158
|
+
name,
|
|
159
|
+
appId,
|
|
160
|
+
appSecret,
|
|
161
|
+
backend,
|
|
162
|
+
im: imType === 'feishu' ? undefined : 'telegram',
|
|
163
|
+
cwd,
|
|
164
|
+
};
|
|
165
|
+
if (proxy) bot.proxy = proxy;
|
|
166
|
+
|
|
167
|
+
// 检查重名
|
|
168
|
+
const existing = bots.findIndex(b => b.name === name);
|
|
169
|
+
if (existing >= 0) {
|
|
170
|
+
bots[existing] = bot;
|
|
171
|
+
console.log(`✅ 已替换: ${name}`);
|
|
172
|
+
} else {
|
|
173
|
+
bots.push(bot);
|
|
174
|
+
console.log(`✅ 已添加: ${name}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addMore = (await promptChoice('继续添加 Bot?', ['Y', 'N'])) === 'Y';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (bots.length === 0) {
|
|
181
|
+
console.log('\n⚠️ 未配置任何 Bot。');
|
|
182
|
+
if ((await promptChoice('至少配置一个 Bot 吗?', ['Y', 'N'])) === 'Y') {
|
|
183
|
+
return runSetupWizard(); // 重新开始
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ===== Step 3: 配置模型供应商 =====
|
|
188
|
+
console.log('\n📌 Step 3: 配置模型供应商\n');
|
|
189
|
+
|
|
190
|
+
const providers: Record<string, any> = {};
|
|
191
|
+
let existingProviders = existingConfig?.providers || {};
|
|
192
|
+
const keepProviders = Object.keys(existingProviders).length > 0
|
|
193
|
+
&& (await promptChoice('保留现有供应商?', ['Y', 'N'])) !== 'N';
|
|
194
|
+
|
|
195
|
+
if (keepProviders) {
|
|
196
|
+
Object.assign(providers, existingProviders);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
let addProvider = true;
|
|
200
|
+
while (addProvider) {
|
|
201
|
+
console.log('\n--- 添加新供应商 ---');
|
|
202
|
+
const provName = await prompt('供应商名称 (如 deepseek, dashscope): ');
|
|
203
|
+
if (!provName) { addProvider = false; continue; }
|
|
204
|
+
|
|
205
|
+
if (providers[provName]) {
|
|
206
|
+
console.log(`⚠️ 供应商 "${provName}" 已存在,将覆盖`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const baseUrl = await prompt('Base URL (如 https://api.deepseek.com/v1): ');
|
|
210
|
+
const apiKey = await prompt('API Key: ');
|
|
211
|
+
const modelsStr = await prompt('模型列表 (逗号分隔,如 deepseek-v4-pro,deepseek-v4-flash): ');
|
|
212
|
+
const models = modelsStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
213
|
+
const format = await promptChoice('API 格式', ['openai', 'anthropic']);
|
|
214
|
+
|
|
215
|
+
const pricing: any = {};
|
|
216
|
+
const priceInput = await prompt('价格 (入/出 每百万 Token,如 0.55,2.19,留空跳过): ');
|
|
217
|
+
if (priceInput) {
|
|
218
|
+
const parts = priceInput.split(',').map(s => parseFloat(s.trim()));
|
|
219
|
+
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
|
|
220
|
+
pricing.inputPerMillion = parts[0];
|
|
221
|
+
pricing.outputPerMillion = parts[1];
|
|
222
|
+
pricing.currency = 'USD';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
providers[provName] = { baseUrl, apiKey, models, format, ...(Object.keys(pricing).length ? { pricing } : {}) };
|
|
227
|
+
console.log(`✅ 已添加: ${provName}`);
|
|
228
|
+
|
|
229
|
+
addProvider = (await promptChoice('继续添加供应商?', ['Y', 'N'])) === 'Y';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ===== Step 4: 选择默认模型 =====
|
|
233
|
+
console.log('\n📌 Step 4: 选择默认模型\n');
|
|
234
|
+
|
|
235
|
+
const allModels: string[] = [];
|
|
236
|
+
for (const [provName, prov] of Object.entries(providers)) {
|
|
237
|
+
for (const m of (prov as any).models || []) {
|
|
238
|
+
allModels.push(`${provName}/${m}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let defaultModel = '';
|
|
243
|
+
if (allModels.length > 0) {
|
|
244
|
+
const existingDefault = existingConfig?.defaultModel || allModels[0];
|
|
245
|
+
const defaultChoice = await prompt(`默认模型 [${existingDefault}]: `);
|
|
246
|
+
defaultModel = defaultChoice || existingDefault;
|
|
247
|
+
} else {
|
|
248
|
+
defaultModel = await prompt('默认模型 (供应商/模型名): ') || 'deepseek/deepseek-v4-pro';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ===== Step 5: 生成灵魂文件 =====
|
|
252
|
+
console.log('\n📌 Step 5: 生成灵魂文件\n');
|
|
253
|
+
|
|
254
|
+
for (const bot of bots) {
|
|
255
|
+
const botSoulDir = getSoulDir(bot.name);
|
|
256
|
+
const templateSoulDir = path.join(getPkgDir(), 'templates', 'soul.template');
|
|
257
|
+
|
|
258
|
+
if (fs.existsSync(botSoulDir) && (await promptChoice(`已存在 ${bot.name} 的灵魂文件,重新生成?`, ['Y', 'N'])) !== 'Y') {
|
|
259
|
+
console.log(`⏭ 跳过: ${bot.name}`);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
fs.mkdirSync(botSoulDir, { recursive: true });
|
|
264
|
+
|
|
265
|
+
const soulFiles = ['rules.md', 'identity.md', 'profile.md', 'workspace.md', 'skills.md'];
|
|
266
|
+
for (const sf of soulFiles) {
|
|
267
|
+
const tmplPath = path.join(templateSoulDir, sf);
|
|
268
|
+
const destPath = path.join(botSoulDir, sf);
|
|
269
|
+
|
|
270
|
+
if (fs.existsSync(destPath) && !fs.existsSync(tmplPath)) {
|
|
271
|
+
continue; // 已有且无模板,保留
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (fs.existsSync(tmplPath)) {
|
|
275
|
+
let content = fs.readFileSync(tmplPath, 'utf-8');
|
|
276
|
+
// 替换模板变量
|
|
277
|
+
content = content.replace(/\{\{backend\}\}/g, bot.backend);
|
|
278
|
+
content = content.replace(/\{\{cwd\}\}/g, bot.cwd || os.homedir());
|
|
279
|
+
content = content.replace(/\{\{botName\}\}/g, bot.name);
|
|
280
|
+
fs.writeFileSync(destPath, content);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
console.log(`✅ ${bot.name}: 灵魂文件已生成 → ${botSoulDir}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ===== Step 6: 写入配置文件 =====
|
|
287
|
+
console.log('\n📌 Step 6: 写入配置文件\n');
|
|
288
|
+
|
|
289
|
+
// 确保数据目录存在
|
|
290
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
291
|
+
|
|
292
|
+
// config.json
|
|
293
|
+
const config: any = {
|
|
294
|
+
system: existingConfig?.system || {
|
|
295
|
+
defaultProjectDir: os.homedir(),
|
|
296
|
+
idleTimeoutMinutes: 30,
|
|
297
|
+
maxReplyLength: 140000,
|
|
298
|
+
},
|
|
299
|
+
providers,
|
|
300
|
+
defaultModel,
|
|
301
|
+
modelAliases: existingConfig?.modelAliases || buildDefaultAliases(defaultModel),
|
|
302
|
+
execServer: existingConfig?.execServer || {
|
|
303
|
+
enabled: true,
|
|
304
|
+
startupTimeoutMs: 15000,
|
|
305
|
+
fallbackToExec: true,
|
|
306
|
+
},
|
|
307
|
+
codex: existingConfig?.codex || {
|
|
308
|
+
reportedModel: 'gpt-5.5',
|
|
309
|
+
model: defaultModel.split('/')[1] || 'deepseek-v4-pro',
|
|
310
|
+
upstream: (providers[defaultModel.split('/')[0]]?.baseUrl || 'https://api.deepseek.com/v1') + '/chat/completions',
|
|
311
|
+
},
|
|
312
|
+
opencode: existingConfig?.opencode || {
|
|
313
|
+
serverUrl: 'http://localhost:4096',
|
|
314
|
+
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
315
|
+
},
|
|
316
|
+
rateLimit: existingConfig?.rateLimit || {
|
|
317
|
+
enabled: true,
|
|
318
|
+
maxRequests: 30,
|
|
319
|
+
windowMs: 60000,
|
|
320
|
+
},
|
|
321
|
+
shutdown: existingConfig?.shutdown || {
|
|
322
|
+
gracePeriodMs: 5000,
|
|
323
|
+
},
|
|
324
|
+
bots,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
328
|
+
console.log(`✅ ${configPath}`);
|
|
329
|
+
|
|
330
|
+
// providers.json
|
|
331
|
+
const providersFile: any = {
|
|
332
|
+
providers,
|
|
333
|
+
defaultModel,
|
|
334
|
+
modelAliases: config.modelAliases,
|
|
335
|
+
};
|
|
336
|
+
fs.writeFileSync(providersPath, JSON.stringify(providersFile, null, 2) + '\n');
|
|
337
|
+
console.log(`✅ ${providersPath}`);
|
|
338
|
+
|
|
339
|
+
// opencode.json(从模板复制)
|
|
340
|
+
const opencodeTemplate = getTemplatePath('opencode.template.json');
|
|
341
|
+
if (fs.existsSync(opencodeTemplate)) {
|
|
342
|
+
const opencodeContent = fs.readFileSync(opencodeTemplate, 'utf-8');
|
|
343
|
+
fs.writeFileSync(opencodePath, opencodeContent);
|
|
344
|
+
console.log(`✅ ${opencodePath}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// 创建必要的子目录
|
|
348
|
+
fs.mkdirSync(path.join(dataDir, 'sessions'), { recursive: true });
|
|
349
|
+
fs.mkdirSync(path.join(dataDir, 'logs'), { recursive: true });
|
|
350
|
+
console.log('✅ 子目录已创建 (sessions/, logs/)');
|
|
351
|
+
|
|
352
|
+
// ===== Step 7: 完成 =====
|
|
353
|
+
console.log('\n╔══════════════════════════════════════════════╗');
|
|
354
|
+
console.log('║ ✅ 配置完成! ║');
|
|
355
|
+
console.log('╚══════════════════════════════════════════════╝\n');
|
|
356
|
+
console.log(`Bot: ${bots.map(b => b.name).join(', ')}`);
|
|
357
|
+
console.log(`默认模型: ${defaultModel}`);
|
|
358
|
+
console.log(`供应商: ${Object.keys(providers).join(', ')}`);
|
|
359
|
+
console.log(`\n下一步:`);
|
|
360
|
+
console.log(` imtoagent start 启动网关`);
|
|
361
|
+
console.log(` imtoagent status 查看状态`);
|
|
362
|
+
console.log();
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ================================================================
|
|
366
|
+
// 工具函数
|
|
367
|
+
// ================================================================
|
|
368
|
+
|
|
369
|
+
/** 构建默认模型别名 */
|
|
370
|
+
function buildDefaultAliases(defaultModel: string): Record<string, string> {
|
|
371
|
+
return {
|
|
372
|
+
default: defaultModel,
|
|
373
|
+
sonnet: defaultModel,
|
|
374
|
+
opus: defaultModel,
|
|
375
|
+
haiku: defaultModel,
|
|
376
|
+
best: defaultModel,
|
|
377
|
+
opencode: defaultModel,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 交互式 prompt(Bun 原生) */
|
|
382
|
+
async function prompt(question: string): Promise<string> {
|
|
383
|
+
// Bun 环境使用原生 prompt
|
|
384
|
+
if (typeof Bun !== 'undefined' && typeof Bun.stdin !== 'undefined') {
|
|
385
|
+
// 尝试使用 readline
|
|
386
|
+
const readline = await import('readline');
|
|
387
|
+
const rl = readline.createInterface({
|
|
388
|
+
input: process.stdin,
|
|
389
|
+
output: process.stdout,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
return new Promise(resolve => {
|
|
393
|
+
rl.question(question, answer => {
|
|
394
|
+
rl.close();
|
|
395
|
+
resolve(answer.trim());
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// fallback
|
|
401
|
+
console.error('⚠️ 无法读取用户输入,请检查运行环境');
|
|
402
|
+
return '';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** 选项选择(Y/N 或自定义选项) */
|
|
406
|
+
async function promptChoice(question: string, options: string[]): Promise<string> {
|
|
407
|
+
const optStr = options.join('/');
|
|
408
|
+
const answer = await prompt(`${question} [${optStr}]: `);
|
|
409
|
+
const upper = answer.toUpperCase();
|
|
410
|
+
|
|
411
|
+
// 精确匹配(全大写比较,返回原始值)
|
|
412
|
+
const exact = options.find(o => o.toUpperCase() === upper);
|
|
413
|
+
if (exact) return exact;
|
|
414
|
+
|
|
415
|
+
// 简写匹配(首字母)
|
|
416
|
+
if (upper.length === 1) {
|
|
417
|
+
const short = options.find(o => o[0].toUpperCase() === upper);
|
|
418
|
+
if (short) return short;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 默认返回第一个
|
|
422
|
+
if (!answer && options.length > 0) return options[0];
|
|
423
|
+
return upper || options[0];
|
|
424
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// ConfigManager — 配置管理
|
|
3
|
+
// ================================================================
|
|
4
|
+
// 从 config.json 和 providers.json 读取,支持 bot 级别配置持久化
|
|
5
|
+
// ================================================================
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
import type { ConfigManager, BotConfig, ProviderConfig } from './types';
|
|
11
|
+
import { getDataDir, getSessionsDir } from '../utils/paths';
|
|
12
|
+
|
|
13
|
+
/** 全局 config.json 结构 */
|
|
14
|
+
interface RawConfig {
|
|
15
|
+
system?: {
|
|
16
|
+
defaultProjectDir?: string;
|
|
17
|
+
idleTimeoutMinutes?: number;
|
|
18
|
+
maxReplyLength?: number;
|
|
19
|
+
};
|
|
20
|
+
providers?: Record<string, {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
models?: string[];
|
|
24
|
+
format?: string;
|
|
25
|
+
pricing?: { inputPerMillion: number; outputPerMillion: number; currency?: string };
|
|
26
|
+
}>;
|
|
27
|
+
defaultModel?: string;
|
|
28
|
+
activeModel?: string;
|
|
29
|
+
modelAliases?: Record<string, string>;
|
|
30
|
+
bots?: Array<{
|
|
31
|
+
name: string;
|
|
32
|
+
appId: string;
|
|
33
|
+
appSecret: string;
|
|
34
|
+
backend: string;
|
|
35
|
+
cwd?: string;
|
|
36
|
+
}>;
|
|
37
|
+
execServer?: { enabled: boolean; startupTimeoutMs: number; fallbackToExec: boolean };
|
|
38
|
+
codex?: any;
|
|
39
|
+
opencode?: any;
|
|
40
|
+
rateLimit?: any;
|
|
41
|
+
shutdown?: any;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Bot 级别配置(持久化在 sessions 目录) */
|
|
45
|
+
interface BotLevelConfig {
|
|
46
|
+
activeModel?: string;
|
|
47
|
+
modelAliases?: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ================================================================
|
|
51
|
+
// FileConfigManager
|
|
52
|
+
// ================================================================
|
|
53
|
+
|
|
54
|
+
export class FileConfigManager implements ConfigManager {
|
|
55
|
+
private rawConfig: RawConfig | null = null;
|
|
56
|
+
private providerConfigs: Map<string, ProviderConfig> = new Map();
|
|
57
|
+
private botConfigs = new Map<string, BotLevelConfig>();
|
|
58
|
+
|
|
59
|
+
constructor() {
|
|
60
|
+
this.loadAll();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 加载所有配置文件 */
|
|
64
|
+
private loadAll(): void {
|
|
65
|
+
// 加载主配置
|
|
66
|
+
try {
|
|
67
|
+
const configPath = path.join(getDataDir(), 'config.json');
|
|
68
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
69
|
+
this.rawConfig = JSON.parse(raw);
|
|
70
|
+
} catch (e: any) {
|
|
71
|
+
console.error(`[Config] 加载 config.json 失败: ${e.message}`);
|
|
72
|
+
this.rawConfig = {} as RawConfig;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 加载 providers.json
|
|
76
|
+
try {
|
|
77
|
+
const provPath = path.join(getDataDir(), 'providers.json');
|
|
78
|
+
const raw = fs.readFileSync(provPath, 'utf-8');
|
|
79
|
+
const provData = JSON.parse(raw);
|
|
80
|
+
|
|
81
|
+
for (const [name, p] of Object.entries(provData.providers || {}) as [string, any][]) {
|
|
82
|
+
this.providerConfigs.set(name, {
|
|
83
|
+
baseUrl: p.baseUrl || '',
|
|
84
|
+
apiKey: p.apiKey || '',
|
|
85
|
+
model: (p.models && p.models[0]) || '',
|
|
86
|
+
format: (p.format as 'anthropic' | 'openai') || 'anthropic',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
} catch (e: any) {
|
|
90
|
+
console.error(`[Config] 加载 providers.json 失败: ${e.message}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 加载默认 providers
|
|
94
|
+
this._loadDefaultProviders();
|
|
95
|
+
|
|
96
|
+
// 加载各 bot 的模型配置
|
|
97
|
+
if (this.rawConfig?.bots) {
|
|
98
|
+
for (const bot of this.rawConfig.bots) {
|
|
99
|
+
this._loadBotConfig(bot.name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 从 config.json 中的 providers 加载默认 provider */
|
|
105
|
+
private _loadDefaultProviders(): void {
|
|
106
|
+
if (!this.rawConfig?.providers) return;
|
|
107
|
+
|
|
108
|
+
for (const [name, p] of Object.entries(this.rawConfig.providers)) {
|
|
109
|
+
if (!this.providerConfigs.has(name)) {
|
|
110
|
+
this.providerConfigs.set(name, {
|
|
111
|
+
baseUrl: p.baseUrl || '',
|
|
112
|
+
apiKey: p.apiKey || '',
|
|
113
|
+
model: (p.models && p.models[0]) || '',
|
|
114
|
+
format: (p.format as 'anthropic' | 'openai') || 'anthropic',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** 加载 Bot 级别配置 */
|
|
121
|
+
private _loadBotConfig(botName: string): void {
|
|
122
|
+
const sessionsDir = getSessionsDir();
|
|
123
|
+
const configPath = path.join(sessionsDir, `${botName}_config.json`);
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
if (fs.existsSync(configPath)) {
|
|
127
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
128
|
+
this.botConfigs.set(botName, JSON.parse(raw));
|
|
129
|
+
} else {
|
|
130
|
+
this.botConfigs.set(botName, {});
|
|
131
|
+
}
|
|
132
|
+
} catch (e: any) {
|
|
133
|
+
console.error(`[Config] 加载 bot ${botName} 配置失败: ${e.message}`);
|
|
134
|
+
this.botConfigs.set(botName, {});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** 保存 Bot 级别配置 */
|
|
139
|
+
private _saveBotConfig(botName: string, config: BotLevelConfig): void {
|
|
140
|
+
const sessionsDir = getSessionsDir();
|
|
141
|
+
const configPath = path.join(sessionsDir, `${botName}_config.json`);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
145
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
this.botConfigs.set(botName, config);
|
|
148
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
149
|
+
} catch (e: any) {
|
|
150
|
+
console.error(`[Config] 保存 bot ${botName} 配置失败: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ================================================================
|
|
155
|
+
// 接口实现
|
|
156
|
+
// ================================================================
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 通过路径获取配置值,如 "system.defaultProjectDir"
|
|
160
|
+
*/
|
|
161
|
+
get<T>(configPath: string): T {
|
|
162
|
+
if (!this.rawConfig) return undefined as T;
|
|
163
|
+
|
|
164
|
+
const keys = configPath.split('.');
|
|
165
|
+
let current: any = this.rawConfig;
|
|
166
|
+
|
|
167
|
+
for (const key of keys) {
|
|
168
|
+
if (current == null) return undefined as T;
|
|
169
|
+
current = current[key];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return current as T;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* 获取 Bot 配置
|
|
177
|
+
*/
|
|
178
|
+
getBotConfig(name: string): BotConfig | null {
|
|
179
|
+
if (!this.rawConfig?.bots) return null;
|
|
180
|
+
|
|
181
|
+
const bot = this.rawConfig.bots.find(b => b.name === name);
|
|
182
|
+
if (!bot) return null;
|
|
183
|
+
|
|
184
|
+
const botLevel = this.botConfigs.get(name) || {};
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
name: bot.name,
|
|
188
|
+
backend: bot.backend,
|
|
189
|
+
appId: bot.appId,
|
|
190
|
+
appSecret: bot.appSecret,
|
|
191
|
+
cwd: bot.cwd,
|
|
192
|
+
activeModel: botLevel.activeModel || this.getActiveModel(),
|
|
193
|
+
modelAliases: botLevel.modelAliases || this.getActiveModelAliases(),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 获取 Provider 配置
|
|
199
|
+
*/
|
|
200
|
+
getProviderConfig(providerId: string): ProviderConfig | null {
|
|
201
|
+
return this.providerConfigs.get(providerId) || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 获取当前活跃模型
|
|
206
|
+
*/
|
|
207
|
+
getActiveModel(): string {
|
|
208
|
+
const cfg = this.rawConfig;
|
|
209
|
+
return cfg?.activeModel || cfg?.defaultModel || 'deepseek/deepseek-v4-pro';
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* 解析模型规格(处理 alias 和 provider/model 格式)
|
|
214
|
+
*/
|
|
215
|
+
resolveModel(modelSpec: string): string {
|
|
216
|
+
const aliases = this.getActiveModelAliases();
|
|
217
|
+
|
|
218
|
+
// 检查是否为 alias
|
|
219
|
+
if (aliases[modelSpec]) {
|
|
220
|
+
return aliases[modelSpec];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// 已经是 provider/model 格式,直接返回
|
|
224
|
+
if (modelSpec.includes('/')) {
|
|
225
|
+
return modelSpec;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 尝试从 provider 中匹配
|
|
229
|
+
for (const [provName, provCfg] of this.providerConfigs) {
|
|
230
|
+
if (provCfg.model === modelSpec) {
|
|
231
|
+
return `${provName}/${modelSpec}`;
|
|
232
|
+
}
|
|
233
|
+
if (provCfg.models?.includes(modelSpec)) {
|
|
234
|
+
return `${provName}/${modelSpec}`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 返回默认模型
|
|
239
|
+
return this.getActiveModel();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ================================================================
|
|
243
|
+
// Bot 级别模型配置持久化
|
|
244
|
+
// ================================================================
|
|
245
|
+
|
|
246
|
+
/** 获取当前模型别名 */
|
|
247
|
+
private getActiveModelAliases(): Record<string, string> {
|
|
248
|
+
return this.rawConfig?.modelAliases || {};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** 保存 Bot 活跃模型 */
|
|
252
|
+
saveActiveModel(botName: string, modelSpec: string): void {
|
|
253
|
+
const botLevel = this.botConfigs.get(botName) || {};
|
|
254
|
+
botLevel.activeModel = modelSpec;
|
|
255
|
+
this._saveBotConfig(botName, botLevel);
|
|
256
|
+
|
|
257
|
+
// 同时更新全局配置
|
|
258
|
+
if (this.rawConfig) {
|
|
259
|
+
this.rawConfig.activeModel = modelSpec;
|
|
260
|
+
try {
|
|
261
|
+
const configPath = path.join(getDataDir(), 'config.json');
|
|
262
|
+
fs.writeFileSync(configPath, JSON.stringify(this.rawConfig, null, 2) + '\n');
|
|
263
|
+
} catch (e: any) {
|
|
264
|
+
console.error(`[Config] 保存全局 activeModel 失败: ${e.message}`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** 保存 Bot 模型别名 */
|
|
270
|
+
saveModelAliases(botName: string, aliases: Record<string, string>): void {
|
|
271
|
+
const botLevel = this.botConfigs.get(botName) || {};
|
|
272
|
+
botLevel.modelAliases = aliases;
|
|
273
|
+
this._saveBotConfig(botName, botLevel);
|
|
274
|
+
}
|
|
275
|
+
}
|