imtoagent 0.3.24 → 0.3.25
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/bin/imtoagent-real +184 -0
- package/index.ts +31 -4
- package/modules/cli/setup.ts +1 -0
- package/modules/core/types.ts +1 -0
- package/modules/utils/config-manager.ts +359 -0
- package/modules/utils/doctor.ts +462 -0
- package/modules/utils/workspace-manager.ts +23 -3
- package/package.json +1 -1
package/bin/imtoagent-real
CHANGED
|
@@ -61,6 +61,12 @@ switch (command) {
|
|
|
61
61
|
case 'health':
|
|
62
62
|
await cmdHealth();
|
|
63
63
|
break;
|
|
64
|
+
case 'doctor':
|
|
65
|
+
await cmdDoctor();
|
|
66
|
+
break;
|
|
67
|
+
case 'config':
|
|
68
|
+
await cmdConfig();
|
|
69
|
+
break;
|
|
64
70
|
case 'autostart':
|
|
65
71
|
await cmdAutostart();
|
|
66
72
|
break;
|
|
@@ -121,6 +127,13 @@ Usage:
|
|
|
121
127
|
imtoagent uninstall Uninstall imtoagent (keep data by default)
|
|
122
128
|
imtoagent uninstall --purge Uninstall and delete all data
|
|
123
129
|
imtoagent health Run comprehensive health check
|
|
130
|
+
imtoagent doctor Diagnose & fix configuration issues
|
|
131
|
+
imtoagent config Manage Bot configuration
|
|
132
|
+
imtoagent config list List all Bots
|
|
133
|
+
imtoagent config show NAME Show Bot details
|
|
134
|
+
imtoagent config add Add a new Bot
|
|
135
|
+
imtoagent config remove NAME Remove a Bot
|
|
136
|
+
imtoagent config modify NAME Modify Bot settings
|
|
124
137
|
imtoagent autostart enable Enable auto-start on login (launchd)
|
|
125
138
|
imtoagent autostart disable Disable auto-start
|
|
126
139
|
imtoagent autostart status Check auto-start status
|
|
@@ -939,6 +952,177 @@ async function cmdHealth(): Promise<void> {
|
|
|
939
952
|
console.log();
|
|
940
953
|
}
|
|
941
954
|
|
|
955
|
+
// ================================================================
|
|
956
|
+
// config — Bot 配置管理
|
|
957
|
+
// ================================================================
|
|
958
|
+
async function cmdConfig(): Promise<void> {
|
|
959
|
+
const subcommand = process.argv[3];
|
|
960
|
+
|
|
961
|
+
switch (subcommand) {
|
|
962
|
+
case 'list':
|
|
963
|
+
await cmdConfigList();
|
|
964
|
+
break;
|
|
965
|
+
case 'show': {
|
|
966
|
+
const name = process.argv[4];
|
|
967
|
+
if (!name) { console.error('Usage: imtoagent config show <name>'); process.exit(1); }
|
|
968
|
+
await cmdConfigShow(name);
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
case 'add':
|
|
972
|
+
await cmdConfigAdd();
|
|
973
|
+
break;
|
|
974
|
+
case 'remove': {
|
|
975
|
+
const name = process.argv[4];
|
|
976
|
+
if (!name) { console.error('Usage: imtoagent config remove <name>'); process.exit(1); }
|
|
977
|
+
await cmdConfigRemove(name);
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case 'modify': {
|
|
981
|
+
const name = process.argv[4];
|
|
982
|
+
if (!name) { console.error('Usage: imtoagent config modify <name>'); process.exit(1); }
|
|
983
|
+
await cmdConfigModify(name);
|
|
984
|
+
break;
|
|
985
|
+
}
|
|
986
|
+
case undefined:
|
|
987
|
+
case 'help':
|
|
988
|
+
case '--help':
|
|
989
|
+
case '-h':
|
|
990
|
+
console.log(`
|
|
991
|
+
imtoagent config — Manage Bot configuration
|
|
992
|
+
|
|
993
|
+
Usage:
|
|
994
|
+
imtoagent config list List all Bots
|
|
995
|
+
imtoagent config show NAME Show Bot details
|
|
996
|
+
imtoagent config add Add a new Bot (interactive)
|
|
997
|
+
imtoagent config remove NAME Remove a Bot
|
|
998
|
+
imtoagent config modify NAME Modify Bot settings
|
|
999
|
+
`);
|
|
1000
|
+
break;
|
|
1001
|
+
default:
|
|
1002
|
+
console.error(`❌ Unknown config subcommand: ${subcommand}`);
|
|
1003
|
+
console.log(' Run "imtoagent config help" for usage.');
|
|
1004
|
+
process.exit(1);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// ---- Config subcommand wrappers ----
|
|
1009
|
+
async function cmdConfigList(): Promise<void> {
|
|
1010
|
+
const m = await import('../modules/utils/config-manager');
|
|
1011
|
+
await m.cmdConfigList();
|
|
1012
|
+
}
|
|
1013
|
+
async function cmdConfigShow(name: string): Promise<void> {
|
|
1014
|
+
const m = await import('../modules/utils/config-manager');
|
|
1015
|
+
await m.cmdConfigShow(name);
|
|
1016
|
+
}
|
|
1017
|
+
async function cmdConfigAdd(): Promise<void> {
|
|
1018
|
+
const m = await import('../modules/utils/config-manager');
|
|
1019
|
+
await m.cmdConfigAdd();
|
|
1020
|
+
}
|
|
1021
|
+
async function cmdConfigRemove(name: string): Promise<void> {
|
|
1022
|
+
const m = await import('../modules/utils/config-manager');
|
|
1023
|
+
await m.cmdConfigRemove(name);
|
|
1024
|
+
}
|
|
1025
|
+
async function cmdConfigModify(name: string): Promise<void> {
|
|
1026
|
+
const m = await import('../modules/utils/config-manager');
|
|
1027
|
+
await m.cmdConfigModify(name);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
// ================================================================
|
|
1032
|
+
// doctor — 配置诊断与自动修复
|
|
1033
|
+
// ================================================================
|
|
1034
|
+
async function cmdDoctor(): Promise<void> {
|
|
1035
|
+
console.log(`\n🔧 imtoagent Doctor — Configuration Diagnosis\n`);
|
|
1036
|
+
|
|
1037
|
+
try {
|
|
1038
|
+
const { runDoctorChecks, formatIssues } = await import('../modules/utils/doctor');
|
|
1039
|
+
const issues = await runDoctorChecks();
|
|
1040
|
+
|
|
1041
|
+
// 分组
|
|
1042
|
+
const fixableIssues = issues.filter(i => i.fixable);
|
|
1043
|
+
const unfixableIssues = issues.filter(i => !i.fixable);
|
|
1044
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
1045
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
1046
|
+
const infos = issues.filter(i => i.severity === 'info');
|
|
1047
|
+
|
|
1048
|
+
// 打印所有问题
|
|
1049
|
+
if (issues.length === 0) {
|
|
1050
|
+
console.log(' ✅ All checks passed! Nothing to fix.\n');
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// 打印 errors
|
|
1055
|
+
if (errors.length > 0) {
|
|
1056
|
+
console.log('❌ Errors:');
|
|
1057
|
+
for (const e of errors) {
|
|
1058
|
+
console.log(` ${e.message}`);
|
|
1059
|
+
if (e.fixable && e.fixDescription) {
|
|
1060
|
+
console.log(` → 🔧 ${e.fixDescription}`);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
console.log();
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
// 打印 warnings
|
|
1067
|
+
if (warnings.length > 0) {
|
|
1068
|
+
console.log('⚠️ Warnings:');
|
|
1069
|
+
for (const w of warnings) {
|
|
1070
|
+
console.log(` ${w.message}`);
|
|
1071
|
+
}
|
|
1072
|
+
console.log();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// 打印 infos
|
|
1076
|
+
if (infos.length > 0) {
|
|
1077
|
+
console.log('✅ OK:');
|
|
1078
|
+
for (const i of infos) {
|
|
1079
|
+
console.log(` ${i.message}`);
|
|
1080
|
+
}
|
|
1081
|
+
console.log();
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// 尝试自动修复
|
|
1085
|
+
if (fixableIssues.length > 0) {
|
|
1086
|
+
console.log(`─── Auto-Fix ───`);
|
|
1087
|
+
let fixed = 0;
|
|
1088
|
+
for (const issue of fixableIssues) {
|
|
1089
|
+
if (!issue.fix) continue;
|
|
1090
|
+
try {
|
|
1091
|
+
console.log(`\n🔧 Fixing: ${issue.fixDescription}`);
|
|
1092
|
+
const success = await issue.fix();
|
|
1093
|
+
if (success) {
|
|
1094
|
+
console.log(` ✅ Fixed`);
|
|
1095
|
+
fixed++;
|
|
1096
|
+
} else {
|
|
1097
|
+
console.log(` ❌ Fix failed`);
|
|
1098
|
+
}
|
|
1099
|
+
} catch (e: any) {
|
|
1100
|
+
console.log(` ❌ Fix failed: ${e.message}`);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
console.log(`\n ${fixed}/${fixableIssues.length} issues fixed`);
|
|
1104
|
+
if (fixed > 0) {
|
|
1105
|
+
console.log(`\n💡 Run "imtoagent doctor" again to re-check after fixes.\n`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// Summary
|
|
1110
|
+
console.log('── Summary ──');
|
|
1111
|
+
if (errors.length === 0) {
|
|
1112
|
+
console.log(' ✅ No errors found');
|
|
1113
|
+
} else {
|
|
1114
|
+
console.log(` ❌ ${errors.length} error(s) — some may be fixable`);
|
|
1115
|
+
}
|
|
1116
|
+
if (warnings.length > 0) {
|
|
1117
|
+
console.log(` ⚠️ ${warnings.length} warning(s) — review recommended`);
|
|
1118
|
+
}
|
|
1119
|
+
console.log();
|
|
1120
|
+
|
|
1121
|
+
} catch (e: any) {
|
|
1122
|
+
console.error(`❌ Doctor check failed: ${e.message}\n`);
|
|
1123
|
+
process.exit(1);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
942
1126
|
// ================================================================
|
|
943
1127
|
// autostart — launchd integration (macOS only)
|
|
944
1128
|
// ================================================================
|
package/index.ts
CHANGED
|
@@ -289,6 +289,7 @@ interface BotConfig {
|
|
|
289
289
|
appSecret: string;
|
|
290
290
|
backend: 'claude' | 'codex' | 'opencode';
|
|
291
291
|
cwd?: string;
|
|
292
|
+
isAdmin?: boolean; // true = 可以修改网关配置,默认第一个 Bot 为 true
|
|
292
293
|
}
|
|
293
294
|
|
|
294
295
|
// ================================================================
|
|
@@ -307,6 +308,7 @@ class Bot {
|
|
|
307
308
|
soul: string;
|
|
308
309
|
client: Lark.Client;
|
|
309
310
|
im: IMModule;
|
|
311
|
+
isAdmin: boolean;
|
|
310
312
|
config: any;
|
|
311
313
|
workspaceManager: WorkspaceManager;
|
|
312
314
|
|
|
@@ -327,6 +329,7 @@ class Bot {
|
|
|
327
329
|
this.appSecret = cfg.appSecret;
|
|
328
330
|
this.defaultCwd = cfg.cwd || globalConfig.system?.defaultProjectDir || path.join(os.homedir(), 'Projects');
|
|
329
331
|
this.config = globalConfig;
|
|
332
|
+
this.isAdmin = cfg.isAdmin !== undefined ? cfg.isAdmin : true; // 默认 true(后向兼容老用户)
|
|
330
333
|
this.workspaceManager = workspaceManager;
|
|
331
334
|
|
|
332
335
|
// 确保工作空间目录存在
|
|
@@ -412,8 +415,13 @@ class Bot {
|
|
|
412
415
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
413
416
|
const hasFiles = fs.readdirSync(dir).some((f: string) => f.endsWith('.md'));
|
|
414
417
|
if (hasFiles) return;
|
|
418
|
+
// 根据 isAdmin 生成不同的 rules.md
|
|
419
|
+
const rulesMd = this.isAdmin
|
|
420
|
+
? '# Bot Rules\n\nYou are an **admin Bot** with full gateway management privileges.\n\n- You can modify IMtoAgent configuration (config.json, providers, Bot management)\n- You have access to any directory (global workspace mode)\n- Sensitive information such as project keys, tokens, and passwords must not be leaked\n- Destructive commands must not be executed without explicit confirmation'
|
|
421
|
+
: '# Bot Rules\n\nYou are a **non-admin Bot** with restricted privileges.\n\n- ⛔ NEVER modify any files under ~/.imtoagent/ (config.json, providers.json, etc.)\n- ⛔ NEVER read or expose gateway configuration contents\n- ⛔ NEVER attempt to add, remove, or modify Bot configurations\n- Your working directory is limited to your assigned workspace\n- All file operations must stay within your workspace boundary\n- Sensitive information such as project keys, tokens, and passwords must not be leaked\n- Destructive commands must not be executed without explicit confirmation';
|
|
422
|
+
|
|
415
423
|
const defaults: Record<string, string> = {
|
|
416
|
-
'rules.md':
|
|
424
|
+
'rules.md': rulesMd,
|
|
417
425
|
'identity.md': `# Identity\n\n- I am an AI programming assistant connected via IMtoAgent\n- I run on the ${this.backend === 'codex' ? 'Codex' : 'Claude Code'} backend\n- Reply in Chinese`,
|
|
418
426
|
'profile.md': '# User Profile\n\nThis file can be modified by the Agent. When the user says "remember xxx" or "I prefer xxx", the Agent should update this file.\n\n## Modification Guide (Agent Only)\n\nRead this file → Add/delete/modify entries based on user requests → Save',
|
|
419
427
|
'workspace.md': '# Project Environment\n\nAuto-generated by IMtoAgent.',
|
|
@@ -437,11 +445,30 @@ class Bot {
|
|
|
437
445
|
for (const file of order) {
|
|
438
446
|
const fp = dir + '/' + file;
|
|
439
447
|
if (fs.existsSync(fp)) {
|
|
440
|
-
const
|
|
441
|
-
if (
|
|
448
|
+
const c = fs.readFileSync(fp, 'utf-8').trim();
|
|
449
|
+
if (c) parts.push(c);
|
|
442
450
|
}
|
|
443
451
|
}
|
|
444
452
|
} catch {}
|
|
453
|
+
|
|
454
|
+
// Inject config CLI reference — so the Agent knows how to manage Bots via natural language
|
|
455
|
+
const adminTag = this.isAdmin ? '' : ' (you are a non-admin Bot — you cannot use these commands, tell the user to use an admin Bot)';
|
|
456
|
+
const bt = String.fromCharCode(96); // backtick
|
|
457
|
+
const fence = bt + bt + bt;
|
|
458
|
+
const cliRef = '## IMtoAgent Config CLI' +
|
|
459
|
+
'\n\nYou can manage Bot configurations via these CLI commands:' + adminTag +
|
|
460
|
+
'\n\n' + fence + 'bash' +
|
|
461
|
+
'\nimtoagent config list # List all Bots' +
|
|
462
|
+
'\nimtoagent config show <BotName> # Show a Bot\'s details' +
|
|
463
|
+
'\nimtoagent config add # Add a new Bot (interactive)' +
|
|
464
|
+
'\nimtoagent config remove <BotName> # Remove a Bot' +
|
|
465
|
+
'\nimtoagent config modify <BotName> # Modify a Bot\'s settings' +
|
|
466
|
+
'\nimtoagent restore # Hot-reload after config changes' +
|
|
467
|
+
'\nimtoagent doctor # Diagnose config issues' +
|
|
468
|
+
'\n' + fence +
|
|
469
|
+
'\n\nWhen the user asks to manage Bots, use these commands. After changes, run ' + bt + 'imtoagent restore' + bt + '.';
|
|
470
|
+
parts.push(cliRef);
|
|
471
|
+
|
|
445
472
|
return parts.join('\n\n');
|
|
446
473
|
}
|
|
447
474
|
|
|
@@ -499,7 +526,7 @@ class Bot {
|
|
|
499
526
|
: `⏸ ${this.backend} idle | ${this.activeModel}`);
|
|
500
527
|
|
|
501
528
|
cmd('/info', ({ session }) =>
|
|
502
|
-
`🤖 ${this.name} (${this.backend})\nModel: ${this.activeModel}\nDirectory: ${session?.cwd || this.defaultCwd}\nSessions: ${this.sessions.size}`);
|
|
529
|
+
`🤖 ${this.name} (${this.backend})${this.isAdmin ? ' ⭐' : ''}\nModel: ${this.activeModel}\nDirectory: ${session?.cwd || this.defaultCwd}\nSessions: ${this.sessions.size}`);
|
|
503
530
|
|
|
504
531
|
cmd('/stats', ({ session }) => {
|
|
505
532
|
if (!session || session.stats.calls === 0) return '📊 No calls yet';
|
package/modules/cli/setup.ts
CHANGED
package/modules/core/types.ts
CHANGED
|
@@ -242,6 +242,7 @@ export interface BotConfig {
|
|
|
242
242
|
/** 唯一标识(UUID,用于目录/文件隔离,改名不影响) */
|
|
243
243
|
id?: string;
|
|
244
244
|
name: string;
|
|
245
|
+
im: string; // 'feishu' | 'telegram' | 'wecom' | 'wechat'
|
|
245
246
|
backend: string; // 'claude' | 'codex' | 'opencode'
|
|
246
247
|
appId: string;
|
|
247
248
|
appSecret: string;
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// config-manager.ts — 配置管理 CRUD
|
|
3
|
+
// ================================================================
|
|
4
|
+
// imtoagent config list — 列出所有 Bot
|
|
5
|
+
// imtoagent config show NAME — 显示某个 Bot 的完整配置
|
|
6
|
+
// imtoagent config add — 交互式添加 Bot
|
|
7
|
+
// imtoagent config remove NAME — 删除 Bot
|
|
8
|
+
// imtoagent config modify NAME — 修改 Bot 配置
|
|
9
|
+
// ================================================================
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as readline from 'readline';
|
|
14
|
+
import { getDataDir, getConfigPath, getProvidersPath } from './paths';
|
|
15
|
+
import crypto from 'crypto';
|
|
16
|
+
|
|
17
|
+
// ================================================================
|
|
18
|
+
// 类型
|
|
19
|
+
// ================================================================
|
|
20
|
+
|
|
21
|
+
const VALID_BACKENDS = ['claude', 'codex', 'opencode'] as const;
|
|
22
|
+
const VALID_IMS = ['feishu', 'telegram', 'wecom', 'wechat'] as const;
|
|
23
|
+
|
|
24
|
+
export type BackendType = typeof VALID_BACKENDS[number];
|
|
25
|
+
export type IMType = typeof VALID_IMS[number];
|
|
26
|
+
|
|
27
|
+
export interface BotEntry {
|
|
28
|
+
id?: string;
|
|
29
|
+
name: string;
|
|
30
|
+
im: string;
|
|
31
|
+
appId: string;
|
|
32
|
+
appSecret: string;
|
|
33
|
+
backend: string;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RawConfig {
|
|
38
|
+
system?: Record<string, any>;
|
|
39
|
+
providers?: Record<string, any>;
|
|
40
|
+
defaultModel?: string;
|
|
41
|
+
activeModel?: string;
|
|
42
|
+
modelAliases?: Record<string, string>;
|
|
43
|
+
bots?: BotEntry[];
|
|
44
|
+
execServer?: any;
|
|
45
|
+
codex?: any;
|
|
46
|
+
opencode?: any;
|
|
47
|
+
rateLimit?: any;
|
|
48
|
+
shutdown?: any;
|
|
49
|
+
[key: string]: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ================================================================
|
|
53
|
+
// 加载/保存
|
|
54
|
+
// ================================================================
|
|
55
|
+
|
|
56
|
+
function loadConfig(): { config: RawConfig; configPath: string } {
|
|
57
|
+
const configPath = getConfigPath();
|
|
58
|
+
if (!fs.existsSync(configPath)) {
|
|
59
|
+
throw new Error(`config.json not found at ${configPath} — run "imtoagent setup" first`);
|
|
60
|
+
}
|
|
61
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
62
|
+
const config = JSON.parse(raw) as RawConfig;
|
|
63
|
+
return { config, configPath };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function saveConfig(config: RawConfig, configPath: string): void {
|
|
67
|
+
const tmpPath = configPath + '.tmp';
|
|
68
|
+
try {
|
|
69
|
+
fs.writeFileSync(tmpPath, JSON.stringify(config, null, 2) + '\n');
|
|
70
|
+
fs.renameSync(tmpPath, configPath);
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Clean up tmp on failure
|
|
73
|
+
try { fs.unlinkSync(tmpPath); } catch {}
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ================================================================
|
|
79
|
+
// 交互式 prompt(复用 setup 的风格)
|
|
80
|
+
// ================================================================
|
|
81
|
+
|
|
82
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
83
|
+
|
|
84
|
+
function prompt(question: string): Promise<string> {
|
|
85
|
+
return new Promise((resolve) => {
|
|
86
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function confirm(question: string): Promise<boolean> {
|
|
91
|
+
const answer = await prompt(`${question} [y/N] `);
|
|
92
|
+
return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ================================================================
|
|
96
|
+
// config list
|
|
97
|
+
// ================================================================
|
|
98
|
+
|
|
99
|
+
export async function cmdConfigList(): Promise<void> {
|
|
100
|
+
const { config } = loadConfig();
|
|
101
|
+
const bots = config.bots || [];
|
|
102
|
+
|
|
103
|
+
if (bots.length === 0) {
|
|
104
|
+
console.log(' No Bots configured. Run "imtoagent config add" to add one.');
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`\n📋 Configured Bots (${bots.length}):\n`);
|
|
109
|
+
for (const bot of bots) {
|
|
110
|
+
const botId = bot.id ? ` [${bot.id.slice(0, 8)}]` : '';
|
|
111
|
+
const adminTag = bot.isAdmin ? ' ⭐' : '';
|
|
112
|
+
const cwd = bot.cwd ? ` (cwd: ${bot.cwd})` : '';
|
|
113
|
+
console.log(` • ${bot.name}${adminTag}${botId}`);
|
|
114
|
+
console.log(` IM: ${bot.im || "(not set)"} | Backend: ${bot.backend}${cwd}`);
|
|
115
|
+
}
|
|
116
|
+
console.log();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ================================================================
|
|
120
|
+
// config show NAME
|
|
121
|
+
// ================================================================
|
|
122
|
+
|
|
123
|
+
export async function cmdConfigShow(name: string): Promise<void> {
|
|
124
|
+
const { config } = loadConfig();
|
|
125
|
+
const bots = config.bots || [];
|
|
126
|
+
const bot = bots.find(b => b.name === name);
|
|
127
|
+
|
|
128
|
+
if (!bot) {
|
|
129
|
+
console.error(`❌ Bot "${name}" not found`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`\n📋 Bot: ${bot.name}\n`);
|
|
134
|
+
console.log(` ID: ${bot.id || '(auto-generated)'}`);
|
|
135
|
+
console.log(` IM: ${bot.im || "(not set)"}`);
|
|
136
|
+
console.log(` Backend: ${bot.backend}`);
|
|
137
|
+
console.log(` App ID: ${maskSecret(bot.appId)}`);
|
|
138
|
+
console.log(` App Secret: ${maskSecret(bot.appSecret)}`);
|
|
139
|
+
console.log(` Admin: ${bot.isAdmin ? '✅ Yes' : '❌ No'}`);
|
|
140
|
+
if (bot.cwd) console.log(` CWD: ${bot.cwd}`);
|
|
141
|
+
console.log();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function maskSecret(s: string): string {
|
|
145
|
+
if (s.length <= 8) return '***';
|
|
146
|
+
return s.slice(0, 4) + '...' + s.slice(-4);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ================================================================
|
|
150
|
+
// config add
|
|
151
|
+
// ================================================================
|
|
152
|
+
|
|
153
|
+
export async function cmdConfigAdd(): Promise<void> {
|
|
154
|
+
console.log('\n➕ Add a new Bot\n');
|
|
155
|
+
|
|
156
|
+
// 1. Bot name
|
|
157
|
+
let name: string;
|
|
158
|
+
while (true) {
|
|
159
|
+
name = await prompt('Bot name (e.g. MyAssistantBot): ');
|
|
160
|
+
if (!name) { console.log(' ⚠️ Name is required'); continue; }
|
|
161
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) { console.log(' ⚠️ Name can only contain letters, numbers, hyphens, underscores'); continue; }
|
|
162
|
+
|
|
163
|
+
const { config } = loadConfig();
|
|
164
|
+
if (config.bots?.find(b => b.name === name)) {
|
|
165
|
+
console.log(` ⚠️ Bot "${name}" already exists`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 2. IM platform
|
|
172
|
+
console.log('\n IM platforms:');
|
|
173
|
+
console.log(' 1. feishu (飞书/Lark)');
|
|
174
|
+
console.log(' 2. telegram (Telegram)');
|
|
175
|
+
console.log(' 3. wecom (企业微信)');
|
|
176
|
+
console.log(' 4. wechat (个人微信)');
|
|
177
|
+
let im: string;
|
|
178
|
+
while (true) {
|
|
179
|
+
im = await prompt('\n IM platform (1-4 or name): ');
|
|
180
|
+
const numMap: Record<string, string> = { '1': 'feishu', '2': 'telegram', '3': 'wecom', '4': 'wechat' };
|
|
181
|
+
im = numMap[im] || im.toLowerCase();
|
|
182
|
+
if (VALID_IMS.includes(im as IMType)) break;
|
|
183
|
+
console.log(' ⚠️ Valid options: feishu, telegram, wecom, wechat');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 3. IM credentials
|
|
187
|
+
let appId = await prompt(`\n App ID (飞书 App ID / Telegram Bot Token / etc): `);
|
|
188
|
+
if (!appId) { console.log(' ⚠️ App ID is required'); process.exit(1); }
|
|
189
|
+
|
|
190
|
+
let appSecret = await prompt(` App Secret (飞书 App Secret / leave blank for Telegram): `);
|
|
191
|
+
|
|
192
|
+
// 4. Backend
|
|
193
|
+
console.log('\n Backends:');
|
|
194
|
+
console.log(' 1. claude (Claude Code)');
|
|
195
|
+
console.log(' 2. codex (OpenAI Codex)');
|
|
196
|
+
console.log(' 3. opencode (OpenCode)');
|
|
197
|
+
let backend: string;
|
|
198
|
+
while (true) {
|
|
199
|
+
backend = await prompt('\n Backend (1-3 or name): ');
|
|
200
|
+
const numMap: Record<string, string> = { '1': 'claude', '2': 'codex', '3': 'opencode' };
|
|
201
|
+
backend = numMap[backend] || backend.toLowerCase();
|
|
202
|
+
if (VALID_BACKENDS.includes(backend as BackendType)) break;
|
|
203
|
+
console.log(' ⚠️ Valid options: claude, codex, opencode');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 5. Working directory (optional)
|
|
207
|
+
const cwd = await prompt('\n Working directory (leave blank for default): ');
|
|
208
|
+
|
|
209
|
+
// Summary
|
|
210
|
+
console.log('\n── Summary ──');
|
|
211
|
+
console.log(` Name: ${name}`);
|
|
212
|
+
console.log(` IM: ${im}`);
|
|
213
|
+
console.log(` App ID: ${maskSecret(appId)}`);
|
|
214
|
+
console.log(` App Secret: ${appSecret ? maskSecret(appSecret) : '(not set)'}`);
|
|
215
|
+
console.log(` Backend: ${backend}`);
|
|
216
|
+
if (cwd) console.log(` CWD: ${cwd}`);
|
|
217
|
+
console.log();
|
|
218
|
+
|
|
219
|
+
const ok = await confirm('Create this Bot?');
|
|
220
|
+
if (!ok) { console.log(' Cancelled.'); rl.close(); return; }
|
|
221
|
+
|
|
222
|
+
// Apply
|
|
223
|
+
const { config, configPath } = loadConfig();
|
|
224
|
+
if (!config.bots) config.bots = [];
|
|
225
|
+
|
|
226
|
+
const newBot: BotEntry = {
|
|
227
|
+
id: crypto.randomUUID(),
|
|
228
|
+
name,
|
|
229
|
+
im,
|
|
230
|
+
appId,
|
|
231
|
+
appSecret: appSecret || '',
|
|
232
|
+
backend,
|
|
233
|
+
isAdmin: false, // 通过 CLI 添加的 Bot 默认非 admin
|
|
234
|
+
...(cwd ? { cwd } : {}),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
config.bots.push(newBot);
|
|
238
|
+
saveConfig(config, configPath);
|
|
239
|
+
|
|
240
|
+
console.log(`\n✅ Bot "${name}" created!`);
|
|
241
|
+
console.log(` Run "imtoagent restore" to hot-reload the gateway.\n`);
|
|
242
|
+
rl.close();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ================================================================
|
|
246
|
+
// config remove NAME
|
|
247
|
+
// ================================================================
|
|
248
|
+
|
|
249
|
+
export async function cmdConfigRemove(name: string): Promise<void> {
|
|
250
|
+
const { config, configPath } = loadConfig();
|
|
251
|
+
const bots = config.bots || [];
|
|
252
|
+
const idx = bots.findIndex(b => b.name === name);
|
|
253
|
+
|
|
254
|
+
if (idx === -1) {
|
|
255
|
+
console.error(`❌ Bot "${name}" not found`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const bot = bots[idx];
|
|
260
|
+
console.log(`\n🗑️ Remove Bot: ${name}`);
|
|
261
|
+
console.log(` IM: ${bot.im || "(not set)"} | Backend: ${bot.backend}\n`);
|
|
262
|
+
|
|
263
|
+
const ok = await confirm('Are you sure? This cannot be undone');
|
|
264
|
+
if (!ok) { console.log(' Cancelled.'); rl.close(); return; }
|
|
265
|
+
|
|
266
|
+
config.bots = bots.filter((_, i) => i !== idx);
|
|
267
|
+
saveConfig(config, configPath);
|
|
268
|
+
|
|
269
|
+
console.log(`\n✅ Bot "${name}" removed`);
|
|
270
|
+
console.log(` Run "imtoagent restore" to hot-reload the gateway.\n`);
|
|
271
|
+
rl.close();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ================================================================
|
|
275
|
+
// config modify NAME
|
|
276
|
+
// ================================================================
|
|
277
|
+
|
|
278
|
+
export async function cmdConfigModify(name: string): Promise<void> {
|
|
279
|
+
const { config, configPath } = loadConfig();
|
|
280
|
+
const bot = config.bots?.find(b => b.name === name);
|
|
281
|
+
|
|
282
|
+
if (!bot) {
|
|
283
|
+
console.error(`❌ Bot "${name}" not found`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
console.log(`\n✏️ Modify Bot: ${name}\n`);
|
|
288
|
+
console.log(` Current settings:`);
|
|
289
|
+
console.log(` IM: ${bot.im || "(not set)"}`);
|
|
290
|
+
console.log(` App ID: ${maskSecret(bot.appId)}`);
|
|
291
|
+
console.log(` App Secret: ${bot.appSecret ? maskSecret(bot.appSecret) : '(not set)'}`);
|
|
292
|
+
console.log(` Backend: ${bot.backend}`);
|
|
293
|
+
console.log(` CWD: ${bot.cwd || '(default)'}`);
|
|
294
|
+
console.log();
|
|
295
|
+
|
|
296
|
+
console.log(' What do you want to change?');
|
|
297
|
+
console.log(' 1. IM platform');
|
|
298
|
+
console.log(' 2. App ID');
|
|
299
|
+
console.log(' 3. App Secret');
|
|
300
|
+
console.log(' 4. Backend');
|
|
301
|
+
console.log(' 5. Working directory');
|
|
302
|
+
console.log(` 6. Admin status (${bot.isAdmin ? 'ON' : 'OFF'})`);
|
|
303
|
+
console.log(' 7. Quit');
|
|
304
|
+
|
|
305
|
+
const choice = await prompt('\n Choice (1-7): ');
|
|
306
|
+
|
|
307
|
+
switch (choice) {
|
|
308
|
+
case '1': {
|
|
309
|
+
console.log('\n Valid: feishu, telegram, wecom, wechat');
|
|
310
|
+
const newIm = await prompt(` New IM platform (current: ${bot.im}): `);
|
|
311
|
+
if (newIm && VALID_IMS.includes(newIm as IMType)) bot.im = newIm;
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case '2': {
|
|
315
|
+
const newAppId = await prompt(` New App ID (current: ${maskSecret(bot.appId)}): `);
|
|
316
|
+
if (newAppId) bot.appId = newAppId;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
case '3': {
|
|
320
|
+
const newSecret = await prompt(` New App Secret: `);
|
|
321
|
+
if (newSecret) bot.appSecret = newSecret;
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case '4': {
|
|
325
|
+
console.log('\n Valid: claude, codex, opencode');
|
|
326
|
+
const newBackend = await prompt(` New backend (current: ${bot.backend}): `);
|
|
327
|
+
if (newBackend && VALID_BACKENDS.includes(newBackend as BackendType)) bot.backend = newBackend;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case '5': {
|
|
331
|
+
const newCwd = await prompt(` New working directory (current: ${bot.cwd || 'default'}): `);
|
|
332
|
+
if (newCwd) bot.cwd = newCwd;
|
|
333
|
+
else if (newCwd === '') delete bot.cwd;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
case '6': {
|
|
337
|
+
const current = bot.isAdmin ? 'ON' : 'OFF';
|
|
338
|
+
const newVal = await prompt(` Toggle admin status (current: ${current}, yes/no): `);
|
|
339
|
+
if (newVal && (newVal.toLowerCase() === 'yes' || newVal.toLowerCase() === 'y')) {
|
|
340
|
+
bot.isAdmin = !bot.isAdmin;
|
|
341
|
+
console.log(` Admin status → ${bot.isAdmin ? 'ON' : 'OFF'}`);
|
|
342
|
+
}
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case '7':
|
|
346
|
+
console.log(' No changes.');
|
|
347
|
+
rl.close();
|
|
348
|
+
return;
|
|
349
|
+
default:
|
|
350
|
+
console.log(' Invalid choice.');
|
|
351
|
+
rl.close();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
saveConfig(config, configPath);
|
|
356
|
+
console.log(`\n✅ Bot "${name}" updated`);
|
|
357
|
+
console.log(` Run "imtoagent restore" to hot-reload the gateway.\n`);
|
|
358
|
+
rl.close();
|
|
359
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// doctor.ts — 配置诊断与自动修复
|
|
3
|
+
// ================================================================
|
|
4
|
+
// imtoagent doctor
|
|
5
|
+
// 检查 config.json, providers.json, 数据目录, 后端, 端口, API Key 格式等
|
|
6
|
+
// 对可修复问题,用户确认后自动修复
|
|
7
|
+
// ================================================================
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import * as os from 'os';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import { getDataDir, getConfigPath, getProvidersPath, getSessionsDir, getLogsDir, getOpencodeConfigPath } from './paths';
|
|
14
|
+
import { checkBackend, checkAllBackends } from './backend-check';
|
|
15
|
+
|
|
16
|
+
// ================================================================
|
|
17
|
+
// Issue 类型
|
|
18
|
+
// ================================================================
|
|
19
|
+
|
|
20
|
+
export type IssueSeverity = 'error' | 'warning' | 'info';
|
|
21
|
+
|
|
22
|
+
export interface DoctorIssue {
|
|
23
|
+
severity: IssueSeverity;
|
|
24
|
+
category: string;
|
|
25
|
+
message: string;
|
|
26
|
+
fixable: boolean;
|
|
27
|
+
fixDescription?: string;
|
|
28
|
+
fix?: () => Promise<boolean> | boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ================================================================
|
|
32
|
+
// API Key 格式验证
|
|
33
|
+
// ================================================================
|
|
34
|
+
|
|
35
|
+
const API_KEY_PATTERNS: Record<string, { prefix: string; minLength: number }> = {
|
|
36
|
+
// OpenAI 格式
|
|
37
|
+
'openai': { prefix: 'sk-proj-', minLength: 20 },
|
|
38
|
+
// Anthropic 格式
|
|
39
|
+
'anthropic': { prefix: 'sk-ant-', minLength: 20 },
|
|
40
|
+
// 百炼/DashScope 格式
|
|
41
|
+
'dashscope': { prefix: 'sk-', minLength: 20 },
|
|
42
|
+
// 通用 sk- 格式
|
|
43
|
+
'generic-sk': { prefix: 'sk-', minLength: 10 },
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function validateApiKey(key: string): { valid: boolean; reason?: string } {
|
|
47
|
+
if (!key) return { valid: false, reason: 'empty' };
|
|
48
|
+
if (key.includes('YOUR_') || key.includes('xxx') || key.includes('PLACEHOLDER') || key.includes('placeholder')) {
|
|
49
|
+
return { valid: false, reason: 'placeholder value' };
|
|
50
|
+
}
|
|
51
|
+
// 太短的 key 大概率是无效的
|
|
52
|
+
if (key.length < 8) return { valid: false, reason: `too short (${key.length} chars)` };
|
|
53
|
+
return { valid: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ================================================================
|
|
57
|
+
// 诊断检查
|
|
58
|
+
// ================================================================
|
|
59
|
+
|
|
60
|
+
export async function runDoctorChecks(): Promise<DoctorIssue[]> {
|
|
61
|
+
const issues: DoctorIssue[] = [];
|
|
62
|
+
const dataDir = getDataDir();
|
|
63
|
+
const configPath = getConfigPath();
|
|
64
|
+
const providersPath = getProvidersPath();
|
|
65
|
+
const sessionsDir = getSessionsDir();
|
|
66
|
+
const logsDir = getLogsDir();
|
|
67
|
+
|
|
68
|
+
// ---- 1. 数据目录结构 ----
|
|
69
|
+
const requiredDirs = [
|
|
70
|
+
{ path: dataDir, name: 'Data directory (~/.imtoagent/)' },
|
|
71
|
+
{ path: sessionsDir, name: 'Sessions directory' },
|
|
72
|
+
{ path: logsDir, name: 'Logs directory' },
|
|
73
|
+
];
|
|
74
|
+
for (const dir of requiredDirs) {
|
|
75
|
+
if (!fs.existsSync(dir.path)) {
|
|
76
|
+
const dirPath = dir.path;
|
|
77
|
+
issues.push({
|
|
78
|
+
severity: 'error',
|
|
79
|
+
category: 'Directory',
|
|
80
|
+
message: `${dir.name} not found: ${dirPath}`,
|
|
81
|
+
fixable: true,
|
|
82
|
+
fixDescription: `Create directory: ${dirPath}`,
|
|
83
|
+
fix: () => { fs.mkdirSync(dirPath, { recursive: true }); return true; },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---- 2. config.json ----
|
|
89
|
+
let configRaw: string | null = null;
|
|
90
|
+
let config: any = null;
|
|
91
|
+
|
|
92
|
+
if (!fs.existsSync(configPath)) {
|
|
93
|
+
issues.push({
|
|
94
|
+
severity: 'error',
|
|
95
|
+
category: 'Config',
|
|
96
|
+
message: 'config.json not found — run "imtoagent setup" to create it',
|
|
97
|
+
fixable: false,
|
|
98
|
+
});
|
|
99
|
+
return issues; // 没有配置文件,后续检查无意义
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
configRaw = fs.readFileSync(configPath, 'utf-8');
|
|
104
|
+
config = JSON.parse(configRaw);
|
|
105
|
+
issues.push({ severity: 'info', category: 'Config', message: 'config.json parse OK', fixable: false });
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
// 尝试修复常见 JSON 语法错误
|
|
108
|
+
const fixed = tryFixJSON(configRaw);
|
|
109
|
+
if (fixed !== null) {
|
|
110
|
+
issues.push({
|
|
111
|
+
severity: 'error',
|
|
112
|
+
category: 'Config',
|
|
113
|
+
message: `config.json has syntax errors: ${e.message}`,
|
|
114
|
+
fixable: true,
|
|
115
|
+
fixDescription: 'Auto-fix common JSON syntax issues (trailing commas, comments)',
|
|
116
|
+
fix: () => {
|
|
117
|
+
fs.writeFileSync(configPath, fixed + '\n');
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
config = JSON.parse(fixed);
|
|
122
|
+
} else {
|
|
123
|
+
issues.push({
|
|
124
|
+
severity: 'error',
|
|
125
|
+
category: 'Config',
|
|
126
|
+
message: `config.json parse error: ${e.message}`,
|
|
127
|
+
fixable: false,
|
|
128
|
+
});
|
|
129
|
+
return issues;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 检查必要字段
|
|
134
|
+
if (!config.bots || !Array.isArray(config.bots)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
severity: 'error',
|
|
137
|
+
category: 'Config',
|
|
138
|
+
message: 'No "bots" array in config.json — no Bots configured',
|
|
139
|
+
fixable: false,
|
|
140
|
+
});
|
|
141
|
+
} else if (config.bots.length === 0) {
|
|
142
|
+
issues.push({
|
|
143
|
+
severity: 'warning',
|
|
144
|
+
category: 'Config',
|
|
145
|
+
message: '"bots" array is empty — no Bots configured',
|
|
146
|
+
fixable: false,
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
for (let i = 0; i < config.bots.length; i++) {
|
|
150
|
+
const bot = config.bots[i];
|
|
151
|
+
const botLabel = bot.name || `bots[${i}]`;
|
|
152
|
+
const botIssues: string[] = [];
|
|
153
|
+
|
|
154
|
+
if (!bot.name) botIssues.push('missing "name"');
|
|
155
|
+
if (!bot.appId) botIssues.push('missing "appId"');
|
|
156
|
+
if (!bot.appSecret) botIssues.push('missing "appSecret"');
|
|
157
|
+
if (!bot.backend) botIssues.push('missing "backend"');
|
|
158
|
+
if (bot.backend && !['claude', 'codex', 'opencode'].includes(bot.backend)) {
|
|
159
|
+
botIssues.push(`unknown backend "${bot.backend}" (expected: claude/codex/opencode)`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (botIssues.length > 0) {
|
|
163
|
+
issues.push({
|
|
164
|
+
severity: 'error',
|
|
165
|
+
category: 'Config',
|
|
166
|
+
message: `Bot "${botLabel}": ${botIssues.join(', ')}`,
|
|
167
|
+
fixable: false,
|
|
168
|
+
});
|
|
169
|
+
} else {
|
|
170
|
+
issues.push({ severity: 'info', category: 'Config', message: `Bot "${bot.name}" OK (${bot.backend})`, fixable: false });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 检查 system 配置
|
|
176
|
+
if (!config.system) {
|
|
177
|
+
const systemPath = 'system';
|
|
178
|
+
issues.push({
|
|
179
|
+
severity: 'warning',
|
|
180
|
+
category: 'Config',
|
|
181
|
+
message: 'Missing "system" section in config.json',
|
|
182
|
+
fixable: true,
|
|
183
|
+
fixDescription: 'Add default system config (idleTimeoutMinutes: 30, etc.)',
|
|
184
|
+
fix: () => {
|
|
185
|
+
config.system = { defaultProjectDir: os.homedir(), idleTimeoutMinutes: 30 };
|
|
186
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
187
|
+
return true;
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---- 3. providers.json ----
|
|
193
|
+
let providers: any = null;
|
|
194
|
+
|
|
195
|
+
if (!fs.existsSync(providersPath)) {
|
|
196
|
+
issues.push({
|
|
197
|
+
severity: 'warning',
|
|
198
|
+
category: 'Providers',
|
|
199
|
+
message: 'providers.json not found',
|
|
200
|
+
fixable: false,
|
|
201
|
+
});
|
|
202
|
+
} else {
|
|
203
|
+
try {
|
|
204
|
+
const provRaw = fs.readFileSync(providersPath, 'utf-8');
|
|
205
|
+
providers = JSON.parse(provRaw);
|
|
206
|
+
issues.push({ severity: 'info', category: 'Providers', message: 'providers.json parse OK', fixable: false });
|
|
207
|
+
|
|
208
|
+
// 检查 placeholder API keys
|
|
209
|
+
const provStr = JSON.stringify(providers);
|
|
210
|
+
if (provStr.includes('YOUR_') || provStr.includes('sk-xxx') || provStr.includes('PLACEHOLDER')) {
|
|
211
|
+
issues.push({
|
|
212
|
+
severity: 'warning',
|
|
213
|
+
category: 'Providers',
|
|
214
|
+
message: 'providers.json may contain placeholder API keys',
|
|
215
|
+
fixable: false,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 验证每个 provider 的 API key
|
|
220
|
+
if (providers.providers) {
|
|
221
|
+
for (const [provName, provCfg] of Object.entries(providers.providers) as [string, any][]) {
|
|
222
|
+
if (provCfg.apiKey) {
|
|
223
|
+
const result = validateApiKey(provCfg.apiKey);
|
|
224
|
+
if (!result.valid) {
|
|
225
|
+
issues.push({
|
|
226
|
+
severity: 'warning',
|
|
227
|
+
category: 'Providers',
|
|
228
|
+
message: `Provider "${provName}" API key looks invalid (${result.reason})`,
|
|
229
|
+
fixable: false,
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
issues.push({ severity: 'info', category: 'Providers', message: `Provider "${provName}" API key format OK`, fixable: false });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch (e: any) {
|
|
238
|
+
const fixed = tryFixJSON(fs.readFileSync(providersPath, 'utf-8'));
|
|
239
|
+
if (fixed !== null) {
|
|
240
|
+
issues.push({
|
|
241
|
+
severity: 'error',
|
|
242
|
+
category: 'Providers',
|
|
243
|
+
message: `providers.json has syntax errors: ${e.message}`,
|
|
244
|
+
fixable: true,
|
|
245
|
+
fixDescription: 'Auto-fix common JSON syntax issues',
|
|
246
|
+
fix: () => {
|
|
247
|
+
fs.writeFileSync(providersPath, fixed + '\n');
|
|
248
|
+
return true;
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
} else {
|
|
252
|
+
issues.push({
|
|
253
|
+
severity: 'error',
|
|
254
|
+
category: 'Providers',
|
|
255
|
+
message: `providers.json parse error: ${e.message}`,
|
|
256
|
+
fixable: false,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- 4. 后端检查 ----
|
|
263
|
+
if (config.bots && config.bots.length > 0) {
|
|
264
|
+
const checkedTypes = new Set<string>();
|
|
265
|
+
for (const bot of config.bots) {
|
|
266
|
+
if (bot.backend && ['claude', 'codex', 'opencode'].includes(bot.backend) && !checkedTypes.has(bot.backend)) {
|
|
267
|
+
checkedTypes.add(bot.backend);
|
|
268
|
+
const info = checkBackend(bot.backend as any);
|
|
269
|
+
if (info.installed) {
|
|
270
|
+
issues.push({ severity: 'info', category: 'Backend', message: `${info.label} v${info.version} (${info.installSource})`, fixable: false });
|
|
271
|
+
} else {
|
|
272
|
+
issues.push({
|
|
273
|
+
severity: 'error',
|
|
274
|
+
category: 'Backend',
|
|
275
|
+
message: `${info.label} not installed — Bot "${bot.name}" requires it`,
|
|
276
|
+
fixable: false,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---- 5. 端口检查 ----
|
|
284
|
+
try {
|
|
285
|
+
const net = await import('net');
|
|
286
|
+
const checkPort = (port: number): Promise<boolean> => {
|
|
287
|
+
return new Promise((resolve) => {
|
|
288
|
+
const socket = new net.Socket();
|
|
289
|
+
socket.setTimeout(2000);
|
|
290
|
+
socket.on('connect', () => { socket.destroy(); resolve(true); });
|
|
291
|
+
socket.on('error', () => resolve(false));
|
|
292
|
+
socket.on('timeout', () => { socket.destroy(); resolve(false); });
|
|
293
|
+
socket.connect(port, '127.0.0.1');
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
const reachable = await checkPort(18899);
|
|
297
|
+
if (reachable) {
|
|
298
|
+
// 检查是否是 imtoagent 自己的进程
|
|
299
|
+
try {
|
|
300
|
+
const lsofOut = execSync(`lsof -i :18899 2>/dev/null`, { encoding: 'utf-8', timeout: 3000 }).trim();
|
|
301
|
+
if (lsofOut && lsofOut.includes('imtoagent') || lsofOut.includes('bun') || lsofOut.includes('node')) {
|
|
302
|
+
issues.push({ severity: 'info', category: 'Port', message: 'Port 18899 in use by imtoagent gateway', fixable: false });
|
|
303
|
+
} else {
|
|
304
|
+
issues.push({
|
|
305
|
+
severity: 'error',
|
|
306
|
+
category: 'Port',
|
|
307
|
+
message: `Port 18899 occupied by another process:\n${lsofOut.split('\n').slice(0, 3).join('\n')}`,
|
|
308
|
+
fixable: false,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
} catch {
|
|
312
|
+
issues.push({ severity: 'info', category: 'Port', message: 'Port 18899 in use', fixable: false });
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
issues.push({ severity: 'info', category: 'Port', message: 'Port 18899 is free', fixable: false });
|
|
316
|
+
}
|
|
317
|
+
} catch {
|
|
318
|
+
issues.push({ severity: 'warning', category: 'Port', message: 'Port check failed (skipped)', fixable: false });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ---- 6. Bot appId/appSecret 格式 ----
|
|
322
|
+
if (config.bots) {
|
|
323
|
+
for (const bot of config.bots) {
|
|
324
|
+
if (bot.appId && bot.appId.includes('YOUR_')) {
|
|
325
|
+
issues.push({
|
|
326
|
+
severity: 'warning',
|
|
327
|
+
category: 'Credentials',
|
|
328
|
+
message: `Bot "${bot.name}" appId is placeholder: ${bot.appId}`,
|
|
329
|
+
fixable: false,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
if (bot.appSecret && bot.appSecret.includes('YOUR_')) {
|
|
333
|
+
issues.push({
|
|
334
|
+
severity: 'warning',
|
|
335
|
+
category: 'Credentials',
|
|
336
|
+
message: `Bot "${bot.name}" appSecret is placeholder`,
|
|
337
|
+
fixable: false,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---- 7. Soul 目录检查 ----
|
|
344
|
+
if (config.bots) {
|
|
345
|
+
for (const bot of config.bots) {
|
|
346
|
+
const botKey = bot.id || bot.name;
|
|
347
|
+
const soulDir = path.join(dataDir, 'soul', botKey);
|
|
348
|
+
if (fs.existsSync(soulDir)) {
|
|
349
|
+
const soulFiles = fs.readdirSync(soulDir);
|
|
350
|
+
if (soulFiles.length === 0) {
|
|
351
|
+
issues.push({
|
|
352
|
+
severity: 'warning',
|
|
353
|
+
category: 'Soul',
|
|
354
|
+
message: `Bot "${bot.name}" soul directory is empty: ${soulDir}`,
|
|
355
|
+
fixable: false,
|
|
356
|
+
});
|
|
357
|
+
} else {
|
|
358
|
+
issues.push({ severity: 'info', category: 'Soul', message: `Bot "${bot.name}" soul: ${soulFiles.length} file(s)`, fixable: false });
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ---- 8. opencode.json 检查 ----
|
|
365
|
+
const opencodePath = getOpencodeConfigPath();
|
|
366
|
+
if (fs.existsSync(opencodePath)) {
|
|
367
|
+
try {
|
|
368
|
+
const ocRaw = fs.readFileSync(opencodePath, 'utf-8');
|
|
369
|
+
JSON.parse(ocRaw);
|
|
370
|
+
issues.push({ severity: 'info', category: 'Config', message: 'opencode.json parse OK', fixable: false });
|
|
371
|
+
} catch (e: any) {
|
|
372
|
+
const fixed = tryFixJSON(fs.readFileSync(opencodePath, 'utf-8'));
|
|
373
|
+
if (fixed !== null) {
|
|
374
|
+
issues.push({
|
|
375
|
+
severity: 'error',
|
|
376
|
+
category: 'Config',
|
|
377
|
+
message: `opencode.json has syntax errors: ${e.message}`,
|
|
378
|
+
fixable: true,
|
|
379
|
+
fixDescription: 'Auto-fix common JSON syntax issues',
|
|
380
|
+
fix: () => {
|
|
381
|
+
fs.writeFileSync(opencodePath, fixed + '\n');
|
|
382
|
+
return true;
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
} else {
|
|
386
|
+
issues.push({
|
|
387
|
+
severity: 'error',
|
|
388
|
+
category: 'Config',
|
|
389
|
+
message: `opencode.json parse error: ${e.message}`,
|
|
390
|
+
fixable: false,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return issues;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ================================================================
|
|
400
|
+
// JSON 修复 — 处理常见语法错误
|
|
401
|
+
// ================================================================
|
|
402
|
+
|
|
403
|
+
function tryFixJSON(raw: string): string | null {
|
|
404
|
+
// 尝试 1: 直接解析
|
|
405
|
+
try { JSON.parse(raw); return raw; } catch {}
|
|
406
|
+
|
|
407
|
+
// 尝试 2: 移除行尾逗号 (trailing commas)
|
|
408
|
+
let fixed = raw.replace(/,\s*([}\]])/g, '$1');
|
|
409
|
+
try { JSON.parse(fixed); return fixed; } catch {}
|
|
410
|
+
|
|
411
|
+
// 尝试 3: 移除单行注释 (// ...)
|
|
412
|
+
fixed = fixed.replace(/\/\/.*$/gm, '');
|
|
413
|
+
try { JSON.parse(fixed); return fixed; } catch {}
|
|
414
|
+
|
|
415
|
+
// 尝试 4: 移除多行注释 (/* ... */)
|
|
416
|
+
fixed = fixed.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
417
|
+
try { JSON.parse(fixed); return fixed; } catch {}
|
|
418
|
+
|
|
419
|
+
// 尝试 5: 移除尾随逗号 + 注释组合
|
|
420
|
+
fixed = raw.replace(/\/\/.*$/gm, '').replace(/,\s*([}\]])/g, '$1');
|
|
421
|
+
try { JSON.parse(fixed); return fixed; } catch {}
|
|
422
|
+
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ================================================================
|
|
427
|
+
// 格式化输出
|
|
428
|
+
// ================================================================
|
|
429
|
+
|
|
430
|
+
export function formatIssues(issues: DoctorIssue[]): string {
|
|
431
|
+
const errors = issues.filter(i => i.severity === 'error');
|
|
432
|
+
const warnings = issues.filter(i => i.severity === 'warning');
|
|
433
|
+
const infos = issues.filter(i => i.severity === 'info');
|
|
434
|
+
|
|
435
|
+
let output = '';
|
|
436
|
+
|
|
437
|
+
if (errors.length > 0) {
|
|
438
|
+
output += `\n❌ Errors (${errors.length}):\n`;
|
|
439
|
+
for (const e of errors) {
|
|
440
|
+
output += ` ${e.message}\n`;
|
|
441
|
+
if (e.fixable && e.fixDescription) {
|
|
442
|
+
output += ` → Fix: ${e.fixDescription}\n`;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (warnings.length > 0) {
|
|
448
|
+
output += `\n⚠️ Warnings (${warnings.length}):\n`;
|
|
449
|
+
for (const w of warnings) {
|
|
450
|
+
output += ` ${w.message}\n`;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (infos.length > 0) {
|
|
455
|
+
output += `\n✅ OK (${infos.length}):\n`;
|
|
456
|
+
for (const i of infos) {
|
|
457
|
+
output += ` ${i.message}\n`;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return output;
|
|
462
|
+
}
|
|
@@ -145,19 +145,39 @@ export class WorkspaceManager {
|
|
|
145
145
|
/**
|
|
146
146
|
* 检查路径是否允许该 Bot 访问。
|
|
147
147
|
*
|
|
148
|
+
* 所有模式:禁止访问 ~/.imtoagent/ 下的配置敏感文件(配置保护)。
|
|
148
149
|
* 沙盒模式:路径必须在 Bot 的工作空间范围内(或子目录)。
|
|
149
|
-
*
|
|
150
|
+
* 全局模式:不做其他限制,允许访问任意路径(信任用户配置的全局目录)。
|
|
150
151
|
*
|
|
151
152
|
* 返回 true 表示允许,false 表示拒绝。
|
|
152
153
|
*/
|
|
153
154
|
isPathAllowed(botKey: string, targetPath: string): boolean {
|
|
154
|
-
|
|
155
|
+
const resolved = path.resolve(targetPath);
|
|
156
|
+
|
|
157
|
+
// ⛔ 配置保护:禁止访问 ~/.imtoagent/ 下的敏感配置文件
|
|
158
|
+
// 白名单:允许访问 workspaces/ 和 soul/ 目录
|
|
159
|
+
const dataDir = path.resolve(getDataDir());
|
|
160
|
+
if (resolved === dataDir || resolved.startsWith(dataDir + path.sep)) {
|
|
161
|
+
const wsDir = path.resolve(this.workspacesDir);
|
|
162
|
+
const soulGlob = path.resolve(dataDir, 'soul');
|
|
163
|
+
// 允许:workspaces/ 下的内容、全局模式下的 soul/
|
|
164
|
+
if (resolved === wsDir || resolved.startsWith(wsDir + path.sep)) {
|
|
165
|
+
// OK — workspace 路径在工作空间范围内(沙盒模式下还需额外检查)
|
|
166
|
+
} else if (this.config.mode === 'global' &&
|
|
167
|
+
(resolved === soulGlob || resolved.startsWith(soulGlob + path.sep))) {
|
|
168
|
+
// OK — 全局模式下的 soul 目录
|
|
169
|
+
} else {
|
|
170
|
+
// 其他 ~/.imtoagent/ 路径一律禁止(config.json、providers.json、bot-ids.json 等)
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 全局模式:配置保护已通过,不做其他边界限制
|
|
155
176
|
if (this.config.mode === 'global') {
|
|
156
177
|
return true;
|
|
157
178
|
}
|
|
158
179
|
|
|
159
180
|
// 沙盒模式:路径必须在工作空间内
|
|
160
|
-
const resolved = path.resolve(targetPath);
|
|
161
181
|
const wsPath = this.getWorkspacePath(botKey);
|
|
162
182
|
const resolvedWs = path.resolve(wsPath);
|
|
163
183
|
|