imtoagent 0.3.23 → 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 +734 -0
- package/index.ts +31 -4
- package/modules/cli/setup.ts +103 -36
- 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
|
@@ -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
|
|