imtoagent 0.3.7 → 0.3.8
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 +1 -7
- package/modules/agent/codex-exec-server.ts +0 -4
- package/modules/proxy/codex-proxy.ts +1 -19
- package/package.json +1 -2
- package/install.sh +0 -313
- package/modules/agent/claude.ts +0 -160
- package/modules/agent/codex.ts +0 -275
- package/modules/agent/opencode.ts +0 -247
- package/scripts/postinstall.ts +0 -70
package/index.ts
CHANGED
|
@@ -72,8 +72,7 @@ registerIM('wechat', {
|
|
|
72
72
|
},
|
|
73
73
|
});
|
|
74
74
|
import { startAnthropicProxy, stopAnthropicProxy } from './modules/proxy/anthropic-proxy';
|
|
75
|
-
import {
|
|
76
|
-
import { initOpenCodeConfig } from './modules/agent/opencode';
|
|
75
|
+
import { initCodexProxyConfig } from './modules/proxy/codex-proxy';
|
|
77
76
|
import { checkRateLimit, setRateLimitConfig } from './modules/rate-limiter';
|
|
78
77
|
import { setCurrentBot } from './modules/bot-context';
|
|
79
78
|
import { getDataDir, getSessionsDir, getSoulDir, getBotKey, getRestoreMarkerPath } from './modules/utils/paths';
|
|
@@ -904,11 +903,6 @@ async function main() {
|
|
|
904
903
|
upstream: codexCfg.upstream || 'https://api.deepseek.com/v1/chat/completions',
|
|
905
904
|
apiKey,
|
|
906
905
|
});
|
|
907
|
-
const ocCfg = config.opencode || {};
|
|
908
|
-
initOpenCodeConfig({
|
|
909
|
-
serverUrl: ocCfg.serverUrl || 'http://localhost:4096',
|
|
910
|
-
defaultModel: ocCfg.defaultModel || { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
911
|
-
});
|
|
912
906
|
const rlCfg = config.rateLimit || {};
|
|
913
907
|
if (rlCfg.enabled !== false) {
|
|
914
908
|
setRateLimitConfig({
|
|
@@ -22,9 +22,6 @@ let _config: ExecServerConfig = {
|
|
|
22
22
|
maxToolCallsPerTurn: 80,
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
export function setExecServerConfig(cfg: Partial<ExecServerConfig>) {
|
|
26
|
-
_config = { ..._config, ...cfg };
|
|
27
|
-
}
|
|
28
25
|
|
|
29
26
|
// ================================================================
|
|
30
27
|
// 事件类型
|
|
@@ -510,4 +507,3 @@ export async function shutdownAppServer(): Promise<void> {
|
|
|
510
507
|
export const shutdownExecServer = shutdownAppServer;
|
|
511
508
|
|
|
512
509
|
// 向后兼容的类型别名
|
|
513
|
-
export type CodexExecServerClient = CodexAppServerClient;
|
|
@@ -525,19 +525,10 @@ async function streamResponse(upstreamRes: Response, resWriter: WritableStreamDe
|
|
|
525
525
|
}
|
|
526
526
|
|
|
527
527
|
// ================================================================
|
|
528
|
-
//
|
|
529
|
-
// usage 累加器 — 供网关读取 Codex 的 Token/成本统计
|
|
528
|
+
// usage 累加器 — 内部统计,供 accumulateProxyUsage 记录
|
|
530
529
|
// ================================================================
|
|
531
530
|
let _proxyUsage = { inputTokens: 0, outputTokens: 0 };
|
|
532
531
|
|
|
533
|
-
export function getProxyUsage() {
|
|
534
|
-
return { ..._proxyUsage };
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
export function resetProxyUsage() {
|
|
538
|
-
_proxyUsage = { inputTokens: 0, outputTokens: 0 };
|
|
539
|
-
}
|
|
540
|
-
|
|
541
532
|
export function accumulateProxyUsage(inputTokens: number, outputTokens: number) {
|
|
542
533
|
_proxyUsage.inputTokens += inputTokens;
|
|
543
534
|
_proxyUsage.outputTokens += outputTokens;
|
|
@@ -646,12 +637,3 @@ export async function handleCodexRequest(
|
|
|
646
637
|
res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'internal error' })); return;
|
|
647
638
|
}
|
|
648
639
|
}
|
|
649
|
-
|
|
650
|
-
// 兼容旧引用(不再启动独立服务器)
|
|
651
|
-
export function startCodexProxy(_port?: number): Promise<number> {
|
|
652
|
-
console.log('[Codex Proxy] Merged into port 18899');
|
|
653
|
-
return Promise.resolve(18899);
|
|
654
|
-
}
|
|
655
|
-
export function stopCodexProxy(): Promise<void> {
|
|
656
|
-
return Promise.resolve();
|
|
657
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "imtoagent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.8",
|
|
4
4
|
"description": "IM ↔ Agent 统一网关 — 飞书/Telegram/微信/企业微信对接 Claude Code/Codex/OpenCode",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"templates/",
|
|
14
14
|
"scripts/",
|
|
15
15
|
"README.md",
|
|
16
|
-
"install.sh",
|
|
17
16
|
"bin/imtoagent.ts",
|
|
18
17
|
"bin/imtoagent.cjs",
|
|
19
18
|
"bin/imtoagent-real"
|
package/install.sh
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
#
|
|
3
|
-
# imtoagent — One-Click Install Script
|
|
4
|
-
# Usage: curl -fsSL https://raw.githubusercontent.com/imtoagent/imtoagent/main/scripts/install.sh | bash
|
|
5
|
-
#
|
|
6
|
-
# This script detects the environment, installs dependencies, installs/upgrades
|
|
7
|
-
# imtoagent, and optionally runs the interactive setup wizard.
|
|
8
|
-
#
|
|
9
|
-
# Flags:
|
|
10
|
-
# --non-interactive Skip interactive setup (for CI/automated installs)
|
|
11
|
-
# --skip-bun Skip bun installation check
|
|
12
|
-
# --skip-start Don't start the gateway after install
|
|
13
|
-
|
|
14
|
-
set -euo pipefail
|
|
15
|
-
|
|
16
|
-
# ============================================================
|
|
17
|
-
# Colors & Helpers
|
|
18
|
-
# ============================================================
|
|
19
|
-
RED='\033[0;31m'
|
|
20
|
-
GREEN='\033[0;32m'
|
|
21
|
-
YELLOW='\033[1;33m'
|
|
22
|
-
BLUE='\033[0;34m'
|
|
23
|
-
CYAN='\033[0;36m'
|
|
24
|
-
BOLD='\033[1m'
|
|
25
|
-
NC='\033[0m'
|
|
26
|
-
|
|
27
|
-
info() { echo -e "${BLUE}ℹ ${NC}$1"; }
|
|
28
|
-
ok() { echo -e "${GREEN}✅ ${NC}$1"; }
|
|
29
|
-
warn() { echo -e "${YELLOW}⚠️ ${NC}$1"; }
|
|
30
|
-
error() { echo -e "${RED}❌ ${NC}$1"; }
|
|
31
|
-
step() { echo -e "\n${BOLD}${CYAN}▸ $1${NC}"; }
|
|
32
|
-
done_ok() { echo -e " ${GREEN}✓${NC} $1"; }
|
|
33
|
-
|
|
34
|
-
# ============================================================
|
|
35
|
-
# Parse flags
|
|
36
|
-
# ============================================================
|
|
37
|
-
NON_INTERACTIVE=false
|
|
38
|
-
SKIP_BUN=false
|
|
39
|
-
SKIP_START=false
|
|
40
|
-
|
|
41
|
-
for arg in "$@"; do
|
|
42
|
-
case "$arg" in
|
|
43
|
-
--non-interactive) NON_INTERACTIVE=true ;;
|
|
44
|
-
--skip-bun) SKIP_BUN=true ;;
|
|
45
|
-
--skip-start) SKIP_START=true ;;
|
|
46
|
-
esac
|
|
47
|
-
done
|
|
48
|
-
|
|
49
|
-
# ============================================================
|
|
50
|
-
# Banner
|
|
51
|
-
# ============================================================
|
|
52
|
-
echo ""
|
|
53
|
-
echo -e "${BOLD} ┌──────────────────────────────────────────┐${NC}"
|
|
54
|
-
echo -e "${BOLD} │ ${CYAN}imtoagent${NC} — IM ↔ Agent Unified Gateway${BOLD} │${NC}"
|
|
55
|
-
echo -e "${BOLD} └──────────────────────────────────────────┘${NC}"
|
|
56
|
-
echo ""
|
|
57
|
-
|
|
58
|
-
# ============================================================
|
|
59
|
-
# 1. OS Detection
|
|
60
|
-
# ============================================================
|
|
61
|
-
step "1. Detecting environment"
|
|
62
|
-
|
|
63
|
-
OS=""
|
|
64
|
-
ARCH=""
|
|
65
|
-
|
|
66
|
-
case "$(uname -s)" in
|
|
67
|
-
Darwin*) OS="macos" ;;
|
|
68
|
-
Linux*) OS="linux" ;;
|
|
69
|
-
*) error "Unsupported OS: $(uname -s)"; exit 1 ;;
|
|
70
|
-
esac
|
|
71
|
-
|
|
72
|
-
case "$(uname -m)" in
|
|
73
|
-
arm64|aarch64) ARCH="aarch64" ;;
|
|
74
|
-
x86_64|amd64) ARCH="x64" ;;
|
|
75
|
-
*) ARCH="unknown" ;;
|
|
76
|
-
esac
|
|
77
|
-
|
|
78
|
-
done_ok "OS: $OS ($(uname -m))"
|
|
79
|
-
|
|
80
|
-
# ============================================================
|
|
81
|
-
# 2. Bun Detection & Installation
|
|
82
|
-
# ============================================================
|
|
83
|
-
if [ "$SKIP_BUN" = true ]; then
|
|
84
|
-
info "Skipping bun check (--skip-bun)"
|
|
85
|
-
else
|
|
86
|
-
step "2. Checking bun"
|
|
87
|
-
|
|
88
|
-
BUN_BIN=""
|
|
89
|
-
|
|
90
|
-
# Check environment variable first
|
|
91
|
-
if [ -n "${BUN_BIN:-}" ] && [ -x "$BUN_BIN" ]; then
|
|
92
|
-
BUN_BIN="$BUN_BIN"
|
|
93
|
-
# Check common paths
|
|
94
|
-
elif [ -x "$HOME/.bun/bin/bun" ]; then
|
|
95
|
-
BUN_BIN="$HOME/.bun/bin/bun"
|
|
96
|
-
elif [ -x "/opt/homebrew/bin/bun" ]; then
|
|
97
|
-
BUN_BIN="/opt/homebrew/bin/bun"
|
|
98
|
-
elif [ -x "/usr/local/bin/bun" ]; then
|
|
99
|
-
BUN_BIN="/usr/local/bin/bun"
|
|
100
|
-
elif command -v bun &>/dev/null; then
|
|
101
|
-
BUN_BIN="$(command -v bun)"
|
|
102
|
-
fi
|
|
103
|
-
|
|
104
|
-
if [ -n "$BUN_BIN" ]; then
|
|
105
|
-
BUN_VER=$("$BUN_BIN" --version 2>/dev/null || echo "unknown")
|
|
106
|
-
done_ok "bun found: $BUN_BIN (v${BUN_VER})"
|
|
107
|
-
else
|
|
108
|
-
warn "bun not found — installing..."
|
|
109
|
-
echo ""
|
|
110
|
-
|
|
111
|
-
if [ "$OS" = "macos" ] || [ "$OS" = "linux" ]; then
|
|
112
|
-
echo " Downloading and installing bun..."
|
|
113
|
-
curl -fsSL https://bun.sh/install | bash 2>&1 | tail -5
|
|
114
|
-
|
|
115
|
-
# Add bun to PATH for this session
|
|
116
|
-
if [ -f "$HOME/.bun/bin/bun" ]; then
|
|
117
|
-
BUN_BIN="$HOME/.bun/bin/bun"
|
|
118
|
-
export PATH="$HOME/.bun/bin:$PATH"
|
|
119
|
-
elif command -v bun &>/dev/null; then
|
|
120
|
-
BUN_BIN="$(command -v bun)"
|
|
121
|
-
fi
|
|
122
|
-
|
|
123
|
-
if [ -n "$BUN_BIN" ] && [ -x "$BUN_BIN" ]; then
|
|
124
|
-
BUN_VER=$("$BUN_BIN" --version 2>/dev/null || echo "unknown")
|
|
125
|
-
done_ok "bun installed: v${BUN_VER}"
|
|
126
|
-
else
|
|
127
|
-
error "bun installation failed. Please install manually:"
|
|
128
|
-
echo " curl -fsSL https://bun.sh/install | bash"
|
|
129
|
-
echo " Then re-run this script."
|
|
130
|
-
exit 1
|
|
131
|
-
fi
|
|
132
|
-
else
|
|
133
|
-
error "Unsupported platform for automatic bun install."
|
|
134
|
-
echo " Install bun manually: https://bun.sh"
|
|
135
|
-
exit 1
|
|
136
|
-
fi
|
|
137
|
-
fi
|
|
138
|
-
fi
|
|
139
|
-
|
|
140
|
-
# ============================================================
|
|
141
|
-
# 3. Node.js Check (npm requires node)
|
|
142
|
-
# ============================================================
|
|
143
|
-
step "3. Checking node/npm"
|
|
144
|
-
|
|
145
|
-
if command -v node &>/dev/null; then
|
|
146
|
-
NODE_VER=$(node --version)
|
|
147
|
-
done_ok "node: ${NODE_VER}"
|
|
148
|
-
else
|
|
149
|
-
warn "node not found — npm install will fail"
|
|
150
|
-
echo ""
|
|
151
|
-
echo " You need Node.js installed. Options:"
|
|
152
|
-
echo " macOS: brew install node"
|
|
153
|
-
echo " Linux: apt install nodejs npm (or use nvm)"
|
|
154
|
-
echo ""
|
|
155
|
-
echo " Or install via nvm:"
|
|
156
|
-
echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash"
|
|
157
|
-
echo " source ~/.bashrc # or ~/.zshrc"
|
|
158
|
-
echo " nvm install 20"
|
|
159
|
-
echo ""
|
|
160
|
-
if [ "$NON_INTERACTIVE" = true ]; then
|
|
161
|
-
error "Cannot continue in non-interactive mode without node."
|
|
162
|
-
exit 1
|
|
163
|
-
fi
|
|
164
|
-
read -rp " Continue anyway? [y/N] " CONTINUE
|
|
165
|
-
case "$CONTINUE" in
|
|
166
|
-
[yY]*) info "Continuing..." ;;
|
|
167
|
-
*) error "Aborted."; exit 1 ;;
|
|
168
|
-
esac
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
if command -v npm &>/dev/null; then
|
|
172
|
-
NPM_VER=$(npm --version)
|
|
173
|
-
done_ok "npm: v${NPM_VER}"
|
|
174
|
-
else
|
|
175
|
-
error "npm not found — required for installation"
|
|
176
|
-
exit 1
|
|
177
|
-
fi
|
|
178
|
-
|
|
179
|
-
# ============================================================
|
|
180
|
-
# 4. Install / Upgrade imtoagent
|
|
181
|
-
# ============================================================
|
|
182
|
-
step "4. Installing imtoagent"
|
|
183
|
-
|
|
184
|
-
# Check if already installed
|
|
185
|
-
EXISTING_VER=""
|
|
186
|
-
if command -v imtoagent &>/dev/null; then
|
|
187
|
-
EXISTING_VER=$(imtoagent --version 2>/dev/null || echo "unknown")
|
|
188
|
-
warn "Already installed: v${EXISTING_VER}"
|
|
189
|
-
echo ""
|
|
190
|
-
if [ "$NON_INTERACTIVE" = true ]; then
|
|
191
|
-
info "Non-interactive mode — upgrading"
|
|
192
|
-
else
|
|
193
|
-
read -rp " Upgrade to latest? [Y/n] " UPGRADE
|
|
194
|
-
case "$UPGRADE" in
|
|
195
|
-
[nN]*) info "Skipping upgrade."; exit 0 ;;
|
|
196
|
-
esac
|
|
197
|
-
fi
|
|
198
|
-
fi
|
|
199
|
-
|
|
200
|
-
echo " Running: npm install -g imtoagent"
|
|
201
|
-
echo ""
|
|
202
|
-
npm install -g imtoagent 2>&1 | tail -10
|
|
203
|
-
|
|
204
|
-
INSTALLED_VER=$(imtoagent --version 2>/dev/null || echo "unknown")
|
|
205
|
-
done_ok "imtoagent v${INSTALLED_VER} installed globally"
|
|
206
|
-
|
|
207
|
-
# ============================================================
|
|
208
|
-
# 5. Configuration Check & Setup
|
|
209
|
-
# ============================================================
|
|
210
|
-
step "5. Checking configuration"
|
|
211
|
-
|
|
212
|
-
CONFIG_DIR="$HOME/.imtoagent"
|
|
213
|
-
CONFIG_FILE="$CONFIG_DIR/config.json"
|
|
214
|
-
|
|
215
|
-
NEED_SETUP=true
|
|
216
|
-
|
|
217
|
-
if [ -f "$CONFIG_FILE" ]; then
|
|
218
|
-
done_ok "Config found: $CONFIG_FILE"
|
|
219
|
-
|
|
220
|
-
# Quick check if config has any bots (grep for botType field)
|
|
221
|
-
if grep -q '"botType"' "$CONFIG_FILE" 2>/dev/null; then
|
|
222
|
-
BOT_COUNT=$(grep -c '"botType"' "$CONFIG_FILE" 2>/dev/null || echo "0")
|
|
223
|
-
done_ok "$BOT_COUNT bot(s) configured — skipping setup wizard"
|
|
224
|
-
NEED_SETUP=false
|
|
225
|
-
else
|
|
226
|
-
warn "No bots configured in config"
|
|
227
|
-
fi
|
|
228
|
-
else
|
|
229
|
-
warn "No configuration found"
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
if [ "$NEED_SETUP" = true ]; then
|
|
233
|
-
if [ "$NON_INTERACTIVE" = true ]; then
|
|
234
|
-
info "Non-interactive mode — skipping setup wizard"
|
|
235
|
-
echo ""
|
|
236
|
-
echo " Run 'imtoagent setup' manually to configure."
|
|
237
|
-
else
|
|
238
|
-
echo ""
|
|
239
|
-
echo " Starting interactive setup wizard..."
|
|
240
|
-
echo " You'll need:"
|
|
241
|
-
echo " • IM platform credentials (Feishu App ID/Secret, Telegram Token, etc.)"
|
|
242
|
-
echo " • Agent backend (Claude Code / Codex / OpenCode)"
|
|
243
|
-
echo " • Model provider API keys"
|
|
244
|
-
echo ""
|
|
245
|
-
read -rp " Start setup now? [Y/n] " START_SETUP
|
|
246
|
-
case "$START_SETUP" in
|
|
247
|
-
[nN]*)
|
|
248
|
-
info "Skipping setup. Run 'imtoagent setup' later to configure."
|
|
249
|
-
;;
|
|
250
|
-
*)
|
|
251
|
-
echo ""
|
|
252
|
-
imtoagent setup
|
|
253
|
-
;;
|
|
254
|
-
esac
|
|
255
|
-
fi
|
|
256
|
-
fi
|
|
257
|
-
|
|
258
|
-
# ============================================================
|
|
259
|
-
# 6. Start Gateway (optional)
|
|
260
|
-
# ============================================================
|
|
261
|
-
if [ "$SKIP_START" = true ]; then
|
|
262
|
-
info "Skipping gateway start (--skip-start)"
|
|
263
|
-
else
|
|
264
|
-
step "6. Starting gateway"
|
|
265
|
-
|
|
266
|
-
# Check if already running
|
|
267
|
-
if imtoagent status 2>/dev/null | grep -q "running"; then
|
|
268
|
-
warn "Gateway already running"
|
|
269
|
-
imtoagent status 2>/dev/null || true
|
|
270
|
-
else
|
|
271
|
-
if [ "$NON_INTERACTIVE" = true ]; then
|
|
272
|
-
info "Non-interactive mode — starting gateway in background"
|
|
273
|
-
imtoagent start
|
|
274
|
-
sleep 3
|
|
275
|
-
imtoagent status 2>/dev/null || true
|
|
276
|
-
else
|
|
277
|
-
read -rp " Start gateway now? [Y/n] " START_GATEWAY
|
|
278
|
-
case "$START_GATEWAY" in
|
|
279
|
-
[nN]*)
|
|
280
|
-
info "Not starting. Run 'imtoagent start' when ready."
|
|
281
|
-
;;
|
|
282
|
-
*)
|
|
283
|
-
imtoagent start
|
|
284
|
-
sleep 3
|
|
285
|
-
imtoagent status 2>/dev/null || true
|
|
286
|
-
;;
|
|
287
|
-
esac
|
|
288
|
-
fi
|
|
289
|
-
fi
|
|
290
|
-
fi
|
|
291
|
-
|
|
292
|
-
# ============================================================
|
|
293
|
-
# Summary
|
|
294
|
-
# ============================================================
|
|
295
|
-
echo ""
|
|
296
|
-
echo -e "${BOLD} ──────────────────────────────────────────${NC}"
|
|
297
|
-
echo -e "${GREEN}${BOLD} Installation Complete!${NC}"
|
|
298
|
-
echo -e "${BOLD} ──────────────────────────────────────────${NC}"
|
|
299
|
-
echo ""
|
|
300
|
-
echo " Version: v${INSTALLED_VER}"
|
|
301
|
-
echo " Config: ${CONFIG_FILE}"
|
|
302
|
-
echo " Logs: ${CONFIG_DIR}/logs/"
|
|
303
|
-
echo ""
|
|
304
|
-
echo " Quick commands:"
|
|
305
|
-
echo " imtoagent status # Check gateway status"
|
|
306
|
-
echo " imtoagent stop # Stop the gateway"
|
|
307
|
-
echo " imtoagent setup # Configure bots"
|
|
308
|
-
echo " imtoagent restore # Hot reload"
|
|
309
|
-
echo ""
|
|
310
|
-
echo " Send ${BOLD}/help${NC} to your Bot in IM to see available commands."
|
|
311
|
-
echo ""
|
|
312
|
-
echo " Docs: https://github.com/imtoagent/imtoagent"
|
|
313
|
-
echo ""
|
package/modules/agent/claude.ts
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// Claude Agent 模块
|
|
2
|
-
// 对接 Claude Agent SDK,通过 :18899 Proxy 调用 Provider
|
|
3
|
-
|
|
4
|
-
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
5
|
-
import type { AgentContext } from '../types';
|
|
6
|
-
import { parseToBlocks, type UnifiedBlock } from '../capabilities';
|
|
7
|
-
import { buildSystemPrompt, resolveCapabilities } from '../prompt-builder';
|
|
8
|
-
import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
9
|
-
|
|
10
|
-
// ================================================================
|
|
11
|
-
// 工具函数
|
|
12
|
-
// ================================================================
|
|
13
|
-
function resolveAlias(modelSpec: string): string {
|
|
14
|
-
const i = modelSpec.indexOf('/');
|
|
15
|
-
return i >= 0 ? modelSpec.slice(i + 1) : modelSpec;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function extractText(msg: SDKMessage): string | null {
|
|
19
|
-
if (msg.type !== 'assistant') return null;
|
|
20
|
-
const content = (msg as any).message?.content;
|
|
21
|
-
if (!Array.isArray(content)) return null;
|
|
22
|
-
return content.filter((b: any) => b.type === 'text').map((b: any) => b.text).join('') || null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function extractToolInfo(msg: SDKMessage): { name: string; summary: string } | null {
|
|
26
|
-
if (msg.type !== 'assistant') return null;
|
|
27
|
-
const content = (msg as any).message?.content;
|
|
28
|
-
if (!Array.isArray(content)) return null;
|
|
29
|
-
const tool = content.find((b: any) => b.type === 'tool_use');
|
|
30
|
-
if (!tool?.name) return null;
|
|
31
|
-
const input = tool.input || {};
|
|
32
|
-
let summary = '';
|
|
33
|
-
if (['Read','Edit','Write'].includes(tool.name) && input.file_path) {
|
|
34
|
-
const p = String(input.file_path);
|
|
35
|
-
summary = p.includes('/') ? p.split('/').pop()! : p;
|
|
36
|
-
} else if (tool.name === 'Bash' && input.command) {
|
|
37
|
-
summary = String(input.command).trim().slice(0, 60);
|
|
38
|
-
}
|
|
39
|
-
return { name: tool.name, summary };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ================================================================
|
|
43
|
-
// Claude 模块类
|
|
44
|
-
// ================================================================
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 注意:此模块目前依赖宿主 Bot 实例的方法(reply/sendProgress/addToolLog 等)。
|
|
48
|
-
* Phase 2 第一步先原样提取,后续逐步解耦为干净接口。
|
|
49
|
-
*/
|
|
50
|
-
export class ClaudeAgentModule {
|
|
51
|
-
private ctx: AgentContext;
|
|
52
|
-
|
|
53
|
-
constructor(ctx: AgentContext) {
|
|
54
|
-
this.ctx = ctx;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async handleMessage(chatId: string, text: string, session: any) {
|
|
58
|
-
session.generator.push({
|
|
59
|
-
type: 'user', message: { role: 'user', content: [{ type: 'text', text }] },
|
|
60
|
-
});
|
|
61
|
-
if (!session.running) this._startLoop(chatId);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
private async _startLoop(chatId: string) {
|
|
65
|
-
const ctx = this.ctx;
|
|
66
|
-
const session = ctx.sessions.get(chatId);
|
|
67
|
-
if (!session || session.running) return;
|
|
68
|
-
session.running = true;
|
|
69
|
-
console.log(`[${ctx.name}] Claude loop started chat=${chatId.slice(-8)}`);
|
|
70
|
-
|
|
71
|
-
try {
|
|
72
|
-
const modelSpec = ctx.activeModel;
|
|
73
|
-
const modelName = modelSpec.slice(modelSpec.indexOf('/') + 1);
|
|
74
|
-
const aliases = ctx.modelAliases;
|
|
75
|
-
const customEnv: Record<string, string> = {
|
|
76
|
-
...process.env,
|
|
77
|
-
ANTHROPIC_BASE_URL: 'http://localhost:18899',
|
|
78
|
-
ANTHROPIC_API_KEY: '', ANTHROPIC_AUTH_TOKEN: '', ANTHROPIC_MODEL: '',
|
|
79
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: resolveAlias(aliases.sonnet),
|
|
80
|
-
ANTHROPIC_DEFAULT_OPUS_MODEL: resolveAlias(aliases.opus),
|
|
81
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: resolveAlias(aliases.haiku),
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const queryOptions: any = {
|
|
85
|
-
cwd: session.cwd || ctx.defaultCwd,
|
|
86
|
-
maxTurns: 50, model: modelName,
|
|
87
|
-
permissionMode: session.permissionMode || 'bypassPermissions',
|
|
88
|
-
persistSession: true,
|
|
89
|
-
};
|
|
90
|
-
if (session.sdkSessionId) {
|
|
91
|
-
queryOptions.resume = session.sdkSessionId;
|
|
92
|
-
} else {
|
|
93
|
-
queryOptions.sessionId = crypto.randomUUID();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const botName = ctx.name;
|
|
97
|
-
const systemPrompt = buildSystemPrompt({
|
|
98
|
-
imModule: ctx.imModule || null,
|
|
99
|
-
botName,
|
|
100
|
-
});
|
|
101
|
-
console.log(`[Claude] 📝 system prompt built (${systemPrompt.length} chars, bot=${botName})`);
|
|
102
|
-
queryOptions.systemPrompt = systemPrompt;
|
|
103
|
-
|
|
104
|
-
const q = query({
|
|
105
|
-
prompt: session.generator.generate(),
|
|
106
|
-
options: queryOptions, env: customEnv,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
let fullResponse = '', toolCalls = 0;
|
|
110
|
-
let callInput = 0, callOutput = 0, callCost = 0, callDur = 0;
|
|
111
|
-
|
|
112
|
-
for await (const msg of q) {
|
|
113
|
-
const msgAny = msg as any;
|
|
114
|
-
if (msgAny.session_id && !session.sdkSessionId) {
|
|
115
|
-
session.sdkSessionId = msgAny.session_id;
|
|
116
|
-
ctx.persistSession(chatId, session);
|
|
117
|
-
}
|
|
118
|
-
const text = extractText(msg);
|
|
119
|
-
if (text) fullResponse += text;
|
|
120
|
-
const toolInfo = extractToolInfo(msg);
|
|
121
|
-
if (toolInfo) { toolCalls++; ctx.addToolLog(chatId, toolInfo); }
|
|
122
|
-
|
|
123
|
-
if (msg.type === 'result') {
|
|
124
|
-
const result = msg as any;
|
|
125
|
-
callInput = result.usage?.input_tokens || 0;
|
|
126
|
-
callOutput = result.usage?.output_tokens || 0;
|
|
127
|
-
callCost = result.total_cost_usd || 0;
|
|
128
|
-
callDur = result.duration_ms || 0;
|
|
129
|
-
|
|
130
|
-
ctx.accumulateStats(session, {
|
|
131
|
-
inputTokens: callInput, outputTokens: callOutput,
|
|
132
|
-
costUSD: callCost, durationMs: callDur,
|
|
133
|
-
numTurns: result.num_turns || 0,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
if (result.subtype === 'error' || result.subtype === 'cancelled') {
|
|
137
|
-
await ctx.reply(chatId, `❌ ${result.error || result.result || 'Unknown error'}`);
|
|
138
|
-
} else if (fullResponse) {
|
|
139
|
-
await ctx.sendFormattedReply(chatId, fullResponse);
|
|
140
|
-
} else {
|
|
141
|
-
await ctx.reply(chatId, `✅ Completed (${toolCalls} steps)`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
ctx.flushToolLog(chatId);
|
|
145
|
-
const costStr = callCost > 0 ? `Cost $${callCost.toFixed(4)}\n` : '';
|
|
146
|
-
await ctx.sendProgress(chatId,
|
|
147
|
-
`✅ Completed (${toolCalls} steps)\nInput ${callInput.toLocaleString()} Token\nOutput ${callOutput.toLocaleString()} Token\n${costStr}Duration ${(callDur/1000).toFixed(1)}s`);
|
|
148
|
-
fullResponse = ''; toolCalls = 0;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
} catch (e: any) {
|
|
152
|
-
console.error(`[${ctx.name}] Claude error: ${e.message}`);
|
|
153
|
-
await ctx.reply(chatId, `❌ ${e.message}`);
|
|
154
|
-
} finally {
|
|
155
|
-
session.running = false;
|
|
156
|
-
session.generator.close();
|
|
157
|
-
ctx.persistSession(chatId, session);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
package/modules/agent/codex.ts
DELETED
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
// Codex Agent 模块
|
|
2
|
-
// 对接 Codex CLI,通过 :18899 Proxy 调用 Provider
|
|
3
|
-
|
|
4
|
-
import { getProxyUsage, resetProxyUsage } from '../proxy/codex-proxy';
|
|
5
|
-
import { calculateCost } from '../proxy/anthropic-proxy';
|
|
6
|
-
import type { AgentContext } from '../types';
|
|
7
|
-
import { getAppServerManager, type AgentEvent } from './codex-exec-server';
|
|
8
|
-
// ================================================================
|
|
9
|
-
// 类型
|
|
10
|
-
// ================================================================
|
|
11
|
-
interface CodexJsonEvent {
|
|
12
|
-
type: string;
|
|
13
|
-
thread_id?: string;
|
|
14
|
-
item?: { type: string; text?: string; name?: string; arguments?: string; output?: string };
|
|
15
|
-
text?: string;
|
|
16
|
-
delta?: string;
|
|
17
|
-
message?: { content?: { type: string; text?: string }[] };
|
|
18
|
-
error?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const TOOL_NAMES: Record<string, string> = {
|
|
22
|
-
Bash: 'Execute command', Read: 'Read file', Edit: 'Edit file', Write: 'Write file',
|
|
23
|
-
Glob: 'Search files', Grep: 'Search content', WebSearch: 'Web search', WebFetch: 'Fetch webpage',
|
|
24
|
-
NotebookEdit: 'Edit Notebook',
|
|
25
|
-
// Codex tool names
|
|
26
|
-
command_execution: 'Execute command', exec_command: 'Execute command', write_stdin: 'Write to stdin', update_plan: 'Update plan',
|
|
27
|
-
request_user_input: 'Request input', apply_patch: 'Apply patch', view_image: 'View image',
|
|
28
|
-
spawn_agent: 'Spawn agent', send_input: 'Send input', resume_agent: 'Resume agent',
|
|
29
|
-
wait_agent: 'Wait agent', close_agent: 'Close agent',
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// ================================================================
|
|
33
|
-
// Codex CLI 调用
|
|
34
|
-
// ================================================================
|
|
35
|
-
function processCodexStream(stdout: string, onTool?: (name: string, args: Record<string, any>) => void): { threadId: string; response: string } {
|
|
36
|
-
let threadId = '', response = '';
|
|
37
|
-
for (const line of stdout.split('\n')) {
|
|
38
|
-
if (!line.trim()) continue;
|
|
39
|
-
try {
|
|
40
|
-
const evt: CodexJsonEvent = JSON.parse(line);
|
|
41
|
-
if (evt.type === 'thread.started' && evt.thread_id) {
|
|
42
|
-
threadId = evt.thread_id;
|
|
43
|
-
} else if (evt.type === 'item.completed') {
|
|
44
|
-
if (evt.item?.type === 'agent_message') {
|
|
45
|
-
response = (response ? response + '\n' : '') + (evt.item.text || '');
|
|
46
|
-
} else if (TOOL_NAMES[evt.item?.type || ''] && onTool) {
|
|
47
|
-
onTool(evt.item.type || 'unknown', { command: (evt.item as any).command || '' });
|
|
48
|
-
}
|
|
49
|
-
} else if (evt.type === 'error' || evt.type === 'thread.error') {
|
|
50
|
-
console.error(`[Codex] event error: ${(evt as any).message || (evt as any).error || JSON.stringify(evt)}`);
|
|
51
|
-
}
|
|
52
|
-
} catch {}
|
|
53
|
-
}
|
|
54
|
-
return { threadId, response };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function spawnCodexExec(
|
|
58
|
-
cwd: string, prompt: string,
|
|
59
|
-
onTool?: (name: string, args: Record<string, any>) => void
|
|
60
|
-
): Promise<{ threadId: string; response: string }> {
|
|
61
|
-
console.error(`[Codex] spawnExec cwd=${cwd} prompt_len=${prompt.length}`);
|
|
62
|
-
const child = Bun.spawn(['codex', 'exec', '-p', 'imtoagent', '-s', 'danger-full-access',
|
|
63
|
-
'--skip-git-repo-check', '--json', prompt], {
|
|
64
|
-
cwd, stdout: 'pipe', stderr: 'pipe',
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// Safe read stdout/stderr: catch subprocess kill exceptions, ensure reject carries Error object
|
|
68
|
-
let stdout = '', stderr = '';
|
|
69
|
-
try {
|
|
70
|
-
[stdout, stderr] = await Promise.all([
|
|
71
|
-
new Response(child.stdout).text().catch((e: any) => { throw new Error(`stdout read failed: ${e?.message || e}`); }),
|
|
72
|
-
new Response(child.stderr).text().catch((e: any) => { throw new Error(`stderr read failed: ${e?.message || e}`); }),
|
|
73
|
-
]);
|
|
74
|
-
} catch (ioErr: any) {
|
|
75
|
-
// Subprocess may have been killed, try to get exit code
|
|
76
|
-
try { child.kill('SIGKILL'); } catch {}
|
|
77
|
-
throw new Error(`codex exec I/O error: ${ioErr.message}`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const code = await child.exited.catch(() => -1);
|
|
81
|
-
console.error(`[Codex] exec exit=${code} stdout_len=${stdout.length} stderr_len=${stderr.length}`);
|
|
82
|
-
const { threadId, response } = processCodexStream(stdout, onTool);
|
|
83
|
-
if (code !== 0 || !threadId) throw new Error(`codex exec exit ${code}: ${stderr.slice(0, 300)}`);
|
|
84
|
-
return { threadId, response };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
async function spawnCodexResume(
|
|
88
|
-
cwd: string, threadId: string, prompt: string,
|
|
89
|
-
onTool?: (name: string, args: Record<string, any>) => void
|
|
90
|
-
): Promise<{ response: string }> {
|
|
91
|
-
console.error(`[Codex] spawnResume cwd=${cwd} threadId=${threadId.slice(-8)} prompt_len=${prompt.length}`);
|
|
92
|
-
const child = Bun.spawn(['codex', 'exec', 'resume', threadId,
|
|
93
|
-
'--dangerously-bypass-approvals-and-sandbox', '-c', 'model_provider=imtoagent', '-c', 'model=gpt-5.5', '--json', '--skip-git-repo-check', prompt], {
|
|
94
|
-
cwd, stdout: 'pipe', stderr: 'pipe',
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
let stdout = '', stderr = '';
|
|
98
|
-
try {
|
|
99
|
-
[stdout, stderr] = await Promise.all([
|
|
100
|
-
new Response(child.stdout).text().catch((e: any) => { throw new Error(`stdout read failed: ${e?.message || e}`); }),
|
|
101
|
-
new Response(child.stderr).text().catch((e: any) => { throw new Error(`stderr read failed: ${e?.message || e}`); }),
|
|
102
|
-
]);
|
|
103
|
-
} catch (ioErr: any) {
|
|
104
|
-
try { child.kill('SIGKILL'); } catch {}
|
|
105
|
-
throw new Error(`codex exec resume I/O error: ${ioErr.message}`);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const code = await child.exited.catch(() => -1);
|
|
109
|
-
console.error(`[Codex] resume exit=${code} stdout_len=${stdout.length} stderr_len=${stderr.length}`);
|
|
110
|
-
if (code !== 0) throw new Error(`codex exec resume exit ${code}: ${stderr.slice(0, 300)}`);
|
|
111
|
-
return { response: processCodexStream(stdout, onTool).response };
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ================================================================
|
|
115
|
-
// App-Server 路径(优先使用——流式输出 + 长记忆 + 崩溃不丢上下文)
|
|
116
|
-
// ================================================================
|
|
117
|
-
async function runViaAppServer(
|
|
118
|
-
cwd: string, prompt: string, chatId: string, session: any,
|
|
119
|
-
onTool: (name: string, args: Record<string, any>) => void,
|
|
120
|
-
isFresh: boolean
|
|
121
|
-
): Promise<{ threadId: string; response: string; usage: { inputTokens: number; outputTokens: number } }> {
|
|
122
|
-
const manager = getAppServerManager();
|
|
123
|
-
const client = await manager.getClient(chatId);
|
|
124
|
-
|
|
125
|
-
// app-server 同进程内线程存活
|
|
126
|
-
// 但进程重启后旧 thread 过期,需要判断代际
|
|
127
|
-
const currentGen = manager.generation;
|
|
128
|
-
const threadExpired = session._appServerGen !== currentGen;
|
|
129
|
-
if (isFresh || !session.codexThreadId || threadExpired) {
|
|
130
|
-
session.codexThreadId = await client.startThread(cwd);
|
|
131
|
-
session._appServerGen = currentGen;
|
|
132
|
-
console.log(`[Codex] app-server new thread=${session.codexThreadId.slice(-8)}${threadExpired ? ' (process restarted)' : ''}`);
|
|
133
|
-
}
|
|
134
|
-
// 后续消息直接 turn/start(同线程延续上下文)
|
|
135
|
-
|
|
136
|
-
await client.sendPrompt(session.codexThreadId, prompt, cwd);
|
|
137
|
-
|
|
138
|
-
let response = '';
|
|
139
|
-
let totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
140
|
-
const startTime = Date.now();
|
|
141
|
-
const MAX_DURATION = 600_000; // 10 分钟总超时
|
|
142
|
-
|
|
143
|
-
for await (const event of client.receiveEvents()) {
|
|
144
|
-
// 超时保护
|
|
145
|
-
if (Date.now() - startTime > MAX_DURATION) {
|
|
146
|
-
console.error('[Codex] app-server task timed out (10min)');
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
switch (event.type) {
|
|
151
|
-
case 'text_delta':
|
|
152
|
-
response += event.textDelta || '';
|
|
153
|
-
break;
|
|
154
|
-
case 'tool_call':
|
|
155
|
-
onTool(event.toolName || 'unknown', event.toolInput || {});
|
|
156
|
-
break;
|
|
157
|
-
case 'turn_result':
|
|
158
|
-
// 累加多轮 token(非终端 turn_result 来自每轮的 turn/completed)
|
|
159
|
-
totalUsage.inputTokens += event.usage?.inputTokens || 0;
|
|
160
|
-
totalUsage.outputTokens += event.usage?.outputTokens || 0;
|
|
161
|
-
break;
|
|
162
|
-
case 'error':
|
|
163
|
-
throw new Error(`app-server error: ${event.error}`);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return { threadId: session.codexThreadId, response, usage: totalUsage };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ================================================================
|
|
171
|
-
// Codex 模块类
|
|
172
|
-
// ================================================================
|
|
173
|
-
export class CodexAgentModule {
|
|
174
|
-
private ctx: AgentContext;
|
|
175
|
-
|
|
176
|
-
constructor(ctx: AgentContext) {
|
|
177
|
-
this.ctx = ctx;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async handleMessage(chatId: string, text: string, session: any) {
|
|
181
|
-
const ctx = this.ctx;
|
|
182
|
-
const cwd = session.cwd || ctx.defaultCwd;
|
|
183
|
-
console.log(`[${ctx.name}] Codex chat=${chatId.slice(-8)} startFresh=${session.startFresh || false}`);
|
|
184
|
-
|
|
185
|
-
const onTool = (name: string, args: Record<string, any>) => {
|
|
186
|
-
const cmd = args.cmd || args.command || '';
|
|
187
|
-
const justification = args.justification || args.description || '';
|
|
188
|
-
const summary = cmd ? cmd.slice(0, 80) : justification.slice(0, 80);
|
|
189
|
-
ctx.addToolLog(chatId, { name, summary });
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
resetProxyUsage();
|
|
193
|
-
try {
|
|
194
|
-
let effectiveText = text;
|
|
195
|
-
if (session.codexMode === 'plan') {
|
|
196
|
-
effectiveText = `[Mode: Plan then execute] Please create a clear plan first, wait for my confirmation before executing. User request: ${text}`;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const isFresh = session.startFresh || !session.codexThreadId;
|
|
200
|
-
let response: string;
|
|
201
|
-
let execServerUsage: { inputTokens: number; outputTokens: number } | null = null;
|
|
202
|
-
|
|
203
|
-
session.startFresh = false;
|
|
204
|
-
await ctx.sendProgress(chatId, '💭 Thinking...');
|
|
205
|
-
|
|
206
|
-
// 优先尝试 app-server
|
|
207
|
-
console.error(`[${ctx.name}] DEBUG entering app-server branch, isFresh=${isFresh}, threadId=${session.codexThreadId?.slice(-8)}`);
|
|
208
|
-
let useExecFallback = false;
|
|
209
|
-
try {
|
|
210
|
-
const r = await runViaAppServer(cwd, effectiveText, chatId, session, onTool, isFresh);
|
|
211
|
-
response = r.response;
|
|
212
|
-
execServerUsage = r.usage;
|
|
213
|
-
} catch (appErr: any) {
|
|
214
|
-
const errMsg = appErr.message || '';
|
|
215
|
-
console.error(`[${ctx.name}] app-server failed: ${errMsg}`);
|
|
216
|
-
|
|
217
|
-
// thread not found → app-server 进程内线程丢了,尝试重新创建
|
|
218
|
-
if (errMsg.includes('thread not found') || errMsg.includes('Thread not found')) {
|
|
219
|
-
try {
|
|
220
|
-
session.codexThreadId = undefined;
|
|
221
|
-
const r2 = await runViaAppServer(cwd, effectiveText, chatId, session, onTool, true);
|
|
222
|
-
response = r2.response;
|
|
223
|
-
execServerUsage = r2.usage;
|
|
224
|
-
console.error(`[${ctx.name}] app-server thread rebuilt successfully`);
|
|
225
|
-
} catch {
|
|
226
|
-
useExecFallback = true;
|
|
227
|
-
}
|
|
228
|
-
} else {
|
|
229
|
-
useExecFallback = true;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (useExecFallback) {
|
|
234
|
-
getAppServerManager().removeClient(chatId);
|
|
235
|
-
if (isFresh || !session.codexThreadId) {
|
|
236
|
-
const r = await spawnCodexExec(cwd, effectiveText, onTool);
|
|
237
|
-
session.codexThreadId = r.threadId;
|
|
238
|
-
response = r.response;
|
|
239
|
-
console.log(`[${ctx.name}] Fresh session thread=${r.threadId.slice(-8)}`);
|
|
240
|
-
} else {
|
|
241
|
-
const r = await spawnCodexResume(cwd, session.codexThreadId, effectiveText, onTool);
|
|
242
|
-
response = r.response;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
ctx.flushToolLog(chatId);
|
|
246
|
-
|
|
247
|
-
// 优先使用 app-server 返回的 usage,否则从 proxy 获取
|
|
248
|
-
const usage = execServerUsage || getProxyUsage();
|
|
249
|
-
if (usage.inputTokens > 0 || usage.outputTokens > 0) {
|
|
250
|
-
const cost = calculateCost(ctx.activeModel, usage.inputTokens, usage.outputTokens);
|
|
251
|
-
ctx.accumulateStats(session, { ...usage, costUSD: cost });
|
|
252
|
-
await ctx.sendProgress(chatId,
|
|
253
|
-
`Input ${usage.inputTokens.toLocaleString()} Token\nOutput ${usage.outputTokens.toLocaleString()} Token\nCost $${cost.toFixed(4)}`);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (response) {
|
|
257
|
-
await ctx.sendFormattedReply(chatId, response);
|
|
258
|
-
}
|
|
259
|
-
else await ctx.reply(chatId, '✅ Completed');
|
|
260
|
-
ctx.persistSession(chatId, session);
|
|
261
|
-
} catch (e: any) {
|
|
262
|
-
console.error(`[${ctx.name}] Codex error: ${e.message}`);
|
|
263
|
-
session.codexThreadId = undefined;
|
|
264
|
-
try {
|
|
265
|
-
const r = await spawnCodexExec(cwd, text, onTool);
|
|
266
|
-
session.codexThreadId = r.threadId;
|
|
267
|
-
if (r.response) {
|
|
268
|
-
await ctx.sendFormattedReply(chatId, r.response);
|
|
269
|
-
}
|
|
270
|
-
} catch (e2: any) {
|
|
271
|
-
await ctx.reply(chatId, `❌ Processing failed: ${e2.message}`);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
// OpenCode Agent 模块
|
|
2
|
-
// 对接 opencode serve HTTP API,通过 :18899 Anthropic Proxy 调 Provider
|
|
3
|
-
//
|
|
4
|
-
// 设计:薄模块 — oc serve 管理 session/工具/provider,Gateway 只做 IM 翻译
|
|
5
|
-
|
|
6
|
-
import type { AgentContext, SessionData } from '../types';
|
|
7
|
-
import { parseToBlocks } from '../capabilities';
|
|
8
|
-
import { resolveCapabilities, buildSystemPrompt } from '../prompt-builder';
|
|
9
|
-
import { calculateCost } from '../proxy/anthropic-proxy';
|
|
10
|
-
import * as path from 'path';
|
|
11
|
-
import * as fs from 'fs';
|
|
12
|
-
import { getDataDir } from '../utils/paths';
|
|
13
|
-
|
|
14
|
-
// ================================================================
|
|
15
|
-
// 配置(从 config.json 读取)
|
|
16
|
-
// ================================================================
|
|
17
|
-
|
|
18
|
-
interface OpenCodeConfig {
|
|
19
|
-
serverUrl: string;
|
|
20
|
-
defaultModel: { providerID: string; modelID: string };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
let _ocConfig: OpenCodeConfig | null = null;
|
|
24
|
-
|
|
25
|
-
export function initOpenCodeConfig(cfg: OpenCodeConfig) {
|
|
26
|
-
_ocConfig = cfg;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getOcConfig(): OpenCodeConfig {
|
|
30
|
-
if (!_ocConfig) {
|
|
31
|
-
try {
|
|
32
|
-
|
|
33
|
-
const raw = JSON.parse(fs.readFileSync(path.join(getDataDir(), 'config.json'), 'utf-8'));
|
|
34
|
-
const oc = raw.opencode || {};
|
|
35
|
-
_ocConfig = {
|
|
36
|
-
serverUrl: oc.serverUrl || 'http://localhost:4096',
|
|
37
|
-
defaultModel: oc.defaultModel || { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
38
|
-
};
|
|
39
|
-
} catch {
|
|
40
|
-
_ocConfig = {
|
|
41
|
-
serverUrl: 'http://localhost:4096',
|
|
42
|
-
defaultModel: { providerID: 'anthropic', modelID: 'claude-sonnet-4-5' },
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return _ocConfig;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const OC_SERVER_URL = () => getOcConfig().serverUrl;
|
|
50
|
-
const OC_DEFAULT_MODEL = () => getOcConfig().defaultModel;
|
|
51
|
-
|
|
52
|
-
// ================================================================
|
|
53
|
-
// OpenCode Server HTTP 客户端
|
|
54
|
-
// ================================================================
|
|
55
|
-
|
|
56
|
-
interface OcMessagePart {
|
|
57
|
-
type: string;
|
|
58
|
-
text?: string;
|
|
59
|
-
tool_call?: { name: string; arguments: Record<string, any> };
|
|
60
|
-
tool_result?: { content: string };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
interface OcMessage {
|
|
64
|
-
info: { id: string; role: string; model?: string };
|
|
65
|
-
parts: OcMessagePart[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function ocCreateSession(title: string): Promise<string> {
|
|
69
|
-
const ac = new AbortController();
|
|
70
|
-
const timer = setTimeout(() => ac.abort(), 300_000);
|
|
71
|
-
const res = await fetch(`${OC_SERVER_URL()}/session`, {
|
|
72
|
-
method: 'POST',
|
|
73
|
-
headers: { 'Content-Type': 'application/json' },
|
|
74
|
-
body: JSON.stringify({ title }),
|
|
75
|
-
signal: ac.signal,
|
|
76
|
-
}).finally(() => clearTimeout(timer));
|
|
77
|
-
if (!res.ok) throw new Error(`oc create session: ${res.status} ${await res.text()}`);
|
|
78
|
-
const data = await res.json();
|
|
79
|
-
return data.id;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function ocSendPrompt(
|
|
83
|
-
sessionId: string,
|
|
84
|
-
initialText: string,
|
|
85
|
-
system?: string,
|
|
86
|
-
onTool?: (name: string, args: Record<string, any>) => void
|
|
87
|
-
): Promise<{ response: string }> {
|
|
88
|
-
const MAX_TURNS = 50;
|
|
89
|
-
const TURN_TIMEOUT = 300_000;
|
|
90
|
-
const startTime = Date.now();
|
|
91
|
-
const MAX_DURATION = 600_000; // 10 分钟总超时
|
|
92
|
-
|
|
93
|
-
let promptText = initialText;
|
|
94
|
-
let accumulatedResponse = '';
|
|
95
|
-
let turn = 0;
|
|
96
|
-
|
|
97
|
-
while (turn < MAX_TURNS) {
|
|
98
|
-
if (Date.now() - startTime > MAX_DURATION) {
|
|
99
|
-
console.error('[OpenCode] Task timed out (10min)');
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
turn++;
|
|
103
|
-
|
|
104
|
-
const body: any = {
|
|
105
|
-
model: OC_DEFAULT_MODEL(),
|
|
106
|
-
parts: [{ type: 'text', text: promptText }],
|
|
107
|
-
};
|
|
108
|
-
if (turn === 1 && system) body.system = system;
|
|
109
|
-
|
|
110
|
-
const ac = new AbortController();
|
|
111
|
-
const timer = setTimeout(() => ac.abort(), TURN_TIMEOUT);
|
|
112
|
-
const res = await fetch(`${OC_SERVER_URL()}/session/${sessionId}/message`, {
|
|
113
|
-
method: 'POST',
|
|
114
|
-
headers: { 'Content-Type': 'application/json' },
|
|
115
|
-
body: JSON.stringify(body),
|
|
116
|
-
signal: ac.signal,
|
|
117
|
-
}).finally(() => clearTimeout(timer));
|
|
118
|
-
if (!res.ok) throw new Error(`oc send prompt: ${res.status} ${await res.text()}`);
|
|
119
|
-
|
|
120
|
-
const data: OcMessage = await res.json();
|
|
121
|
-
let hasToolCall = false;
|
|
122
|
-
|
|
123
|
-
for (const part of data.parts || []) {
|
|
124
|
-
if (part.type === 'text' && part.text) {
|
|
125
|
-
accumulatedResponse += (accumulatedResponse ? '\n' : '') + part.text;
|
|
126
|
-
} else if (part.type === 'tool_call' && part.tool_call) {
|
|
127
|
-
hasToolCall = true;
|
|
128
|
-
if (onTool) onTool(part.tool_call.name, part.tool_call.arguments);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// 纯文本回复或没有 tool_call → 任务完成
|
|
133
|
-
if (!hasToolCall) break;
|
|
134
|
-
|
|
135
|
-
// 有 tool_call,继续推进(空 prompt)
|
|
136
|
-
promptText = 'Continue';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return { response: accumulatedResponse };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
async function ocDeleteSession(sessionId: string): Promise<void> {
|
|
143
|
-
await fetch(`${OC_SERVER_URL()}/session/${sessionId}`, { method: 'DELETE' }).catch(() => {});
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async function ocHealthCheck(): Promise<boolean> {
|
|
147
|
-
try {
|
|
148
|
-
const res = await fetch(`${OC_SERVER_URL()}/global/health`);
|
|
149
|
-
return res.ok;
|
|
150
|
-
} catch {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// ================================================================
|
|
156
|
-
// Agent 模块类
|
|
157
|
-
// ================================================================
|
|
158
|
-
export class OpenCodeAgentModule {
|
|
159
|
-
private ctx: AgentContext;
|
|
160
|
-
|
|
161
|
-
constructor(ctx: AgentContext) {
|
|
162
|
-
this.ctx = ctx;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
async handleMessage(chatId: string, text: string, session: SessionData) {
|
|
166
|
-
const ctx = this.ctx;
|
|
167
|
-
console.log(`[${ctx.name}] OpenCode chat=${chatId.slice(-8)}`);
|
|
168
|
-
|
|
169
|
-
// 工具回调
|
|
170
|
-
const onTool = (name: string, args: Record<string, any>) => {
|
|
171
|
-
const summary = args.command || args.cmd || args.query || JSON.stringify(args).slice(0, 80);
|
|
172
|
-
ctx.addToolLog(chatId, { name, summary });
|
|
173
|
-
};
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
// ① Plan 模式处理
|
|
177
|
-
let effectiveText = text;
|
|
178
|
-
if (session.codexMode === 'plan') {
|
|
179
|
-
effectiveText = `[Mode: Plan then execute] Please create a clear plan first, wait for my confirmation before executing. User request: ${text}`;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ② 清理标记
|
|
183
|
-
const shouldClear = session.startFresh;
|
|
184
|
-
session.startFresh = false;
|
|
185
|
-
|
|
186
|
-
// ③ 获取或创建 OpenCode session
|
|
187
|
-
if (shouldClear || !session.ocSessionId) {
|
|
188
|
-
if (session.ocSessionId) {
|
|
189
|
-
await ocDeleteSession(session.ocSessionId);
|
|
190
|
-
console.log(`[${ctx.name}] Cleared oc session=${session.ocSessionId.slice(-8)}`);
|
|
191
|
-
}
|
|
192
|
-
session.ocSessionId = await ocCreateSession(chatId);
|
|
193
|
-
console.log(`[${ctx.name}] Created oc session=${session.ocSessionId.slice(-8)}`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// ④ 发送进度提示
|
|
197
|
-
await ctx.sendProgress(chatId, '💭 Thinking...');
|
|
198
|
-
|
|
199
|
-
// ④.⑤ 构建系统提示词
|
|
200
|
-
const systemPrompt = buildSystemPrompt({
|
|
201
|
-
imModule: ctx.imModule || null,
|
|
202
|
-
botName: ctx.name,
|
|
203
|
-
});
|
|
204
|
-
console.log(`[${ctx.name}] 📝 system prompt built (${systemPrompt.length} chars, bot=${ctx.name})`);
|
|
205
|
-
|
|
206
|
-
// ⑤ 发送 prompt(多轮循环)
|
|
207
|
-
const { response } = await ocSendPrompt(
|
|
208
|
-
session.ocSessionId,
|
|
209
|
-
effectiveText,
|
|
210
|
-
systemPrompt,
|
|
211
|
-
onTool,
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
// ⑥ 刷新工具日志
|
|
215
|
-
ctx.flushToolLog(chatId);
|
|
216
|
-
|
|
217
|
-
// ⑦ 输出
|
|
218
|
-
if (response) {
|
|
219
|
-
await ctx.sendFormattedReply(chatId, response);
|
|
220
|
-
} else {
|
|
221
|
-
await ctx.reply(chatId, '✅ Completed');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// ⑧ 统计
|
|
225
|
-
const { sharedState } = await import('../proxy/anthropic-proxy');
|
|
226
|
-
const lastUsage = (sharedState as any).lastCallUsage;
|
|
227
|
-
if (lastUsage && (lastUsage.inputTokens > 0 || lastUsage.outputTokens > 0)) {
|
|
228
|
-
const cost = calculateCost(ctx.activeModel, lastUsage.inputTokens, lastUsage.outputTokens);
|
|
229
|
-
ctx.accumulateStats(session, { ...lastUsage, costUSD: cost });
|
|
230
|
-
await ctx.sendProgress(chatId,
|
|
231
|
-
`Input ${lastUsage.inputTokens.toLocaleString()} Token\nOutput ${lastUsage.outputTokens.toLocaleString()} Token\nCost $${cost.toFixed(4)}`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ⑨ 持久化会话
|
|
235
|
-
ctx.persistSession(chatId, session);
|
|
236
|
-
|
|
237
|
-
} catch (err: any) {
|
|
238
|
-
console.error(`[${ctx.name}] OpenCode error: ${err.message}`);
|
|
239
|
-
await ctx.reply(chatId, `⚠️ OpenCode error: ${err.message}`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
/** 健康检查 */
|
|
244
|
-
async healthCheck(): Promise<boolean> {
|
|
245
|
-
return ocHealthCheck();
|
|
246
|
-
}
|
|
247
|
-
}
|
package/scripts/postinstall.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// ================================================================
|
|
3
|
-
// postinstall.ts — npm 安装后引导脚本
|
|
4
|
-
// ================================================================
|
|
5
|
-
// package.json 中 "scripts": { "postinstall": "bun run scripts/postinstall.ts" }
|
|
6
|
-
// 安装后自动运行,检测是否需要初始化配置
|
|
7
|
-
// 如果是全新安装且终端交互,自动引导进入 setup
|
|
8
|
-
// ================================================================
|
|
9
|
-
|
|
10
|
-
import * as fs from 'fs';
|
|
11
|
-
import * as path from 'path';
|
|
12
|
-
import { execSync } from 'child_process';
|
|
13
|
-
|
|
14
|
-
const HOME = process.env.HOME || process.env.USERPROFILE || '';
|
|
15
|
-
const DATA_DIR = path.join(HOME, '.imtoagent');
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const configExists = fs.existsSync(path.join(DATA_DIR, 'config.json'));
|
|
19
|
-
|
|
20
|
-
if (configExists) {
|
|
21
|
-
console.log(`
|
|
22
|
-
✅ imtoagent upgraded successfully!
|
|
23
|
-
Data directory: ${DATA_DIR}
|
|
24
|
-
Configuration file kept as-is, no need to reconfigure.
|
|
25
|
-
Run "imtoagent start" to start the gateway.
|
|
26
|
-
`);
|
|
27
|
-
} else {
|
|
28
|
-
console.log(`
|
|
29
|
-
╔══════════════════════════════════════════════════════════╗
|
|
30
|
-
║ ║
|
|
31
|
-
║ 🎉 imtoagent installed successfully! ║
|
|
32
|
-
║ ║
|
|
33
|
-
║ First-time use requires configuring IM credentials and a model provider ║
|
|
34
|
-
║ ║
|
|
35
|
-
╚══════════════════════════════════════════════════════════╝
|
|
36
|
-
`);
|
|
37
|
-
|
|
38
|
-
// 检测是否为交互式终端,是则自动引导进入 setup
|
|
39
|
-
if (process.stdin.isTTY) {
|
|
40
|
-
const readline = await import('readline');
|
|
41
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
42
|
-
const answer = await new Promise<string>(resolve => {
|
|
43
|
-
rl.question('Launch the configuration wizard now? [Y/n]: ', resolve);
|
|
44
|
-
});
|
|
45
|
-
rl.close();
|
|
46
|
-
|
|
47
|
-
const yes = answer.trim().toLowerCase();
|
|
48
|
-
if (yes === '' || yes === 'y' || yes === 'yes') {
|
|
49
|
-
console.log('\n🚀 Launching configuration wizard...\n');
|
|
50
|
-
// 调用 setup 向导
|
|
51
|
-
const pkgDir = path.resolve(import.meta.dirname, '..');
|
|
52
|
-
execSync('bun run bin/imtoagent setup', {
|
|
53
|
-
cwd: pkgDir,
|
|
54
|
-
stdio: 'inherit',
|
|
55
|
-
env: { ...process.env },
|
|
56
|
-
});
|
|
57
|
-
} else {
|
|
58
|
-
console.log('\nRun "imtoagent setup" later to configure.');
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
console.log(' Run "imtoagent setup" to start configuring');
|
|
62
|
-
console.log(' Then run "imtoagent start" to start the gateway\n');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
} catch (e: any) {
|
|
66
|
-
// Silently fail, do not affect installation
|
|
67
|
-
if (e.message && !e.message.includes('readline')) {
|
|
68
|
-
console.error(`[postinstall] Failed to display message: ${e.message}`);
|
|
69
|
-
}
|
|
70
|
-
}
|