imtoagent 0.2.5 → 0.3.1
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/index.ts +15 -18
- package/modules/cli/setup.ts +379 -216
- package/modules/core/config.ts +17 -17
- package/modules/core/session.ts +21 -21
- package/modules/core/types.ts +7 -5
- package/modules/prompt-builder.ts +3 -3
- package/modules/types.ts +2 -0
- package/modules/utils/backend-check.ts +86 -14
- package/modules/utils/paths.ts +7 -2
- package/package.json +3 -3
package/index.ts
CHANGED
|
@@ -51,18 +51,12 @@ registerIM('telegram', {
|
|
|
51
51
|
},
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
//
|
|
54
|
+
// 注册企业微信(扫码绑定,无需预填凭证)
|
|
55
55
|
registerIM('wecom', {
|
|
56
56
|
create(cfg: BotConfig) {
|
|
57
57
|
return new WeComIMModule({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
agentId: (cfg as any).agentId,
|
|
61
|
-
token: (cfg as any).token,
|
|
62
|
-
encodingAESKey: (cfg as any).encodingAESKey,
|
|
63
|
-
receiveId: (cfg as any).receiveId,
|
|
64
|
-
webhookPort: (cfg as any).webhookPort,
|
|
65
|
-
webhookPath: (cfg as any).webhookPath,
|
|
58
|
+
botId: (cfg as any).botId,
|
|
59
|
+
secret: (cfg as any).secret,
|
|
66
60
|
});
|
|
67
61
|
},
|
|
68
62
|
});
|
|
@@ -82,7 +76,7 @@ import { getProxyUsage, resetProxyUsage, initCodexProxyConfig } from './modules/
|
|
|
82
76
|
import { initOpenCodeConfig } from './modules/agent/opencode';
|
|
83
77
|
import { checkRateLimit, setRateLimitConfig } from './modules/rate-limiter';
|
|
84
78
|
import { setCurrentBot } from './modules/bot-context';
|
|
85
|
-
import { getDataDir, getSessionsDir, getSoulDir, getRestoreMarkerPath } from './modules/utils/paths';
|
|
79
|
+
import { getDataDir, getSessionsDir, getSoulDir, getBotKey, getRestoreMarkerPath } from './modules/utils/paths';
|
|
86
80
|
|
|
87
81
|
// ===== SDK 核心 =====
|
|
88
82
|
import { AgentRuntime, FileSessionManager, DefaultErrorHandler, DefaultStatsTracker } from './modules/core';
|
|
@@ -281,6 +275,7 @@ type CommandHandler = (ctx: CommandCtx) => Promise<string> | string;
|
|
|
281
275
|
// BotConfig
|
|
282
276
|
// ================================================================
|
|
283
277
|
interface BotConfig {
|
|
278
|
+
id?: string;
|
|
284
279
|
name: string;
|
|
285
280
|
appId: string;
|
|
286
281
|
appSecret: string;
|
|
@@ -292,6 +287,7 @@ interface BotConfig {
|
|
|
292
287
|
// Bot 类 — SDK 完整接入版
|
|
293
288
|
// ================================================================
|
|
294
289
|
class Bot {
|
|
290
|
+
id: string;
|
|
295
291
|
name: string;
|
|
296
292
|
backend: 'claude' | 'codex' | 'opencode';
|
|
297
293
|
appId: string;
|
|
@@ -313,6 +309,7 @@ class Bot {
|
|
|
313
309
|
adapter: AgentAdapter;
|
|
314
310
|
|
|
315
311
|
constructor(cfg: BotConfig, globalConfig: any) {
|
|
312
|
+
this.id = cfg.id || cfg.name; // 后向兼容:无 id 时用 name
|
|
316
313
|
this.name = cfg.name;
|
|
317
314
|
this.backend = cfg.backend;
|
|
318
315
|
this.appId = cfg.appId;
|
|
@@ -353,7 +350,7 @@ class Bot {
|
|
|
353
350
|
this.im = imFactory.create(cfg);
|
|
354
351
|
|
|
355
352
|
// ===== SDK 集成 =====
|
|
356
|
-
this.sessionManager = new CustomSessionManager(this.
|
|
353
|
+
this.sessionManager = new CustomSessionManager(this.id, this.sessions);
|
|
357
354
|
|
|
358
355
|
const adapterCtx = {
|
|
359
356
|
imModule: this.im,
|
|
@@ -388,7 +385,7 @@ class Bot {
|
|
|
388
385
|
}
|
|
389
386
|
|
|
390
387
|
// ===== 灵魂管理 =====
|
|
391
|
-
_soulDir() { return getSoulDir(this.
|
|
388
|
+
_soulDir() { return getSoulDir(this.id); }
|
|
392
389
|
|
|
393
390
|
_initSoul() {
|
|
394
391
|
const dir = this._soulDir();
|
|
@@ -438,7 +435,7 @@ class Bot {
|
|
|
438
435
|
}
|
|
439
436
|
|
|
440
437
|
// ===== Bot 配置 =====
|
|
441
|
-
_botConfigPath() { return path.join(getSessionsDir(), this.
|
|
438
|
+
_botConfigPath() { return path.join(getSessionsDir(), this.id, '_bot.json'); }
|
|
442
439
|
|
|
443
440
|
_loadBotConfig() {
|
|
444
441
|
try {
|
|
@@ -698,7 +695,7 @@ class Bot {
|
|
|
698
695
|
const cmdResp = await this.tryHandleCommand(chatId, text, session);
|
|
699
696
|
if (cmdResp !== null) {
|
|
700
697
|
await this.reply(chatId, cmdResp);
|
|
701
|
-
this.sessionManager.persist(this.
|
|
698
|
+
this.sessionManager.persist(this.id, session);
|
|
702
699
|
return;
|
|
703
700
|
}
|
|
704
701
|
|
|
@@ -719,7 +716,7 @@ class Bot {
|
|
|
719
716
|
sendProgress: async (t: string) => this.sendProgress(chatId, t),
|
|
720
717
|
sendBlocks: async (blocks) => this.sendFormattedReplyDirect(chatId, blocks),
|
|
721
718
|
imCaps: this.im.getCapabilities(),
|
|
722
|
-
}, this.adapter, this.
|
|
719
|
+
}, this.adapter, this.id);
|
|
723
720
|
|
|
724
721
|
// Agent 自主重启信号检测
|
|
725
722
|
if (result?.restart) {
|
|
@@ -1017,7 +1014,7 @@ async function main() {
|
|
|
1017
1014
|
// 避免 --resume 恢复重启前残留的 Claude CLI 子进程 session
|
|
1018
1015
|
for (const bot of bots) {
|
|
1019
1016
|
if (bot.backend !== 'claude') continue;
|
|
1020
|
-
const botDir = path.join(getSessionsDir(), bot.
|
|
1017
|
+
const botDir = path.join(getSessionsDir(), bot.id);
|
|
1021
1018
|
try {
|
|
1022
1019
|
if (fs.existsSync(botDir)) {
|
|
1023
1020
|
for (const file of fs.readdirSync(botDir)) {
|
|
@@ -1052,7 +1049,7 @@ async function main() {
|
|
|
1052
1049
|
const summary = `🔄 IMtoAgent 已重启\n原因: ${reason}\n耗时: ${(uptime / 1000).toFixed(1)}s`;
|
|
1053
1050
|
let sent = 0;
|
|
1054
1051
|
for (const bot of bots) {
|
|
1055
|
-
const snap = data.bots?.[bot.
|
|
1052
|
+
const snap = data.bots?.[bot.id];
|
|
1056
1053
|
if (!snap?.chats?.length) continue;
|
|
1057
1054
|
for (const { chatId } of snap.chats) {
|
|
1058
1055
|
try { await bot.reply(chatId, summary); sent++; break; }
|
|
@@ -1087,7 +1084,7 @@ async function main() {
|
|
|
1087
1084
|
console.log('[Shutdown] 持久化所有 session...');
|
|
1088
1085
|
for (const bot of bots) {
|
|
1089
1086
|
for (const [chatId, session] of bot.sessions.entries()) {
|
|
1090
|
-
try { bot.sessionManager.persist(bot.
|
|
1087
|
+
try { bot.sessionManager.persist(bot.id, session); } catch {}
|
|
1091
1088
|
}
|
|
1092
1089
|
}
|
|
1093
1090
|
const DRAIN_TIMEOUT = 10_000;
|
package/modules/cli/setup.ts
CHANGED
|
@@ -1,219 +1,428 @@
|
|
|
1
1
|
// ================================================================
|
|
2
|
-
// setup.ts —
|
|
2
|
+
// setup.ts — 交互式配置向导(v2)
|
|
3
3
|
// ================================================================
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// 交互方式:
|
|
5
|
+
// ↑↓ 或 空格 — 切换选项
|
|
6
|
+
// 回车 — 确认选择
|
|
7
|
+
// ESC — 返回上一步
|
|
8
|
+
//
|
|
9
|
+
// 支持的 IM 平台:飞书 / Telegram / 企业微信 / 个人微信
|
|
6
10
|
// ================================================================
|
|
7
11
|
|
|
8
12
|
import * as fs from 'fs';
|
|
9
13
|
import * as os from 'os';
|
|
10
14
|
import * as path from 'path';
|
|
11
|
-
import
|
|
12
|
-
import {
|
|
13
|
-
import { checkAllBackends, formatBackendStatus
|
|
15
|
+
import { getDataDir, getPkgDir, getTemplatePath, getSoulDir, getBotKey } from '../utils/paths';
|
|
16
|
+
import { randomUUID } from 'crypto';
|
|
17
|
+
import { checkAllBackends, formatBackendStatus } from '../utils/backend-check';
|
|
18
|
+
|
|
19
|
+
// ================================================================
|
|
20
|
+
// 键盘输入(raw mode)
|
|
21
|
+
// ================================================================
|
|
22
|
+
|
|
23
|
+
const KEY = {
|
|
24
|
+
UP: '\x1b[A',
|
|
25
|
+
DOWN: '\x1b[B',
|
|
26
|
+
ENTER: '\r',
|
|
27
|
+
SPACE: ' ',
|
|
28
|
+
ESC: '\x1b',
|
|
29
|
+
BACKSPACE: '\x7f',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** 读取单个按键 */
|
|
33
|
+
function readKey(): Promise<string> {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
const onData = (data: Buffer) => {
|
|
36
|
+
process.stdin.removeListener('data', onData);
|
|
37
|
+
const s = data.toString();
|
|
38
|
+
if (s === '\x03') process.exit(130); // Ctrl+C
|
|
39
|
+
resolve(s);
|
|
40
|
+
};
|
|
41
|
+
process.stdin.once('data', onData);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ================================================================
|
|
46
|
+
// 菜单选择(↑↓/空格 切换,回车确认)
|
|
47
|
+
// ================================================================
|
|
48
|
+
|
|
49
|
+
async function selectMenu(title: string, options: string[]): Promise<number> {
|
|
50
|
+
let idx = 0;
|
|
51
|
+
const linesAbove = options.length + 2;
|
|
52
|
+
|
|
53
|
+
function render() {
|
|
54
|
+
// 清除之前的输出
|
|
55
|
+
process.stdout.write('\x1B[0G'); // 回到行首
|
|
56
|
+
options.forEach((opt, i) => {
|
|
57
|
+
const prefix = i === idx ? '▸ ' : ' ';
|
|
58
|
+
process.stdout.write(`\x1B[0G${prefix}${opt}\x1B[0K\n`);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 显示标题
|
|
63
|
+
console.log(title);
|
|
64
|
+
render();
|
|
65
|
+
|
|
66
|
+
process.stdin.setRawMode(true);
|
|
67
|
+
process.stdin.resume();
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
while (true) {
|
|
71
|
+
const key = await readKey();
|
|
72
|
+
|
|
73
|
+
if (key === KEY.UP || key === KEY.DOWN || key === KEY.SPACE) {
|
|
74
|
+
// 移动光标
|
|
75
|
+
idx = (idx + (key === KEY.UP ? -1 : 1) + options.length) % options.length;
|
|
76
|
+
// 重绘所有选项
|
|
77
|
+
process.stdout.write(`\x1B[${options.length}A`); // 上移 N 行
|
|
78
|
+
render();
|
|
79
|
+
} else if (key === KEY.ENTER) {
|
|
80
|
+
break;
|
|
81
|
+
} else if (key === KEY.ESC) {
|
|
82
|
+
return -1; // ESC = 返回上一步
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
process.stdin.setRawMode(false);
|
|
87
|
+
process.stdin.pause();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log(`\x1B[0G▸ ${options[idx]} ✓\n`);
|
|
91
|
+
return idx;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ================================================================
|
|
95
|
+
// 文本输入(回车确认,ESC 返回 -1)
|
|
96
|
+
// ================================================================
|
|
97
|
+
|
|
98
|
+
async function promptText(label: string, defaultValue = ''): Promise<string> {
|
|
99
|
+
const buf: string[] = [];
|
|
100
|
+
const defaultHint = defaultValue ? ` [${defaultValue}]` : '';
|
|
101
|
+
|
|
102
|
+
process.stdout.write(`${label}${defaultHint}: `);
|
|
103
|
+
process.stdin.setRawMode(true);
|
|
104
|
+
process.stdin.resume();
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
while (true) {
|
|
108
|
+
const key = await readKey();
|
|
109
|
+
|
|
110
|
+
if (key === KEY.ENTER) {
|
|
111
|
+
break;
|
|
112
|
+
} else if (key === KEY.ESC) {
|
|
113
|
+
process.stdout.write('\x1B[0K\n');
|
|
114
|
+
return -1 as unknown as string; // 特殊返回值表示 ESC
|
|
115
|
+
} else if (key === KEY.BACKSPACE) {
|
|
116
|
+
if (buf.length > 0) {
|
|
117
|
+
buf.pop();
|
|
118
|
+
process.stdout.write('\x1B[1D \x1B[1D'); // 退格删除
|
|
119
|
+
}
|
|
120
|
+
} else if (key === KEY.UP || key === KEY.DOWN) {
|
|
121
|
+
// 忽略方向键
|
|
122
|
+
} else if (key.length === 1 && key !== KEY.SPACE) {
|
|
123
|
+
// 普通字符(空格单独处理)
|
|
124
|
+
buf.push(key);
|
|
125
|
+
process.stdout.write(key);
|
|
126
|
+
} else if (key === KEY.SPACE) {
|
|
127
|
+
buf.push(' ');
|
|
128
|
+
process.stdout.write(' ');
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} finally {
|
|
132
|
+
process.stdin.setRawMode(false);
|
|
133
|
+
process.stdin.pause();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
process.stdout.write('\n');
|
|
137
|
+
const result = buf.join('').trim();
|
|
138
|
+
return result || defaultValue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ================================================================
|
|
142
|
+
// 确认(Y/N,回车确认,ESC 返回 -1)
|
|
143
|
+
// ================================================================
|
|
144
|
+
|
|
145
|
+
async function confirm(label: string, defaultYes = true): Promise<boolean | -1> {
|
|
146
|
+
const hint = defaultYes ? '[Y/n]' : '[y/N]';
|
|
147
|
+
process.stdout.write(`${label} ${hint}: `);
|
|
148
|
+
process.stdin.setRawMode(true);
|
|
149
|
+
process.stdin.resume();
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
while (true) {
|
|
153
|
+
const key = await readKey();
|
|
154
|
+
if (key === KEY.ENTER) {
|
|
155
|
+
process.stdout.write('\n');
|
|
156
|
+
return defaultYes;
|
|
157
|
+
} else if (key === KEY.ESC) {
|
|
158
|
+
process.stdout.write('\x1B[0K\n');
|
|
159
|
+
return -1;
|
|
160
|
+
} else if (key.toLowerCase() === 'y') {
|
|
161
|
+
process.stdout.write('Y\n');
|
|
162
|
+
return true;
|
|
163
|
+
} else if (key.toLowerCase() === 'n') {
|
|
164
|
+
process.stdout.write('N\n');
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
process.stdin.setRawMode(false);
|
|
170
|
+
process.stdin.pause();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ================================================================
|
|
175
|
+
// IM 平台配置定义
|
|
176
|
+
// ================================================================
|
|
177
|
+
|
|
178
|
+
const IM_PLATFORMS = [
|
|
179
|
+
{ value: 'feishu', label: '飞书', desc: 'WebSocket 长连接' },
|
|
180
|
+
{ value: 'telegram', label: 'Telegram', desc: '长轮询' },
|
|
181
|
+
{ value: 'wecom', label: '企业微信', desc: '扫码绑定 + WebSocket' },
|
|
182
|
+
{ value: 'wechat', label: '个人微信', desc: 'iLink + QR 扫码' },
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
/** 每种 IM 需要的凭证字段 */
|
|
186
|
+
const IM_FIELDS: Record<string, { key: string; label: string; required: boolean }[]> = {
|
|
187
|
+
feishu: [
|
|
188
|
+
{ key: 'appId', label: '飞书 App ID (cli_...)', required: true },
|
|
189
|
+
{ key: 'appSecret', label: '飞书 App Secret', required: true },
|
|
190
|
+
],
|
|
191
|
+
telegram: [
|
|
192
|
+
{ key: 'appId', label: 'Bot Token', required: true },
|
|
193
|
+
{ key: 'proxy', label: '代理地址(留空不使用)', required: false },
|
|
194
|
+
],
|
|
195
|
+
wecom: [
|
|
196
|
+
// 企业微信使用扫码绑定,无需预填凭证,启动时自动触发 QR 扫码
|
|
197
|
+
],
|
|
198
|
+
wechat: [
|
|
199
|
+
// 微信通过 iLink + QR 扫码认证,无需预填凭证
|
|
200
|
+
{ key: 'botId', label: 'iLink Bot ID(留空使用 QR 扫码)', required: false },
|
|
201
|
+
{ key: 'botToken', label: 'iLink Bot Token(留空使用 QR 扫码)', required: false },
|
|
202
|
+
],
|
|
203
|
+
};
|
|
14
204
|
|
|
15
205
|
// ================================================================
|
|
16
206
|
// 主流程
|
|
17
207
|
// ================================================================
|
|
208
|
+
|
|
18
209
|
export async function runSetupWizard(): Promise<void> {
|
|
19
210
|
const dataDir = getDataDir();
|
|
20
211
|
const configPath = path.join(dataDir, 'config.json');
|
|
21
|
-
const providersPath = path.join(dataDir, 'providers.json');
|
|
22
|
-
const opencodePath = path.join(dataDir, 'opencode.json');
|
|
23
212
|
|
|
24
213
|
console.log('\n╔══════════════════════════════════════════════╗');
|
|
25
214
|
console.log('║ 🚀 imtoagent 配置向导 ║');
|
|
26
|
-
console.log('
|
|
27
|
-
console.log(
|
|
215
|
+
console.log('╚══════════════════════════════════════════════╝');
|
|
216
|
+
console.log(`\n数据目录: ${dataDir}`);
|
|
217
|
+
console.log(`操作提示: ↑↓/空格 切换 | 回车确认 | ESC 返回\n`);
|
|
28
218
|
|
|
29
219
|
// ===== Step 1: 检测已有配置 =====
|
|
30
220
|
let existingConfig: any = null;
|
|
221
|
+
let mergeMode = false;
|
|
222
|
+
|
|
31
223
|
if (fs.existsSync(configPath)) {
|
|
32
|
-
console.log('📋
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log('
|
|
36
|
-
|
|
37
|
-
const choice = await prompt('请选择 (1/2/3): ');
|
|
38
|
-
if (choice === '3' || choice.toLowerCase() === 'exit') {
|
|
39
|
-
console.log('👋 已取消');
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
224
|
+
console.log('📋 检测到已有配置\n');
|
|
225
|
+
const idx = await selectMenu('选择操作', ['覆盖现有配置', '合并(保留现有 Bot)', '退出']);
|
|
226
|
+
if (idx === -1) return; // ESC
|
|
227
|
+
if (idx === 2) { console.log('👋 已取消'); process.exit(0); }
|
|
228
|
+
if (idx === 1) mergeMode = true;
|
|
42
229
|
|
|
43
230
|
try {
|
|
44
231
|
existingConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
45
232
|
} catch {
|
|
46
233
|
console.log('⚠️ 现有配置文件解析失败,将重新生成');
|
|
47
234
|
existingConfig = null;
|
|
235
|
+
mergeMode = false;
|
|
48
236
|
}
|
|
49
237
|
}
|
|
50
238
|
|
|
51
|
-
// ===== Step
|
|
52
|
-
console.log('📌 检测后端 Agent...\n');
|
|
239
|
+
// ===== Step 2: 检测后端 =====
|
|
240
|
+
console.log('\n📌 检测后端 Agent...\n');
|
|
53
241
|
const backendStatus = checkAllBackends();
|
|
54
242
|
console.log(formatBackendStatus(backendStatus));
|
|
55
243
|
|
|
56
244
|
const installedBackends = backendStatus.filter(b => b.installed);
|
|
57
245
|
if (installedBackends.length === 0) {
|
|
58
246
|
console.log('\n⚠️ 未检测到任何后端 Agent。');
|
|
59
|
-
console.log('
|
|
60
|
-
console.log('\n推荐安装:');
|
|
247
|
+
console.log('推荐安装:');
|
|
61
248
|
console.log(' npm install -g @anthropic-ai/claude-agent-sdk # Claude Code');
|
|
62
249
|
console.log(' npm install -g @openai/codex # Codex');
|
|
63
250
|
console.log(' npm install -g opencode # OpenCode');
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
process.exit(0);
|
|
68
|
-
}
|
|
69
|
-
console.log('\n⚠️ 你可以继续配置 Bot,但启动网关后发消息会报错,直到后端安装完成。\n');
|
|
251
|
+
const r = await confirm('是否继续配置?(启动后发消息会报错,直到后端安装)', false);
|
|
252
|
+
if (r === false || r === -1) { console.log('\n👋 安装后端后请重新运行 "imtoagent setup"'); process.exit(0); }
|
|
253
|
+
console.log('\n⚠️ 已跳过,你可以稍后安装后端。\n');
|
|
70
254
|
} else {
|
|
71
|
-
console.log(`\n✅
|
|
255
|
+
console.log(`\n✅ 已安装: ${installedBackends.map(b => b.label).join(', ')}\n`);
|
|
72
256
|
}
|
|
73
257
|
|
|
74
|
-
// ===== Step
|
|
75
|
-
console.log('📌 Step
|
|
76
|
-
|
|
77
|
-
const bots: any[] = existingConfig?.bots
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
258
|
+
// ===== Step 3: 配置 Bot =====
|
|
259
|
+
console.log('📌 Step 3: 配置 Bot\n');
|
|
260
|
+
|
|
261
|
+
const bots: any[] = (mergeMode && existingConfig?.bots) ? [...existingConfig.bots] : [];
|
|
262
|
+
|
|
263
|
+
let addingBots = true;
|
|
264
|
+
while (addingBots) {
|
|
265
|
+
console.log(`\n--- 添加新 Bot (已有 ${bots.length} 个) ---\n`);
|
|
266
|
+
|
|
267
|
+
// 3a: 选择 IM 平台
|
|
268
|
+
const imLabels = IM_PLATFORMS.map(p => `${p.label} ${p.desc}`);
|
|
269
|
+
const imIdx = await selectMenu('选择 IM 平台', imLabels);
|
|
270
|
+
if (imIdx === -1) { if (bots.length === 0) return; break; } // ESC
|
|
271
|
+
const imType = IM_PLATFORMS[imIdx].value;
|
|
272
|
+
|
|
273
|
+
// 3b: 自动生成 Bot 名称,可自定义
|
|
274
|
+
const defaultName = IM_PLATFORMS[imIdx].label + 'Bot';
|
|
275
|
+
const nameInput = await promptText('Bot 名称', defaultName);
|
|
276
|
+
if ((nameInput as any) === -1) { if (bots.length === 0) return; break; } // ESC
|
|
277
|
+
const botName = nameInput || defaultName; // 留空用默认
|
|
278
|
+
|
|
279
|
+
// 3c: 选择后端
|
|
280
|
+
const backendLabels = backendStatus.map(b =>
|
|
281
|
+
b.installed ? `${b.label} (v${b.version})` : `${b.label} ⚠️ 未安装`
|
|
282
|
+
);
|
|
283
|
+
const backendIdx = await selectMenu('选择后端', backendLabels);
|
|
284
|
+
if (backendIdx === -1) continue; // ESC 返回重新选 IM
|
|
285
|
+
const backend = backendStatus[backendIdx].type;
|
|
286
|
+
const isBackendInstalled = backendStatus[backendIdx].installed;
|
|
287
|
+
|
|
288
|
+
// 后端未安装 → 提示自动安装
|
|
100
289
|
if (!isBackendInstalled) {
|
|
101
|
-
const installCmd = backendStatus.
|
|
102
|
-
console.log(`\n⚠️ ${backend}
|
|
103
|
-
console.log(`
|
|
104
|
-
|
|
105
|
-
|
|
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') {
|
|
290
|
+
const installCmd = backendStatus[backendIdx].installHint || '';
|
|
291
|
+
console.log(`\n⚠️ ${backend} 未安装`);
|
|
292
|
+
console.log(` ${installCmd}\n`);
|
|
293
|
+
const r = await confirm('是否现在自动安装?');
|
|
294
|
+
if (r === true) {
|
|
116
295
|
const { installBackend } = await import('../utils/backend-check');
|
|
117
296
|
const ok = await installBackend(backend as 'claude' | 'codex' | 'opencode');
|
|
118
|
-
if (
|
|
119
|
-
console.log(
|
|
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
|
-
}
|
|
297
|
+
if (ok) {
|
|
298
|
+
console.log(`✅ ${backend} 已安装\n`);
|
|
132
299
|
} else {
|
|
133
|
-
console.log(
|
|
300
|
+
console.log(`⚠️ 安装失败,可稍后手动运行: ${installCmd}\n`);
|
|
301
|
+
const r2 = await confirm('仍要继续配置此 Bot?');
|
|
302
|
+
if (r2 === false) continue;
|
|
134
303
|
}
|
|
304
|
+
} else if (r === -1) {
|
|
305
|
+
continue; // ESC 返回
|
|
135
306
|
} else {
|
|
136
|
-
console.log(
|
|
137
|
-
const confirm = await promptChoice('仍要继续配置此 Bot?', ['Y', 'N']);
|
|
138
|
-
if (confirm !== 'Y') {
|
|
139
|
-
addMore = (await promptChoice('继续添加其他 Bot?', ['Y', 'N'])) !== 'Y';
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
307
|
+
console.log('跳过安装\n');
|
|
142
308
|
}
|
|
143
309
|
}
|
|
144
310
|
|
|
145
|
-
|
|
311
|
+
// 3d: 根据 IM 类型收集凭证
|
|
312
|
+
console.log(`\n--- ${IM_PLATFORMS.find(p => p.value === imType)?.label} 凭证 ---`);
|
|
313
|
+
const fields = IM_FIELDS[imType] || [];
|
|
314
|
+
const credentials: Record<string, string> = {};
|
|
146
315
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
appId = await prompt('Telegram Bot Token: ');
|
|
152
|
-
proxy = await prompt('代理地址 (留空不使用代理): ') || '';
|
|
316
|
+
for (const field of fields) {
|
|
317
|
+
const val = await promptText(field.label + (field.required ? '' : '(可选)'));
|
|
318
|
+
if ((val as any) === -1) { credentials._escaped = 'true'; break; } // ESC
|
|
319
|
+
credentials[field.key] = val;
|
|
153
320
|
}
|
|
321
|
+
if (credentials._escaped) continue; // ESC 返回重新选后端
|
|
322
|
+
|
|
323
|
+
// 3e: 工作目录
|
|
324
|
+
const cwd = await promptText('工作目录', os.homedir());
|
|
325
|
+
if ((cwd as any) === -1) continue;
|
|
154
326
|
|
|
155
|
-
|
|
327
|
+
// 生成唯一 ID(UUID,用于目录隔离,改名不影响)
|
|
328
|
+
const botId = randomUUID();
|
|
156
329
|
|
|
330
|
+
// 构建 Bot 配置(不同 IM 需要的字段不同)
|
|
157
331
|
const bot: any = {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
appSecret,
|
|
332
|
+
id: botId,
|
|
333
|
+
name: botName,
|
|
161
334
|
backend,
|
|
162
|
-
|
|
163
|
-
cwd,
|
|
335
|
+
cwd: cwd || os.homedir(),
|
|
164
336
|
};
|
|
165
|
-
|
|
337
|
+
|
|
338
|
+
// 飞书需要 appId + appSecret
|
|
339
|
+
if (imType === 'feishu') {
|
|
340
|
+
bot.appId = credentials.appId || '';
|
|
341
|
+
bot.appSecret = credentials.appSecret || '';
|
|
342
|
+
}
|
|
343
|
+
// Telegram 需要 appId(Bot Token),可选 proxy
|
|
344
|
+
else if (imType === 'telegram') {
|
|
345
|
+
bot.appId = credentials.appId || '';
|
|
346
|
+
if (credentials.proxy) bot.proxy = credentials.proxy;
|
|
347
|
+
}
|
|
348
|
+
// 企业微信:扫码绑定,无需预填凭证(可选 botId/secret)
|
|
349
|
+
else if (imType === 'wecom') {
|
|
350
|
+
bot.im = 'wecom';
|
|
351
|
+
if (credentials.botId) bot.botId = credentials.botId;
|
|
352
|
+
if (credentials.secret) bot.secret = credentials.secret;
|
|
353
|
+
}
|
|
354
|
+
// 个人微信:可选 botId/botToken,留空则 QR 扫码
|
|
355
|
+
else if (imType === 'wechat') {
|
|
356
|
+
bot.im = 'wechat';
|
|
357
|
+
if (credentials.botId) bot.botId = credentials.botId;
|
|
358
|
+
if (credentials.botToken) bot.botToken = credentials.botToken;
|
|
359
|
+
if (credentials.ilinkUserId) bot.ilinkUserId = credentials.ilinkUserId;
|
|
360
|
+
}
|
|
361
|
+
// 默认:非飞书平台加 im 字段
|
|
362
|
+
else {
|
|
363
|
+
bot.im = imType;
|
|
364
|
+
}
|
|
166
365
|
|
|
167
366
|
// 检查重名
|
|
168
|
-
const
|
|
169
|
-
if (
|
|
170
|
-
bots[
|
|
367
|
+
const existingIdx = bots.findIndex(b => b.name === botName);
|
|
368
|
+
if (existingIdx >= 0) {
|
|
369
|
+
bots[existingIdx] = bot;
|
|
171
370
|
console.log(`✅ 已替换: ${name}`);
|
|
172
371
|
} else {
|
|
173
372
|
bots.push(bot);
|
|
174
373
|
console.log(`✅ 已添加: ${name}`);
|
|
175
374
|
}
|
|
176
375
|
|
|
177
|
-
|
|
376
|
+
// 是否继续添加
|
|
377
|
+
const r = await confirm('继续添加其他 Bot?', true);
|
|
378
|
+
if (r === -1) addingBots = false; // ESC = 不添加了,进入下一步
|
|
379
|
+
else addingBots = (r === true);
|
|
178
380
|
}
|
|
179
381
|
|
|
180
382
|
if (bots.length === 0) {
|
|
181
383
|
console.log('\n⚠️ 未配置任何 Bot。');
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
384
|
+
const r = await confirm('至少配置一个 Bot 吗?');
|
|
385
|
+
if (r === true) return runSetupWizard();
|
|
386
|
+
console.log('\n⚠️ 至少需要一个 Bot,配置已取消');
|
|
387
|
+
return;
|
|
185
388
|
}
|
|
186
389
|
|
|
187
|
-
// ===== Step
|
|
188
|
-
console.log('\n📌 Step
|
|
390
|
+
// ===== Step 4: 配置模型供应商 =====
|
|
391
|
+
console.log('\n📌 Step 4: 配置模型供应商\n');
|
|
189
392
|
|
|
190
393
|
const providers: Record<string, any> = {};
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (keepProviders) {
|
|
196
|
-
Object.assign(providers, existingProviders);
|
|
394
|
+
if (mergeMode && existingConfig?.providers) {
|
|
395
|
+
Object.assign(providers, existingConfig.providers);
|
|
396
|
+
console.log(`✅ 已保留 ${Object.keys(providers).length} 个现有供应商\n`);
|
|
197
397
|
}
|
|
198
398
|
|
|
199
|
-
let
|
|
200
|
-
while (
|
|
201
|
-
console.log('
|
|
202
|
-
const provName = await
|
|
203
|
-
if (
|
|
399
|
+
let addingProviders = true;
|
|
400
|
+
while (addingProviders) {
|
|
401
|
+
console.log('--- 添加新供应商 ---\n');
|
|
402
|
+
const provName = await promptText('供应商名称 (如 deepseek, dashscope)');
|
|
403
|
+
if ((provName as any) === -1) { addingProviders = false; continue; }
|
|
404
|
+
if (!provName) { addingProviders = false; continue; }
|
|
204
405
|
|
|
205
406
|
if (providers[provName]) {
|
|
206
|
-
console.log(`⚠️ 供应商 "${provName}"
|
|
407
|
+
console.log(`⚠️ 供应商 "${provName}" 已存在,将覆盖\n`);
|
|
207
408
|
}
|
|
208
409
|
|
|
209
|
-
const baseUrl = await
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
const
|
|
410
|
+
const baseUrl = await promptText('Base URL (如 https://api.deepseek.com/v1)');
|
|
411
|
+
if ((baseUrl as any) === -1) continue;
|
|
412
|
+
const apiKey = await promptText('API Key');
|
|
413
|
+
if ((apiKey as any) === -1) continue;
|
|
414
|
+
const modelsStr = await promptText('模型列表 (逗号分隔,如 deepseek-v4-pro,deepseek-v4-flash)');
|
|
415
|
+
if ((modelsStr as any) === -1) continue;
|
|
416
|
+
const models = (modelsStr || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
417
|
+
|
|
418
|
+
const formatIdx = await selectMenu('API 格式', ['openai', 'anthropic']);
|
|
419
|
+
if (formatIdx === -1) continue;
|
|
420
|
+
const format = ['openai', 'anthropic'][formatIdx];
|
|
421
|
+
|
|
422
|
+
const priceInput = await promptText('价格 (入/出 每百万 Token,如 0.55,2.19,留空跳过)');
|
|
423
|
+
if ((priceInput as any) === -1) continue;
|
|
214
424
|
|
|
215
425
|
const pricing: any = {};
|
|
216
|
-
const priceInput = await prompt('价格 (入/出 每百万 Token,如 0.55,2.19,留空跳过): ');
|
|
217
426
|
if (priceInput) {
|
|
218
427
|
const parts = priceInput.split(',').map(s => parseFloat(s.trim()));
|
|
219
428
|
if (parts.length === 2 && !isNaN(parts[0]) && !isNaN(parts[1])) {
|
|
@@ -224,13 +433,21 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
224
433
|
}
|
|
225
434
|
|
|
226
435
|
providers[provName] = { baseUrl, apiKey, models, format, ...(Object.keys(pricing).length ? { pricing } : {}) };
|
|
227
|
-
console.log(`✅ 已添加: ${provName}`);
|
|
436
|
+
console.log(`✅ 已添加: ${provName}\n`);
|
|
437
|
+
|
|
438
|
+
const r = await confirm('继续添加供应商?', false);
|
|
439
|
+
if (r === -1) addingProviders = false;
|
|
440
|
+
else addingProviders = (r === true);
|
|
441
|
+
}
|
|
228
442
|
|
|
229
|
-
|
|
443
|
+
if (Object.keys(providers).length === 0) {
|
|
444
|
+
console.log('\n⚠️ 未配置任何供应商。');
|
|
445
|
+
const r = await confirm('至少配置一个供应商吗?');
|
|
446
|
+
if (r === true) { addingProviders = true; }
|
|
230
447
|
}
|
|
231
448
|
|
|
232
|
-
// ===== Step
|
|
233
|
-
console.log('\n📌 Step
|
|
449
|
+
// ===== Step 5: 选择默认模型 =====
|
|
450
|
+
console.log('\n📌 Step 5: 选择默认模型\n');
|
|
234
451
|
|
|
235
452
|
const allModels: string[] = [];
|
|
236
453
|
for (const [provName, prov] of Object.entries(providers)) {
|
|
@@ -242,22 +459,26 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
242
459
|
let defaultModel = '';
|
|
243
460
|
if (allModels.length > 0) {
|
|
244
461
|
const existingDefault = existingConfig?.defaultModel || allModels[0];
|
|
245
|
-
const
|
|
246
|
-
defaultModel =
|
|
462
|
+
const val = await promptText('默认模型', existingDefault);
|
|
463
|
+
defaultModel = (val as any) === -1 ? existingDefault : (val || existingDefault);
|
|
247
464
|
} else {
|
|
248
|
-
defaultModel = await
|
|
465
|
+
defaultModel = await promptText('默认模型 (供应商/模型名)') || 'deepseek/deepseek-v4-pro';
|
|
466
|
+
if ((defaultModel as any) === -1) defaultModel = 'deepseek/deepseek-v4-pro';
|
|
249
467
|
}
|
|
250
468
|
|
|
251
|
-
// ===== Step
|
|
252
|
-
console.log('\n📌 Step
|
|
469
|
+
// ===== Step 6: 生成灵魂文件 =====
|
|
470
|
+
console.log('\n📌 Step 6: 生成灵魂文件\n');
|
|
253
471
|
|
|
254
472
|
for (const bot of bots) {
|
|
255
|
-
const botSoulDir = getSoulDir(bot
|
|
473
|
+
const botSoulDir = getSoulDir(getBotKey(bot));
|
|
256
474
|
const templateSoulDir = path.join(getPkgDir(), 'templates', 'soul.template');
|
|
257
475
|
|
|
258
|
-
if (fs.existsSync(botSoulDir)
|
|
259
|
-
|
|
260
|
-
|
|
476
|
+
if (fs.existsSync(botSoulDir)) {
|
|
477
|
+
const r = await confirm(`已存在 ${bot.name} 的灵魂文件,重新生成?`, false);
|
|
478
|
+
if (r === -1 || r === false) {
|
|
479
|
+
console.log(`⏭ 跳过: ${bot.name}`);
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
261
482
|
}
|
|
262
483
|
|
|
263
484
|
fs.mkdirSync(botSoulDir, { recursive: true });
|
|
@@ -267,29 +488,24 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
267
488
|
const tmplPath = path.join(templateSoulDir, sf);
|
|
268
489
|
const destPath = path.join(botSoulDir, sf);
|
|
269
490
|
|
|
270
|
-
if (fs.existsSync(destPath) && !fs.existsSync(tmplPath))
|
|
271
|
-
continue; // 已有且无模板,保留
|
|
272
|
-
}
|
|
491
|
+
if (fs.existsSync(destPath) && !fs.existsSync(tmplPath)) continue;
|
|
273
492
|
|
|
274
493
|
if (fs.existsSync(tmplPath)) {
|
|
275
494
|
let content = fs.readFileSync(tmplPath, 'utf-8');
|
|
276
|
-
// 替换模板变量
|
|
277
495
|
content = content.replace(/\{\{backend\}\}/g, bot.backend);
|
|
278
496
|
content = content.replace(/\{\{cwd\}\}/g, bot.cwd || os.homedir());
|
|
279
497
|
content = content.replace(/\{\{botName\}\}/g, bot.name);
|
|
280
498
|
fs.writeFileSync(destPath, content);
|
|
281
499
|
}
|
|
282
500
|
}
|
|
283
|
-
console.log(`✅ ${bot.name}:
|
|
501
|
+
console.log(`✅ ${bot.name}: 灵魂文件 → ${botSoulDir}`);
|
|
284
502
|
}
|
|
285
503
|
|
|
286
|
-
// ===== Step
|
|
287
|
-
console.log('\n📌 Step
|
|
504
|
+
// ===== Step 7: 写入配置文件 =====
|
|
505
|
+
console.log('\n📌 Step 7: 写入配置文件\n');
|
|
288
506
|
|
|
289
|
-
// 确保数据目录存在
|
|
290
507
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
291
508
|
|
|
292
|
-
// config.json
|
|
293
509
|
const config: any = {
|
|
294
510
|
system: existingConfig?.system || {
|
|
295
511
|
defaultProjectDir: os.homedir(),
|
|
@@ -327,46 +543,38 @@ export async function runSetupWizard(): Promise<void> {
|
|
|
327
543
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
328
544
|
console.log(`✅ ${configPath}`);
|
|
329
545
|
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
providers,
|
|
333
|
-
defaultModel,
|
|
334
|
-
modelAliases: config.modelAliases,
|
|
335
|
-
};
|
|
546
|
+
const providersFile: any = { providers, defaultModel, modelAliases: config.modelAliases };
|
|
547
|
+
const providersPath = path.join(dataDir, 'providers.json');
|
|
336
548
|
fs.writeFileSync(providersPath, JSON.stringify(providersFile, null, 2) + '\n');
|
|
337
549
|
console.log(`✅ ${providersPath}`);
|
|
338
550
|
|
|
339
|
-
|
|
551
|
+
const opencodePath = path.join(dataDir, 'opencode.json');
|
|
340
552
|
const opencodeTemplate = getTemplatePath('opencode.template.json');
|
|
341
553
|
if (fs.existsSync(opencodeTemplate)) {
|
|
342
|
-
|
|
343
|
-
fs.writeFileSync(opencodePath, opencodeContent);
|
|
554
|
+
fs.writeFileSync(opencodePath, fs.readFileSync(opencodeTemplate, 'utf-8'));
|
|
344
555
|
console.log(`✅ ${opencodePath}`);
|
|
345
556
|
}
|
|
346
557
|
|
|
347
|
-
// 创建必要的子目录
|
|
348
558
|
fs.mkdirSync(path.join(dataDir, 'sessions'), { recursive: true });
|
|
349
559
|
fs.mkdirSync(path.join(dataDir, 'logs'), { recursive: true });
|
|
350
560
|
console.log('✅ 子目录已创建 (sessions/, logs/)');
|
|
351
561
|
|
|
352
|
-
// =====
|
|
562
|
+
// ===== 完成 =====
|
|
353
563
|
console.log('\n╔══════════════════════════════════════════════╗');
|
|
354
564
|
console.log('║ ✅ 配置完成! ║');
|
|
355
565
|
console.log('╚══════════════════════════════════════════════╝\n');
|
|
356
566
|
console.log(`Bot: ${bots.map(b => b.name).join(', ')}`);
|
|
357
567
|
console.log(`默认模型: ${defaultModel}`);
|
|
358
|
-
console.log(`供应商: ${Object.keys(providers).join(', ')}`);
|
|
568
|
+
console.log(`供应商: ${Object.keys(providers).join(', ') || '无'}`);
|
|
359
569
|
console.log(`\n下一步:`);
|
|
360
570
|
console.log(` imtoagent start 启动网关`);
|
|
361
|
-
console.log(` imtoagent status
|
|
362
|
-
console.log();
|
|
571
|
+
console.log(` imtoagent status 查看状态\n`);
|
|
363
572
|
}
|
|
364
573
|
|
|
365
574
|
// ================================================================
|
|
366
575
|
// 工具函数
|
|
367
576
|
// ================================================================
|
|
368
577
|
|
|
369
|
-
/** 构建默认模型别名 */
|
|
370
578
|
function buildDefaultAliases(defaultModel: string): Record<string, string> {
|
|
371
579
|
return {
|
|
372
580
|
default: defaultModel,
|
|
@@ -377,48 +585,3 @@ function buildDefaultAliases(defaultModel: string): Record<string, string> {
|
|
|
377
585
|
opencode: defaultModel,
|
|
378
586
|
};
|
|
379
587
|
}
|
|
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
|
-
}
|
package/modules/core/config.ts
CHANGED
|
@@ -96,7 +96,7 @@ export class FileConfigManager implements ConfigManager {
|
|
|
96
96
|
// 加载各 bot 的模型配置
|
|
97
97
|
if (this.rawConfig?.bots) {
|
|
98
98
|
for (const bot of this.rawConfig.bots) {
|
|
99
|
-
this._loadBotConfig(
|
|
99
|
+
this._loadBotConfig(botKey);
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
}
|
|
@@ -118,36 +118,36 @@ export class FileConfigManager implements ConfigManager {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
/** 加载 Bot 级别配置 */
|
|
121
|
-
private _loadBotConfig(
|
|
121
|
+
private _loadBotConfig(botKey: string): void {
|
|
122
122
|
const sessionsDir = getSessionsDir();
|
|
123
|
-
const configPath = path.join(sessionsDir, `${
|
|
123
|
+
const configPath = path.join(sessionsDir, `${botKey}_config.json`);
|
|
124
124
|
|
|
125
125
|
try {
|
|
126
126
|
if (fs.existsSync(configPath)) {
|
|
127
127
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
128
|
-
this.botConfigs.set(
|
|
128
|
+
this.botConfigs.set(botKey, JSON.parse(raw));
|
|
129
129
|
} else {
|
|
130
|
-
this.botConfigs.set(
|
|
130
|
+
this.botConfigs.set(botKey, {});
|
|
131
131
|
}
|
|
132
132
|
} catch (e: any) {
|
|
133
|
-
console.error(`[Config] 加载 bot ${
|
|
134
|
-
this.botConfigs.set(
|
|
133
|
+
console.error(`[Config] 加载 bot ${botKey} 配置失败: ${e.message}`);
|
|
134
|
+
this.botConfigs.set(botKey, {});
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
138
138
|
/** 保存 Bot 级别配置 */
|
|
139
|
-
private _saveBotConfig(
|
|
139
|
+
private _saveBotConfig(botKey: string, config: BotLevelConfig): void {
|
|
140
140
|
const sessionsDir = getSessionsDir();
|
|
141
|
-
const configPath = path.join(sessionsDir, `${
|
|
141
|
+
const configPath = path.join(sessionsDir, `${botKey}_config.json`);
|
|
142
142
|
|
|
143
143
|
try {
|
|
144
144
|
if (!fs.existsSync(sessionsDir)) {
|
|
145
145
|
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
146
146
|
}
|
|
147
|
-
this.botConfigs.set(
|
|
147
|
+
this.botConfigs.set(botKey, config);
|
|
148
148
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
149
149
|
} catch (e: any) {
|
|
150
|
-
console.error(`[Config] 保存 bot ${
|
|
150
|
+
console.error(`[Config] 保存 bot ${botKey} 配置失败: ${e.message}`);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -249,10 +249,10 @@ export class FileConfigManager implements ConfigManager {
|
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
/** 保存 Bot 活跃模型 */
|
|
252
|
-
saveActiveModel(
|
|
253
|
-
const botLevel = this.botConfigs.get(
|
|
252
|
+
saveActiveModel(botKey: string, modelSpec: string): void {
|
|
253
|
+
const botLevel = this.botConfigs.get(botKey) || {};
|
|
254
254
|
botLevel.activeModel = modelSpec;
|
|
255
|
-
this._saveBotConfig(
|
|
255
|
+
this._saveBotConfig(botKey, botLevel);
|
|
256
256
|
|
|
257
257
|
// 同时更新全局配置
|
|
258
258
|
if (this.rawConfig) {
|
|
@@ -267,9 +267,9 @@ export class FileConfigManager implements ConfigManager {
|
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
/** 保存 Bot 模型别名 */
|
|
270
|
-
saveModelAliases(
|
|
271
|
-
const botLevel = this.botConfigs.get(
|
|
270
|
+
saveModelAliases(botKey: string, aliases: Record<string, string>): void {
|
|
271
|
+
const botLevel = this.botConfigs.get(botKey) || {};
|
|
272
272
|
botLevel.modelAliases = aliases;
|
|
273
|
-
this._saveBotConfig(
|
|
273
|
+
this._saveBotConfig(botKey, botLevel);
|
|
274
274
|
}
|
|
275
275
|
}
|
package/modules/core/session.ts
CHANGED
|
@@ -85,24 +85,24 @@ export class FileSessionManager implements SessionManager {
|
|
|
85
85
|
private cache = new Map<string, Map<string, Session>>();
|
|
86
86
|
|
|
87
87
|
/** 获取 Session 文件路径 */
|
|
88
|
-
private sessionPath(
|
|
88
|
+
private sessionPath(botKey: string, chatId: string): string {
|
|
89
89
|
const sessionsBase = getSessionsDir();
|
|
90
|
-
const botDir = path.join(sessionsBase,
|
|
90
|
+
const botDir = path.join(sessionsBase, botKey);
|
|
91
91
|
return path.join(botDir, `${chatId}.memory.json`);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/** 确保目录存在 */
|
|
95
|
-
private ensureDir(
|
|
95
|
+
private ensureDir(botKey: string): void {
|
|
96
96
|
const sessionsBase = getSessionsDir();
|
|
97
|
-
const botDir = path.join(sessionsBase,
|
|
97
|
+
const botDir = path.join(sessionsBase, botKey);
|
|
98
98
|
if (!fs.existsSync(botDir)) {
|
|
99
99
|
fs.mkdirSync(botDir, { recursive: true });
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
async getOrCreate(
|
|
103
|
+
async getOrCreate(botKey: string, chatId: string, userId: string): Promise<Session> {
|
|
104
104
|
// 先查缓存
|
|
105
|
-
const botCache = this.cache.get(
|
|
105
|
+
const botCache = this.cache.get(botKey);
|
|
106
106
|
if (botCache) {
|
|
107
107
|
const cached = botCache.get(chatId);
|
|
108
108
|
if (cached) {
|
|
@@ -112,8 +112,8 @@ export class FileSessionManager implements SessionManager {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
// 从文件加载
|
|
115
|
-
const filePath = this.sessionPath(
|
|
116
|
-
this.ensureDir(
|
|
115
|
+
const filePath = this.sessionPath(botKey, chatId);
|
|
116
|
+
this.ensureDir(botKey);
|
|
117
117
|
|
|
118
118
|
let session: Session;
|
|
119
119
|
|
|
@@ -147,10 +147,10 @@ export class FileSessionManager implements SessionManager {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
// 缓存
|
|
150
|
-
if (!this.cache.has(
|
|
151
|
-
this.cache.set(
|
|
150
|
+
if (!this.cache.has(botKey)) {
|
|
151
|
+
this.cache.set(botKey, new Map());
|
|
152
152
|
}
|
|
153
|
-
this.cache.get(
|
|
153
|
+
this.cache.get(botKey)!.set(chatId, session);
|
|
154
154
|
|
|
155
155
|
return session;
|
|
156
156
|
}
|
|
@@ -170,8 +170,8 @@ export class FileSessionManager implements SessionManager {
|
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
persist(
|
|
174
|
-
this.ensureDir(
|
|
173
|
+
persist(botKey: string, session: Session): void {
|
|
174
|
+
this.ensureDir(botKey);
|
|
175
175
|
|
|
176
176
|
// 写入时保持旧格式兼容:将 metadata 中的旧 ID 也写入顶层
|
|
177
177
|
const output: Record<string, any> = {
|
|
@@ -203,7 +203,7 @@ export class FileSessionManager implements SessionManager {
|
|
|
203
203
|
// metadata 完整保存
|
|
204
204
|
output.metadata = session.metadata;
|
|
205
205
|
|
|
206
|
-
const filePath = this.sessionPath(
|
|
206
|
+
const filePath = this.sessionPath(botKey, session.chatId);
|
|
207
207
|
try {
|
|
208
208
|
fs.writeFileSync(filePath, JSON.stringify(output, null, 2));
|
|
209
209
|
} catch (e: any) {
|
|
@@ -211,15 +211,15 @@ export class FileSessionManager implements SessionManager {
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
delete(
|
|
214
|
+
delete(botKey: string, chatId: string): void {
|
|
215
215
|
// 清除缓存
|
|
216
|
-
const botCache = this.cache.get(
|
|
216
|
+
const botCache = this.cache.get(botKey);
|
|
217
217
|
if (botCache) {
|
|
218
218
|
botCache.delete(chatId);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
// 删除文件
|
|
222
|
-
const filePath = this.sessionPath(
|
|
222
|
+
const filePath = this.sessionPath(botKey, chatId);
|
|
223
223
|
try {
|
|
224
224
|
if (fs.existsSync(filePath)) {
|
|
225
225
|
fs.unlinkSync(filePath);
|
|
@@ -229,8 +229,8 @@ export class FileSessionManager implements SessionManager {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
cleanupIdle(
|
|
233
|
-
const botCache = this.cache.get(
|
|
232
|
+
cleanupIdle(botKey: string, timeoutMs: number): void {
|
|
233
|
+
const botCache = this.cache.get(botKey);
|
|
234
234
|
if (!botCache) return;
|
|
235
235
|
|
|
236
236
|
const now = Date.now();
|
|
@@ -248,8 +248,8 @@ export class FileSessionManager implements SessionManager {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
listActive(
|
|
252
|
-
const botCache = this.cache.get(
|
|
251
|
+
listActive(botKey: string): Session[] {
|
|
252
|
+
const botCache = this.cache.get(botKey);
|
|
253
253
|
if (!botCache) return [];
|
|
254
254
|
return Array.from(botCache.values());
|
|
255
255
|
}
|
package/modules/core/types.ts
CHANGED
|
@@ -170,15 +170,15 @@ export interface MessageContext {
|
|
|
170
170
|
/** Session 管理器 */
|
|
171
171
|
export interface SessionManager {
|
|
172
172
|
/** 获取或创建 Session */
|
|
173
|
-
getOrCreate(
|
|
173
|
+
getOrCreate(botKey: string, chatId: string, userId: string): Promise<Session>;
|
|
174
174
|
/** 持久化 Session */
|
|
175
|
-
persist(
|
|
175
|
+
persist(botKey: string, session: Session): void;
|
|
176
176
|
/** 删除 Session */
|
|
177
|
-
delete(
|
|
177
|
+
delete(botKey: string, chatId: string): void;
|
|
178
178
|
/** 清理空闲 Session */
|
|
179
|
-
cleanupIdle(
|
|
179
|
+
cleanupIdle(botKey: string, timeoutMs: number): void;
|
|
180
180
|
/** 列出所有活跃 Session */
|
|
181
|
-
listActive(
|
|
181
|
+
listActive(botKey: string): Session[];
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
/** 错误处理器 */
|
|
@@ -233,6 +233,8 @@ export interface ErrorContext {
|
|
|
233
233
|
// ================================================================
|
|
234
234
|
|
|
235
235
|
export interface BotConfig {
|
|
236
|
+
/** 唯一标识(UUID,用于目录/文件隔离,改名不影响) */
|
|
237
|
+
id?: string;
|
|
236
238
|
name: string;
|
|
237
239
|
backend: string; // 'claude' | 'codex' | 'opencode'
|
|
238
240
|
appId: string;
|
|
@@ -37,8 +37,8 @@ export const DEFAULT_TERMINAL_CAPS: IMCapabilities = {
|
|
|
37
37
|
// 从 ~/Desktop/imtoagent/soul/{botName}/ 按顺序加载
|
|
38
38
|
// 加载顺序:rules → identity → profile → workspace → skills
|
|
39
39
|
// ================================================================
|
|
40
|
-
export function loadSoul(
|
|
41
|
-
const soulDir = getSoulDir(
|
|
40
|
+
export function loadSoul(botKey: string): string {
|
|
41
|
+
const soulDir = getSoulDir(botKey);
|
|
42
42
|
const soulOrder = ['rules.md', 'identity.md', 'profile.md', 'workspace.md', 'skills.md'];
|
|
43
43
|
const parts: string[] = [];
|
|
44
44
|
try {
|
|
@@ -64,7 +64,7 @@ export interface PromptBuilderContext {
|
|
|
64
64
|
/** 当 imModule 不可用时,手动指定的能力 */
|
|
65
65
|
caps?: IMCapabilities | null;
|
|
66
66
|
/** Bot 名称,用于加载 Soul */
|
|
67
|
-
|
|
67
|
+
botKey: string;
|
|
68
68
|
/** Agent 特有的额外系统提示(如工具使用指南、工作目录约束等) */
|
|
69
69
|
agentInstructions?: string;
|
|
70
70
|
}
|
package/modules/types.ts
CHANGED
|
@@ -134,6 +134,8 @@ export interface IMModule {
|
|
|
134
134
|
// ================================================================
|
|
135
135
|
|
|
136
136
|
export interface BotConfig {
|
|
137
|
+
/** 唯一标识(UUID,用于目录隔离,改名不影响) */
|
|
138
|
+
id?: string;
|
|
137
139
|
name: string;
|
|
138
140
|
backend: 'claude' | 'codex' | 'opencode';
|
|
139
141
|
appId: string;
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
// ================================================================
|
|
4
4
|
|
|
5
5
|
import { execSync } from 'child_process';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
6
8
|
|
|
7
9
|
export interface BackendInfo {
|
|
8
10
|
type: 'claude' | 'codex' | 'opencode';
|
|
@@ -18,20 +20,64 @@ const BACKEND_DEFS: Omit<BackendInfo, 'installed' | 'version'>[] = [
|
|
|
18
20
|
{ type: 'opencode', label: 'OpenCode', installHint: 'npm install -g opencode' },
|
|
19
21
|
];
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
// ================================================================
|
|
24
|
+
// 获取 npm 全局 bin 目录
|
|
25
|
+
// 解决 PATH 未包含 npm global bin 时的检测失败问题
|
|
26
|
+
// ================================================================
|
|
27
|
+
|
|
28
|
+
let _cachedNpmBin: string | null | undefined = undefined;
|
|
29
|
+
|
|
30
|
+
function getNpmGlobalBin(): string | null {
|
|
31
|
+
if (_cachedNpmBin !== undefined) return _cachedNpmBin;
|
|
22
32
|
try {
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
version = execSync('codex --version', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
28
|
-
} else {
|
|
29
|
-
version = execSync('opencode version', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
33
|
+
const prefix = execSync('npm get prefix', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
34
|
+
if (!prefix) {
|
|
35
|
+
_cachedNpmBin = null;
|
|
36
|
+
return null;
|
|
30
37
|
}
|
|
38
|
+
const binDir = path.join(prefix, 'bin');
|
|
39
|
+
if (fs.existsSync(binDir)) {
|
|
40
|
+
_cachedNpmBin = binDir;
|
|
41
|
+
return binDir;
|
|
42
|
+
}
|
|
43
|
+
_cachedNpmBin = null;
|
|
44
|
+
return null;
|
|
45
|
+
} catch {
|
|
46
|
+
_cachedNpmBin = null;
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkOne(b: Omit<BackendInfo, 'installed' | 'version'>): BackendInfo {
|
|
52
|
+
const versionCmd: Record<string, string> = {
|
|
53
|
+
claude: 'claude --version',
|
|
54
|
+
codex: 'codex --version',
|
|
55
|
+
opencode: 'opencode version',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// 先尝试 PATH 中的命令
|
|
59
|
+
try {
|
|
60
|
+
const version = execSync(versionCmd[b.type], { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
31
61
|
return { ...b, installed: true, version };
|
|
32
62
|
} catch {
|
|
33
|
-
|
|
63
|
+
// PATH 中找不到,继续尝试 npm global bin
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// fallback:直接从 npm global bin 目录运行
|
|
67
|
+
const npmBin = getNpmGlobalBin();
|
|
68
|
+
if (npmBin) {
|
|
69
|
+
const binPath = path.join(npmBin, b.type);
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(binPath)) {
|
|
72
|
+
const version = execSync(`"${binPath}" --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
73
|
+
return { ...b, installed: true, version };
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// bin 存在但执行失败,视为未安装
|
|
77
|
+
}
|
|
34
78
|
}
|
|
79
|
+
|
|
80
|
+
return { ...b, installed: false, version: null };
|
|
35
81
|
}
|
|
36
82
|
|
|
37
83
|
export function checkAllBackends(): BackendInfo[] {
|
|
@@ -59,7 +105,8 @@ export function formatBackendStatus(backends: BackendInfo[]): string {
|
|
|
59
105
|
|
|
60
106
|
/**
|
|
61
107
|
* 自动安装缺失的后端 CLI
|
|
62
|
-
* 使用
|
|
108
|
+
* 使用 bash -lc 加载用户 shell 环境,确保 npm 在 PATH 中
|
|
109
|
+
* 流式输出安装进度,支持 Ctrl+C 中断
|
|
63
110
|
*/
|
|
64
111
|
export async function installBackend(
|
|
65
112
|
type: 'claude' | 'codex' | 'opencode',
|
|
@@ -74,10 +121,16 @@ export async function installBackend(
|
|
|
74
121
|
console.log(` 命令: ${b.installHint}\n`);
|
|
75
122
|
|
|
76
123
|
try {
|
|
77
|
-
|
|
124
|
+
// 获取 npm 全局 bin 目录,用于安装后验证(复用 getNpmGlobalBin 缓存)
|
|
125
|
+
const npmBinDir = getNpmGlobalBin();
|
|
126
|
+
|
|
127
|
+
// 用 zsh -ic 加载用户 shell 环境(匹配 macOS 默认 shell)
|
|
128
|
+
// 传入当前 PATH 环境变量,确保 npm 可执行文件可访问
|
|
129
|
+
const child = Bun.spawn(['zsh', '-ic', b.installHint], {
|
|
78
130
|
stdout: 'pipe',
|
|
79
131
|
stderr: 'pipe',
|
|
80
|
-
stdin: '
|
|
132
|
+
stdin: 'ignore',
|
|
133
|
+
env: { ...process.env },
|
|
81
134
|
});
|
|
82
135
|
|
|
83
136
|
const decoder = new TextDecoder();
|
|
@@ -102,16 +155,35 @@ export async function installBackend(
|
|
|
102
155
|
|
|
103
156
|
if (exitCode !== 0) {
|
|
104
157
|
console.error(`\n❌ ${b.label} 安装失败 (退出码: ${exitCode})`);
|
|
158
|
+
console.error(` 可手动运行: ${b.installHint}`);
|
|
105
159
|
return false;
|
|
106
160
|
}
|
|
107
161
|
|
|
108
|
-
// 安装完成后验证
|
|
162
|
+
// 安装完成后验证 — 优先用 npm bin 目录直接检查
|
|
163
|
+
if (npmBinDir) {
|
|
164
|
+
const binPath = path.join(npmBinDir, b.type);
|
|
165
|
+
try {
|
|
166
|
+
if (fs.existsSync(binPath)) {
|
|
167
|
+
const version = execSync(`"${binPath}" --version`, { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
168
|
+
console.log(`\n✅ ${b.label} 安装成功! 版本: ${version}`);
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// fallback: 通过 PATH 查找
|
|
109
175
|
const info = checkOne(b);
|
|
110
176
|
if (info.installed) {
|
|
111
177
|
console.log(`\n✅ ${b.label} 安装成功! 版本: ${info.version}`);
|
|
112
178
|
return true;
|
|
113
179
|
} else {
|
|
114
|
-
console.error(`\n❌ ${b.label}
|
|
180
|
+
console.error(`\n❌ ${b.label} 安装后仍未检测到`);
|
|
181
|
+
if (npmBinDir) {
|
|
182
|
+
console.error(` npm 全局 bin 目录: ${npmBinDir}`);
|
|
183
|
+
console.error(` 建议将该目录添加到 PATH,或手动运行: ${b.installHint}`);
|
|
184
|
+
} else {
|
|
185
|
+
console.error(` 请手动运行: ${b.installHint}`);
|
|
186
|
+
}
|
|
115
187
|
return false;
|
|
116
188
|
}
|
|
117
189
|
} catch (e: any) {
|
package/modules/utils/paths.ts
CHANGED
|
@@ -191,8 +191,13 @@ export function getLogsDir(): string {
|
|
|
191
191
|
return path.join(getDataDir(), 'logs');
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
/** 解析 Bot 唯一 key:优先用 id(UUID),后向兼容用 name */
|
|
195
|
+
export function getBotKey(bot: { id?: string; name: string }): string {
|
|
196
|
+
return bot.id || bot.name;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function getSoulDir(botKey: string): string {
|
|
200
|
+
return path.join(getDataDir(), 'soul', botKey);
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
export function getRestoreMarkerPath(): string {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imtoagent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "IM
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"imtoagent": "./bin/imtoagent.cjs"
|
|
@@ -53,4 +53,4 @@
|
|
|
53
53
|
"url": "https://github.com/YOUR_USERNAME/imtoagent/issues"
|
|
54
54
|
},
|
|
55
55
|
"homepage": "https://github.com/YOUR_USERNAME/imtoagent#readme"
|
|
56
|
-
}
|
|
56
|
+
}
|