imtoagent 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -0
- package/bin/imtoagent +453 -0
- package/index.ts +1129 -0
- package/modules/agent/claude-adapter.ts +258 -0
- package/modules/agent/claude.ts +160 -0
- package/modules/agent/codex-adapter.ts +232 -0
- package/modules/agent/codex-exec-server.ts +513 -0
- package/modules/agent/codex.ts +275 -0
- package/modules/agent/opencode-adapter.ts +308 -0
- package/modules/agent/opencode.ts +247 -0
- package/modules/bot-context.ts +26 -0
- package/modules/capabilities.ts +189 -0
- package/modules/cli/setup.ts +424 -0
- package/modules/core/config.ts +275 -0
- package/modules/core/error.ts +124 -0
- package/modules/core/index.ts +39 -0
- package/modules/core/runtime.ts +282 -0
- package/modules/core/session.ts +256 -0
- package/modules/core/stats.ts +92 -0
- package/modules/core/types.ts +250 -0
- package/modules/im/feishu.ts +731 -0
- package/modules/im/telegram.ts +639 -0
- package/modules/im/wechat.ts +1094 -0
- package/modules/im/wecom.ts +603 -0
- package/modules/media/feishu-inbound-adapter.ts +108 -0
- package/modules/media/index.ts +27 -0
- package/modules/media/media-store.ts +273 -0
- package/modules/media/resolver.ts +178 -0
- package/modules/media/telegram-inbound-adapter.ts +124 -0
- package/modules/media/types.ts +76 -0
- package/modules/prompt-builder.ts +123 -0
- package/modules/proxy/anthropic-proxy.ts +1083 -0
- package/modules/proxy/codex-proxy.ts +657 -0
- package/modules/rate-limiter.ts +58 -0
- package/modules/types.ts +144 -0
- package/modules/utils/backend-check.ts +121 -0
- package/modules/utils/paths.ts +218 -0
- package/package.json +53 -0
- package/scripts/postinstall.ts +70 -0
- package/templates/config.template.json +57 -0
- package/templates/opencode.template.json +28 -0
- package/templates/providers.template.json +19 -0
- package/templates/soul.template/identity.md +6 -0
- package/templates/soul.template/profile.md +11 -0
- package/templates/soul.template/rules.md +7 -0
- package/templates/soul.template/skills.md +3 -0
- package/templates/soul.template/workspace.md +4 -0
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
|
+
|