imtoagent 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +234 -0
  2. package/bin/imtoagent +453 -0
  3. package/index.ts +1129 -0
  4. package/modules/agent/claude-adapter.ts +258 -0
  5. package/modules/agent/claude.ts +160 -0
  6. package/modules/agent/codex-adapter.ts +232 -0
  7. package/modules/agent/codex-exec-server.ts +513 -0
  8. package/modules/agent/codex.ts +275 -0
  9. package/modules/agent/opencode-adapter.ts +308 -0
  10. package/modules/agent/opencode.ts +247 -0
  11. package/modules/bot-context.ts +26 -0
  12. package/modules/capabilities.ts +189 -0
  13. package/modules/cli/setup.ts +424 -0
  14. package/modules/core/config.ts +275 -0
  15. package/modules/core/error.ts +124 -0
  16. package/modules/core/index.ts +39 -0
  17. package/modules/core/runtime.ts +282 -0
  18. package/modules/core/session.ts +256 -0
  19. package/modules/core/stats.ts +92 -0
  20. package/modules/core/types.ts +250 -0
  21. package/modules/im/feishu.ts +731 -0
  22. package/modules/im/telegram.ts +639 -0
  23. package/modules/im/wechat.ts +1094 -0
  24. package/modules/im/wecom.ts +603 -0
  25. package/modules/media/feishu-inbound-adapter.ts +108 -0
  26. package/modules/media/index.ts +27 -0
  27. package/modules/media/media-store.ts +273 -0
  28. package/modules/media/resolver.ts +178 -0
  29. package/modules/media/telegram-inbound-adapter.ts +124 -0
  30. package/modules/media/types.ts +76 -0
  31. package/modules/prompt-builder.ts +123 -0
  32. package/modules/proxy/anthropic-proxy.ts +1083 -0
  33. package/modules/proxy/codex-proxy.ts +657 -0
  34. package/modules/rate-limiter.ts +58 -0
  35. package/modules/types.ts +144 -0
  36. package/modules/utils/backend-check.ts +121 -0
  37. package/modules/utils/paths.ts +218 -0
  38. package/package.json +53 -0
  39. package/scripts/postinstall.ts +70 -0
  40. package/templates/config.template.json +57 -0
  41. package/templates/opencode.template.json +28 -0
  42. package/templates/providers.template.json +19 -0
  43. package/templates/soul.template/identity.md +6 -0
  44. package/templates/soul.template/profile.md +11 -0
  45. package/templates/soul.template/rules.md +7 -0
  46. package/templates/soul.template/skills.md +3 -0
  47. package/templates/soul.template/workspace.md +4 -0
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # imtoagent — IM ↔ Agent 统一网关
2
+
3
+ 将飞书、Telegram、个人微信、企业微信对接到 Claude Code、Codex (GPT)、OpenCode 等 AI 编程 Agent。
4
+
5
+ 一个网关,多个 IM,多种 Agent,统一端口代理。
6
+
7
+ ## 架构
8
+
9
+ ```
10
+ 飞书/Telegram/微信/企微 → IM Registry 工厂 → Bot 实例
11
+ → AgentRuntime SDK → Agent Adapter
12
+ → 统一 Proxy (:18899) → 上游模型
13
+ ```
14
+
15
+ ### 已支持的 IM 适配器
16
+
17
+ | IM | 连接方式 | 能力 |
18
+ |----|----------|------|
19
+ | **飞书** | WebSocket 长连接 + 自动重连 | 文本、代码块、卡片、文件、图片、语音、按钮 |
20
+ | **Telegram** | 长轮询 + HTTP 代理 | 文本、文件、图片、语音 |
21
+ | **个人微信** | iLink HTTP long-poll + QR 扫码 | 文本、图片、文件、语音(AES-128-ECB 加密) |
22
+ | **企业微信** | HTTP Webhook 回调 + REST API | 文本、文件、图片 |
23
+
24
+ ### 已支持的 Agent 后端
25
+
26
+ | 后端 | 对接方式 |
27
+ |------|----------|
28
+ | **Claude Code** | Claude Agent SDK spawn 子进程 |
29
+ | **Codex** | app-server v2 (stdio JSON-RPC) |
30
+ | **OpenCode** | HTTP API client |
31
+
32
+ ## 快速开始
33
+
34
+ ### 前置条件
35
+
36
+ - **Bun** 运行时(≥1.0.0):`brew install oven-sh/bun/bun`
37
+ - **macOS / Linux**
38
+ - **至少一个 Agent 后端**(见下表,安装 imtoagent 前或后安装均可)
39
+
40
+ | 后端 | 安装命令 |
41
+ |------|----------|
42
+ | Claude Code | `npm install -g @anthropic-ai/claude-agent-sdk` |
43
+ | Codex | `npm install -g @openai/codex` |
44
+ | OpenCode | `npm install -g opencode` |
45
+
46
+ ### 安装
47
+
48
+ #### 方式一:npm 全局安装(推荐)
49
+
50
+ ```bash
51
+ npm install -g imtoagent
52
+ ```
53
+
54
+ 安装完成后自动检测是否需要初始配置,交互式终端会自动引导进入配置向导。
55
+
56
+ #### 方式二:源码安装
57
+
58
+ ```bash
59
+ git clone https://github.com/YOUR_USERNAME/imtoagent.git
60
+ cd imtoagent
61
+ bun install
62
+ bun run bin/imtoagent setup
63
+ ```
64
+
65
+ ### 首次配置
66
+
67
+ ```bash
68
+ imtoagent setup
69
+ ```
70
+
71
+ 交互式配置向导引导你完成:
72
+
73
+ 1. **配置 Bot** — 选择 IM 平台 + Agent 后端
74
+ 2. **配置模型供应商** — 添加 API 凭证(DeepSeek、Dashscope 等)
75
+ 3. **生成灵魂文件** — 为每个 Bot 创建 rules.md / identity.md 等
76
+ 4. **写入配置文件** — 自动生成 `~/.imtoagent/config.json`
77
+
78
+ #### 飞书 Bot 需要
79
+
80
+ - 飞书 App ID(`cli_...`)
81
+ - 飞书 App Secret
82
+ - 飞书应用需开启:机器人、事件订阅、消息收发权限
83
+
84
+ #### Telegram Bot 需要
85
+
86
+ - Telegram Bot Token(从 @BotFather 获取)
87
+ - 可选:代理地址(如 `http://127.0.0.1:7890`)
88
+
89
+ #### 个人微信
90
+
91
+ - 首次运行 `imtoagent start` 后自动弹出 QR 码
92
+ - 用手机微信扫码完成绑定
93
+
94
+ ### 启动网关
95
+
96
+ ```bash
97
+ imtoagent start # 后台启动
98
+ imtoagent status # 查看运行状态
99
+ imtoagent stop # 停止网关
100
+ ```
101
+
102
+ ### 开机自启(macOS launchd)
103
+
104
+ ```bash
105
+ # 创建 launchd 配置
106
+ cat > ~/Library/LaunchAgents/com.imtoagent.plist << 'EOF'
107
+ <?xml version="1.0" encoding="UTF-8"?>
108
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
109
+ <plist version="1.0">
110
+ <dict>
111
+ <key>Label</key>
112
+ <string>com.imtoagent</string>
113
+ <key>ProgramArguments</key>
114
+ <array>
115
+ <string>/opt/homebrew/bin/bun</string>
116
+ <string>run</string>
117
+ <string>/usr/local/lib/node_modules/imtoagent/index.ts</string>
118
+ <string>daemon</string>
119
+ </array>
120
+ <key>WorkingDirectory</key>
121
+ <string>/Users/$USER/.imtoagent</string>
122
+ <key>RunAtLoad</key>
123
+ <true/>
124
+ <key>KeepAlive</key>
125
+ <dict>
126
+ <key>SuccessfulExit</key>
127
+ <false/>
128
+ </dict>
129
+ <key>StandardOutPath</key>
130
+ <string>/Users/$USER/.imtoagent/logs/launchd.out.log</string>
131
+ <key>StandardErrorPath</key>
132
+ <string>/Users/$USER/.imtoagent/logs/launchd.err.log</string>
133
+ </dict>
134
+ </plist>
135
+ EOF
136
+
137
+ # 加载
138
+ launchctl load ~/Library/LaunchAgents/com.imtoagent.plist
139
+ ```
140
+
141
+ ### 常用命令
142
+
143
+ | 命令 | 说明 |
144
+ |------|------|
145
+ | `imtoagent setup` | 交互式配置向导 |
146
+ | `imtoagent start` | 后台启动网关 |
147
+ | `imtoagent stop` | 停止网关 |
148
+ | `imtoagent status` | 查看运行状态 |
149
+ | `imtoagent restore` | 热重载恢复 |
150
+ | `imtoagent daemon` | 前台守护模式(适合 launchd/systemd 托管) |
151
+
152
+ ### 网关内建命令
153
+
154
+ 在 IM 聊天中发送给 Bot:
155
+
156
+ | 命令 | 说明 |
157
+ |------|------|
158
+ | `/help` | 帮助信息 |
159
+ | `/status` | 网关状态 |
160
+ | `/stats` | 使用统计 |
161
+ | `/model` | 切换模型 |
162
+ | `/providers` | 查看供应商 |
163
+ | `/memory` | 查看记忆 |
164
+ | `/soul` | 灵魂管理 |
165
+ | `/reload` | 重新加载 |
166
+ | `/clear` | 清除会话 |
167
+ | `/mode` | 切换模式(权限/auto/plan) |
168
+ | `/dir` | 切换工作目录 |
169
+
170
+ ## 项目结构
171
+
172
+ ```
173
+ imtoagent/
174
+ ├── index.ts # 入口 — IM Registry + Bot 构造 + Proxy 启动
175
+ ├── bin/imtoagent # CLI 命令入口
176
+ ├── modules/
177
+ │ ├── core/ # SDK Core
178
+ │ │ ├── AgentRuntime.ts # 消息处理中枢
179
+ │ │ ├── AgentAdapter.ts # Agent 后端统一抽象
180
+ │ │ ├── SessionManager.ts # 会话持久化
181
+ │ │ └── types.ts # 类型定义
182
+ │ ├── im/ # IM 适配器
183
+ │ │ ├── feishu.ts # 飞书
184
+ │ │ ├── telegram.ts # Telegram
185
+ │ │ ├── wechat.ts # 个人微信
186
+ │ │ └── wecom.ts # 企业微信
187
+ │ ├── agent/ # Agent 后端
188
+ │ │ ├── claude-adapter.ts # Claude Code
189
+ │ │ ├── codex-adapter.ts # Codex
190
+ │ │ └── opencode-adapter.ts # OpenCode
191
+ │ ├── proxy/ # 统一代理
192
+ │ │ └── anthropic-proxy.ts # :18899 Anthropic 格式代理
193
+ │ ├── cli/ # CLI
194
+ │ │ └── setup.ts # 交互式配置向导
195
+ │ └── utils/
196
+ │ └── paths.ts # 路径解析 + 自动初始化
197
+ ├── scripts/
198
+ │ └── postinstall.ts # npm 安装后引导
199
+ ├── templates/ # 配置模板
200
+ │ ├── config.template.json
201
+ │ ├── providers.template.json
202
+ │ ├── opencode.template.json
203
+ │ └── soul.template/
204
+ └── README.md
205
+ ```
206
+
207
+ ## 数据目录
208
+
209
+ 所有运行时数据统一存储在 `~/.imtoagent/`:
210
+
211
+ ```
212
+ ~/.imtoagent/
213
+ ├── config.json # 主配置(Bot + 供应商 + 系统)
214
+ ├── providers.json # 模型供应商配置
215
+ ├── opencode.json # OpenCode 配置
216
+ ├── sessions/ # 会话持久化
217
+ ├── logs/ # 运行日志
218
+ └── soul/ # 灵魂文件(每 Bot 一个目录)
219
+ ├── ClaudeBot/
220
+ ├── CodexBot/
221
+ └── ...
222
+ ```
223
+
224
+ ## 开发
225
+
226
+ ```bash
227
+ bun install
228
+ bun run index.ts # 直接运行
229
+ bun run bin/imtoagent setup # 运行配置向导
230
+ ```
231
+
232
+ ## License
233
+
234
+ MIT
package/bin/imtoagent ADDED
@@ -0,0 +1,453 @@
1
+ #!/usr/bin/env bun
2
+ // ================================================================
3
+ // imtoagent CLI — 全局命令入口
4
+ // ================================================================
5
+ // npm install -g imtoagent 后可用:
6
+ // imtoagent setup — 交互式配置向导
7
+ // imtoagent start — 后台启动网关
8
+ // imtoagent stop — 停止网关
9
+ // imtoagent status — 查看运行状态
10
+ // imtoagent restore — 热重载恢复
11
+ // imtoagent daemon — 前台守护模式(自动重启 + 日志)
12
+ // ================================================================
13
+
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+ import * as readline from 'readline';
17
+ import { getDataDir } from '../modules/utils/paths';
18
+
19
+ const PID_FILE = '/tmp/imtoagent.pid';
20
+
21
+ // ================================================================
22
+ // 命令分发
23
+ // ================================================================
24
+ const command = process.argv[2];
25
+
26
+ switch (command) {
27
+ case 'setup':
28
+ await cmdSetup();
29
+ break;
30
+ case 'start':
31
+ await cmdStart();
32
+ break;
33
+ case 'stop':
34
+ await cmdStop();
35
+ break;
36
+ case 'status':
37
+ await cmdStatus();
38
+ break;
39
+ case 'restore':
40
+ await cmdRestore();
41
+ break;
42
+ case 'daemon':
43
+ await cmdDaemon();
44
+ break;
45
+ case undefined:
46
+ case 'help':
47
+ case '--help':
48
+ case '-h':
49
+ // 无命令 + 无配置 → 自动引导进入 setup
50
+ if (!command && !fs.existsSync(path.join(getDataDir(), 'config.json'))) {
51
+ console.log('⚠️ 未检测到配置文件,请先完成初始配置\n');
52
+ await cmdSetup();
53
+ } else {
54
+ printHelp();
55
+ }
56
+ break;
57
+ default:
58
+ console.error(`❌ 未知命令: ${command}`);
59
+ printHelp();
60
+ process.exit(1);
61
+ }
62
+
63
+ // ================================================================
64
+ // Help
65
+ // ================================================================
66
+ function printHelp() {
67
+ console.log(`
68
+ imtoagent — IM ↔ Agent 统一网关
69
+
70
+ 用法:
71
+ imtoagent setup 交互式配置向导
72
+ imtoagent start 后台启动网关
73
+ imtoagent stop 停止网关
74
+ imtoagent status 查看运行状态
75
+ imtoagent restore 热重载恢复
76
+ imtoagent daemon 前台守护模式(自动重启 + 日志,适合 launchd/systemd 托管)
77
+
78
+ 数据目录: ${getDataDir()}
79
+ `);
80
+ }
81
+
82
+ // ================================================================
83
+ // setup — 交互式向导
84
+ // ================================================================
85
+ async function cmdSetup() {
86
+ const { runSetupWizard } = await import('../modules/cli/setup');
87
+ await runSetupWizard();
88
+ }
89
+
90
+ // ================================================================
91
+ // start — 后台启动
92
+ // ================================================================
93
+ async function cmdStart() {
94
+ // 检查是否已在运行
95
+ if (fs.existsSync(PID_FILE)) {
96
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
97
+ try {
98
+ process.kill(pid, 0);
99
+ console.error(`❌ 网关已在运行 (PID=${pid})`);
100
+ console.error(` 运行 "imtoagent stop" 先停止`);
101
+ process.exit(1);
102
+ } catch {
103
+ // 旧 PID 文件残留,清理
104
+ fs.unlinkSync(PID_FILE);
105
+ }
106
+ }
107
+
108
+ // 检查配置是否存在
109
+ const dataDir = getDataDir();
110
+ const configPath = path.join(dataDir, 'config.json');
111
+ if (!fs.existsSync(configPath)) {
112
+ console.error('❌ 未找到配置文件,请先运行 "imtoagent setup"');
113
+ process.exit(1);
114
+ }
115
+
116
+ // 检查配置的后端是否已安装
117
+ try {
118
+ const { checkBackend } = await import('../modules/utils/backend-check');
119
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
120
+ const missingBackends: string[] = [];
121
+ for (const bot of config.bots || []) {
122
+ if (bot.backend && ['claude', 'codex', 'opencode'].includes(bot.backend)) {
123
+ const info = checkBackend(bot.backend as any);
124
+ if (!info.installed && !missingBackends.includes(bot.backend)) {
125
+ missingBackends.push(bot.backend);
126
+ }
127
+ }
128
+ }
129
+ if (missingBackends.length > 0) {
130
+ console.error(`\n⚠️ 以下后端已配置但未安装:`);
131
+ for (const name of missingBackends) {
132
+ const b = BACKEND_DEFS.find((d) => d.type === name);
133
+ console.error(` ❌ ${name} ${b ? `(${b.label})` : ''}`);
134
+ }
135
+
136
+ // 交互式询问是否自动安装
137
+ const rl = readline.createInterface({
138
+ input: process.stdin,
139
+ output: process.stdout,
140
+ });
141
+
142
+ const answer = await new Promise<string>((resolve) => {
143
+ rl.question('\n🔧 是否自动安装缺失的后端?[Y/n]: ', resolve);
144
+ });
145
+ rl.close();
146
+
147
+ if (answer.trim().toLowerCase() !== 'n') {
148
+ const { installBackend } = await import('../modules/utils/backend-check');
149
+ let allInstalled = true;
150
+
151
+ for (const name of missingBackends) {
152
+ const ok = await installBackend(name as 'claude' | 'codex' | 'opencode');
153
+ if (!ok) allInstalled = false;
154
+ }
155
+
156
+ if (allInstalled) {
157
+ console.log('\n✅ 所有后端安装完成,正在启动网关...\n');
158
+ } else {
159
+ console.error('\n⚠️ 部分后端安装失败。网关仍可启动,但使用未安装的后端时会报错。\n');
160
+ }
161
+ } else {
162
+ console.error('\n跳过安装。网关仍可启动,但使用未安装的后端时会报错。\n');
163
+ }
164
+ }
165
+ } catch {
166
+ // 检查失败不影响启动
167
+ }
168
+
169
+ console.log('🚀 启动 imtoagent 网关...');
170
+ console.log(` 数据目录: ${dataDir}`);
171
+ console.log(` 配置文件: ${configPath}`);
172
+
173
+ // 使用 Bun.spawn 后台启动
174
+ const pkgDir = path.resolve(import.meta.dirname, '..');
175
+ const indexFile = path.join(pkgDir, 'index.ts');
176
+
177
+ const child = Bun.spawn(['bun', 'run', indexFile], {
178
+ cwd: dataDir,
179
+ env: { ...process.env, IMTOAGENT_HOME: dataDir },
180
+ stdout: 'pipe',
181
+ stderr: 'pipe',
182
+ });
183
+
184
+ fs.writeFileSync(PID_FILE, String(child.pid));
185
+ console.log(`✅ 网关已启动 (PID=${child.pid})`);
186
+
187
+ // 后台日志重定向到 logs/
188
+ const logsDir = path.join(dataDir, 'logs');
189
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
190
+ const logFile = path.join(logsDir, 'imtoagent.log');
191
+
192
+ // 启动日志收集
193
+ (async () => {
194
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
195
+ for await (const chunk of child.stdout as any) {
196
+ const line = new TextDecoder().decode(chunk);
197
+ process.stdout.write(line);
198
+ logStream.write(line);
199
+ }
200
+ })().catch(() => {});
201
+
202
+ (async () => {
203
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
204
+ for await (const chunk of child.stderr as any) {
205
+ const line = new TextDecoder().decode(chunk);
206
+ process.stderr.write(line);
207
+ logStream.write(line);
208
+ }
209
+ })().catch(() => {});
210
+
211
+ // 等待启动验证(5 秒内检查 PID 是否存活)
212
+ await new Promise(r => setTimeout(r, 3000));
213
+ try {
214
+ process.kill(child.pid, 0);
215
+ console.log('✅ 网关运行正常');
216
+ } catch {
217
+ console.error('❌ 网关启动失败,查看日志:');
218
+ if (fs.existsSync(logFile)) {
219
+ console.log(fs.readFileSync(logFile, 'utf-8').slice(-2000));
220
+ }
221
+ fs.unlinkSync(PID_FILE);
222
+ process.exit(1);
223
+ }
224
+ }
225
+
226
+ // ================================================================
227
+ // stop — 停止
228
+ // ================================================================
229
+ async function cmdStop() {
230
+ if (!fs.existsSync(PID_FILE)) {
231
+ console.log('ℹ️ 网关未运行');
232
+ return;
233
+ }
234
+
235
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
236
+ try {
237
+ process.kill(pid, 0);
238
+ console.log(`⏹ 正在停止网关 (PID=${pid})...`);
239
+ process.kill(pid, 'SIGTERM');
240
+
241
+ // 等待退出
242
+ for (let i = 0; i < 20; i++) {
243
+ try {
244
+ process.kill(pid, 0);
245
+ await new Promise(r => setTimeout(r, 500));
246
+ } catch {
247
+ break;
248
+ }
249
+ }
250
+
251
+ // 检查是否还在
252
+ try {
253
+ process.kill(pid, 0);
254
+ console.log('⚠️ 进程未响应,强制终止...');
255
+ process.kill(pid, 'SIGKILL');
256
+ } catch {
257
+ console.log('✅ 网关已停止');
258
+ }
259
+ } catch {
260
+ console.log('ℹ️ 网关未运行(PID 文件残留,已清理)');
261
+ }
262
+
263
+ try { fs.unlinkSync(PID_FILE); } catch {}
264
+ }
265
+
266
+ // ================================================================
267
+ // status — 状态
268
+ // ================================================================
269
+ async function cmdStatus() {
270
+ const dataDir = getDataDir();
271
+
272
+ console.log(`\n📊 imtoagent 状态`);
273
+ console.log(` 数据目录: ${dataDir}`);
274
+
275
+ // 进程状态
276
+ if (fs.existsSync(PID_FILE)) {
277
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
278
+ try {
279
+ process.kill(pid, 0);
280
+ console.log(` 进程: ✅ 运行中 (PID=${pid})`);
281
+ } catch {
282
+ console.log(` 进程: ❌ 已停止 (PID=${pid} 不存在)`);
283
+ }
284
+ } else {
285
+ console.log(` 进程: ⏸ 未运行`);
286
+ }
287
+
288
+ // 配置文件
289
+ const configPath = path.join(dataDir, 'config.json');
290
+ if (fs.existsSync(configPath)) {
291
+ try {
292
+ const cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
293
+ const bots = cfg.bots || [];
294
+ console.log(` 配置: ✅ 已配置 (${bots.length} 个 Bot)`);
295
+ for (const bot of bots) {
296
+ console.log(` - ${bot.name} (${bot.backend})`);
297
+ }
298
+ } catch {
299
+ console.log(` 配置: ❌ 解析失败`);
300
+ }
301
+ } else {
302
+ console.log(` 配置: ❌ 未找到 (运行 "imtoagent setup")`);
303
+ }
304
+
305
+ // 日志
306
+ const logFile = path.join(dataDir, 'logs', 'imtoagent.log');
307
+ if (fs.existsSync(logFile)) {
308
+ const stats = fs.statSync(logFile);
309
+ const size = stats.size > 1024 * 1024
310
+ ? (stats.size / (1024 * 1024)).toFixed(1) + ' MB'
311
+ : (stats.size / 1024).toFixed(1) + ' KB';
312
+ console.log(` 日志: ${size} (${logFile})`);
313
+ }
314
+
315
+ console.log();
316
+ }
317
+
318
+ // ================================================================
319
+ // restore — 热重载
320
+ // ================================================================
321
+ async function cmdRestore() {
322
+ if (!fs.existsSync(PID_FILE)) {
323
+ console.error('❌ 网关未运行,无法热重载');
324
+ process.exit(1);
325
+ }
326
+
327
+ const pid = parseInt(fs.readFileSync(PID_FILE, 'utf-8').trim());
328
+ console.log(`🔄 发送 SIGHUP 到网关 (PID=${pid})...`);
329
+ try {
330
+ process.kill(pid, 'SIGHUP');
331
+ console.log('✅ 热重载信号已发送');
332
+ } catch (e: any) {
333
+ console.error(`❌ 发送失败: ${e.message}`);
334
+ process.exit(1);
335
+ }
336
+ }
337
+
338
+ // ================================================================
339
+ // daemon — 前台守护模式(自动重启 + 日志 + 优雅退出)
340
+ // ================================================================
341
+ // 设计用途:
342
+ // - 前台运行,被 launchd / systemd 等进程管理器托管
343
+ // - 崩溃时自动重启(指数退避,最长 30s)
344
+ // - 收到 SIGTERM/SIGINT 时优雅关闭,不重启
345
+ // - 日志写入 ~/.imtoagent/logs/imtoagent.log
346
+ // ================================================================
347
+ async function cmdDaemon(): Promise<void> {
348
+ const dataDir = getDataDir();
349
+ const configPath = path.join(dataDir, 'config.json');
350
+
351
+ if (!fs.existsSync(configPath)) {
352
+ console.error('❌ 未找到配置文件,请先运行 "imtoagent setup"');
353
+ process.exit(1);
354
+ }
355
+
356
+ const logsDir = path.join(dataDir, 'logs');
357
+ if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
358
+ const logFile = path.join(logsDir, 'imtoagent.log');
359
+
360
+ const pkgDir = path.resolve(import.meta.dirname, '..');
361
+ const indexFile = path.join(pkgDir, 'index.ts');
362
+
363
+ console.log(`🛡 imtoagent 守护模式`);
364
+ console.log(` 数据目录: ${dataDir}`);
365
+ console.log(` 日志文件: ${logFile}`);
366
+ console.log(` 按 Ctrl+C 停止\n`);
367
+
368
+ // 优雅退出标记
369
+ let shuttingDown = false;
370
+
371
+ const shutdown = () => {
372
+ if (shuttingDown) return;
373
+ shuttingDown = true;
374
+ console.log('\n🛑 收到停止信号,正在关闭...');
375
+ };
376
+
377
+ process.on('SIGTERM', shutdown);
378
+ process.on('SIGINT', shutdown);
379
+
380
+ let retryDelay = 0;
381
+ const MAX_RETRY_DELAY = 30_000; // 30s 上限
382
+
383
+ while (!shuttingDown) {
384
+ // 首次无延迟,之后指数退避
385
+ if (retryDelay > 0) {
386
+ console.log(` 等待 ${retryDelay / 1000}s 后重启...`);
387
+ await new Promise<void>(resolve => {
388
+ const timer = setTimeout(resolve, retryDelay);
389
+ // 等待期间如果收到停止信号,立即退出
390
+ const check = setInterval(() => {
391
+ if (shuttingDown) {
392
+ clearTimeout(timer);
393
+ clearInterval(check);
394
+ resolve();
395
+ }
396
+ }, 100);
397
+ });
398
+ if (shuttingDown) break;
399
+ }
400
+
401
+ const logStream = fs.createWriteStream(logFile, { flags: 'a' });
402
+
403
+ const child = Bun.spawn(['bun', 'run', indexFile], {
404
+ cwd: dataDir,
405
+ env: { ...process.env, IMTOAGENT_HOME: dataDir },
406
+ stdout: 'pipe',
407
+ stderr: 'pipe',
408
+ });
409
+
410
+ const childPid = child.pid;
411
+ fs.writeFileSync(PID_FILE, String(childPid));
412
+ console.log(`[${new Date().toISOString()}] 🚀 启动网关 (PID=${childPid})`);
413
+
414
+ // 日志收集
415
+ const pumpStdout = (async () => {
416
+ for await (const chunk of child.stdout as any) {
417
+ const line = new TextDecoder().decode(chunk);
418
+ process.stdout.write(line);
419
+ logStream.write(line);
420
+ }
421
+ })().catch(() => {});
422
+
423
+ const pumpStderr = (async () => {
424
+ for await (const chunk of child.stderr as any) {
425
+ const line = new TextDecoder().decode(chunk);
426
+ process.stderr.write(line);
427
+ logStream.write(line);
428
+ }
429
+ })().catch(() => {});
430
+
431
+ // 等待子进程退出
432
+ const exitCode = await child.exited;
433
+ await Promise.allSettled([pumpStdout, pumpStderr]);
434
+ logStream.end();
435
+
436
+ try { fs.unlinkSync(PID_FILE); } catch {}
437
+
438
+ if (shuttingDown) break;
439
+
440
+ // 判断是否需要重启
441
+ if (exitCode === 0) {
442
+ console.log(`[${new Date().toISOString()}] ⏹ 网关正常退出 (code=0),不重启`);
443
+ break;
444
+ }
445
+
446
+ // 崩溃 → 指数退避重启
447
+ retryDelay = retryDelay === 0 ? 3_000 : Math.min(retryDelay * 2, MAX_RETRY_DELAY);
448
+ console.log(`[${new Date().toISOString()}] ⚠️ 网关异常退出 (code=${exitCode}),${retryDelay / 1000}s 后重启`);
449
+ }
450
+
451
+ console.log('👋 守护进程已停止');
452
+ }
453
+