@yvhitxcel/opencode-remote 0.17.0 → 0.18.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/LICENSE +21 -0
- package/README.md +70 -1
- package/dist/cli.js +120 -9
- package/dist/core/adapter.js +12 -0
- package/dist/core/agent-registry.js +77 -0
- package/dist/core/crypto.js +80 -0
- package/dist/core/git-push.js +38 -15
- package/dist/core/handler.js +293 -0
- package/dist/core/log.js +92 -0
- package/dist/core/lru.js +98 -0
- package/dist/core/qiniu.js +2 -2
- package/dist/core/retry.js +46 -0
- package/dist/core/router.js +18 -6
- package/dist/core/state.js +190 -0
- package/dist/core/stats.js +115 -0
- package/dist/feishu/adapter.js +0 -1
- package/dist/feishu/bot.js +4 -2
- package/dist/feishu/commands.js +2 -2
- package/dist/feishu/handler.js +8 -177
- package/dist/opencode/client.js +78 -56
- package/dist/patch_spawn.js +1 -0
- package/dist/plugins/agents/claude-code/index.js +59 -51
- package/dist/plugins/agents/codex/index.js +32 -6
- package/dist/plugins/agents/copilot/index.js +32 -6
- package/dist/plugins/agents/opencode/index.js +36 -14
- package/dist/telegram/adapter.js +19 -3
- package/dist/weixin/adapter.js +37 -15
- package/dist/weixin/api.js +38 -17
- package/dist/weixin/bot.js +58 -23
- package/dist/weixin/commands.js +134 -8
- package/dist/weixin/handler.js +12 -274
- package/dist/weixin/user-adapter-map.js +11 -0
- package/package.json +5 -3
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Persistent state manager — saves threadHistory + threadAgent to disk
|
|
2
|
+
// Bot restart preserves conversation context and agent routing
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync, renameSync } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { homedir } from 'os';
|
|
6
|
+
import { killAllAgentProcesses } from './agent-registry.js';
|
|
7
|
+
|
|
8
|
+
const STATE_DIR = join(homedir(), '.opencode-remote', 'state');
|
|
9
|
+
const STATE_FILE = join(STATE_DIR, 'state.json');
|
|
10
|
+
const STATE_TMP = join(STATE_DIR, 'state.json.tmp');
|
|
11
|
+
const STATE_BAK = join(STATE_DIR, 'state.json.bak');
|
|
12
|
+
|
|
13
|
+
const MAX_HISTORY_PER_THREAD = 20; // 同步 threadHistory 上限
|
|
14
|
+
const MAX_THREADS = 1000; // 总 thread 上限
|
|
15
|
+
const WRITE_DEBOUNCE_MS = 2000; // 写盘防抖
|
|
16
|
+
|
|
17
|
+
let writeTimer = null;
|
|
18
|
+
let dirty = false;
|
|
19
|
+
|
|
20
|
+
function ensureDir() {
|
|
21
|
+
if (!existsSync(STATE_DIR)) mkdirSync(STATE_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function loadState() {
|
|
25
|
+
ensureDir();
|
|
26
|
+
let raw = null;
|
|
27
|
+
if (existsSync(STATE_FILE)) raw = readFileSync(STATE_FILE, 'utf8');
|
|
28
|
+
else if (existsSync(STATE_BAK)) raw = readFileSync(STATE_BAK, 'utf8');
|
|
29
|
+
if (!raw) return { threadHistory: {}, threadAgent: {} };
|
|
30
|
+
try {
|
|
31
|
+
const s = JSON.parse(raw);
|
|
32
|
+
return {
|
|
33
|
+
threadHistory: s.threadHistory || {},
|
|
34
|
+
threadAgent: s.threadAgent || {},
|
|
35
|
+
};
|
|
36
|
+
} catch (e) {
|
|
37
|
+
console.error('[state] Corrupt state, starting fresh:', e.message);
|
|
38
|
+
return { threadHistory: {}, threadAgent: {} };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function doWrite() {
|
|
43
|
+
ensureDir();
|
|
44
|
+
const snapshot = {
|
|
45
|
+
threadHistory: Object.fromEntries(threadHistoryMap),
|
|
46
|
+
threadAgent: Object.fromEntries(threadAgentMap),
|
|
47
|
+
};
|
|
48
|
+
const json = JSON.stringify(snapshot);
|
|
49
|
+
try {
|
|
50
|
+
writeFileSync(STATE_TMP, json, 'utf8');
|
|
51
|
+
if (existsSync(STATE_FILE)) {
|
|
52
|
+
try { renameSync(STATE_FILE, STATE_BAK); } catch {}
|
|
53
|
+
}
|
|
54
|
+
renameSync(STATE_TMP, STATE_FILE);
|
|
55
|
+
} catch (e) {
|
|
56
|
+
console.error('[state] Write failed:', e.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function scheduleWrite() {
|
|
61
|
+
dirty = true;
|
|
62
|
+
if (writeTimer) return;
|
|
63
|
+
writeTimer = setTimeout(() => {
|
|
64
|
+
writeTimer = null;
|
|
65
|
+
if (dirty) {
|
|
66
|
+
dirty = false;
|
|
67
|
+
doWrite();
|
|
68
|
+
}
|
|
69
|
+
}, WRITE_DEBOUNCE_MS);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function flushWrite() {
|
|
73
|
+
if (writeTimer) {
|
|
74
|
+
clearTimeout(writeTimer);
|
|
75
|
+
writeTimer = null;
|
|
76
|
+
}
|
|
77
|
+
if (dirty) {
|
|
78
|
+
dirty = false;
|
|
79
|
+
doWrite();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// In-memory mirror of persistent state
|
|
84
|
+
const threadHistoryMap = new Map(); // threadId -> [{role, content}]
|
|
85
|
+
const threadAgentMap = new Map(); // threadId -> agentName
|
|
86
|
+
|
|
87
|
+
export const threadHistory = {
|
|
88
|
+
get(threadId) {
|
|
89
|
+
const h = threadHistoryMap.get(threadId);
|
|
90
|
+
return h ? h.slice() : [];
|
|
91
|
+
},
|
|
92
|
+
set(threadId, history) {
|
|
93
|
+
if (!Array.isArray(history) || history.length === 0) {
|
|
94
|
+
threadHistoryMap.delete(threadId);
|
|
95
|
+
} else {
|
|
96
|
+
const trimmed = history.slice(-MAX_HISTORY_PER_THREAD);
|
|
97
|
+
threadHistoryMap.set(threadId, trimmed);
|
|
98
|
+
// 上限淘汰: 删最旧的不活跃
|
|
99
|
+
if (threadHistoryMap.size > MAX_THREADS) {
|
|
100
|
+
const first = threadHistoryMap.keys().next().value;
|
|
101
|
+
threadHistoryMap.delete(first);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
scheduleWrite();
|
|
105
|
+
},
|
|
106
|
+
delete(threadId) {
|
|
107
|
+
threadHistoryMap.delete(threadId);
|
|
108
|
+
scheduleWrite();
|
|
109
|
+
},
|
|
110
|
+
has(threadId) {
|
|
111
|
+
return threadHistoryMap.has(threadId);
|
|
112
|
+
},
|
|
113
|
+
size() {
|
|
114
|
+
return threadHistoryMap.size;
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const threadAgent = {
|
|
119
|
+
get(threadId) { return threadAgentMap.get(threadId); },
|
|
120
|
+
set(threadId, agentName) { threadAgentMap.set(threadId, agentName); scheduleWrite(); },
|
|
121
|
+
delete(threadId) { threadAgentMap.delete(threadId); scheduleWrite(); },
|
|
122
|
+
size() { return threadAgentMap.size; },
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 启动时加载
|
|
126
|
+
export function initState() {
|
|
127
|
+
const s = loadState();
|
|
128
|
+
let loaded = 0;
|
|
129
|
+
for (const [k, v] of Object.entries(s.threadHistory)) {
|
|
130
|
+
if (loaded >= MAX_THREADS) break;
|
|
131
|
+
if (Array.isArray(v)) { threadHistoryMap.set(k, v); loaded++; }
|
|
132
|
+
}
|
|
133
|
+
for (const [k, v] of Object.entries(s.threadAgent)) {
|
|
134
|
+
threadAgentMap.set(k, v);
|
|
135
|
+
}
|
|
136
|
+
console.log(`[state] Loaded ${threadHistoryMap.size} history, ${threadAgentMap.size} agent routes`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 退出时刷盘(exit 事件自动触发,无需额外信号处理)
|
|
140
|
+
process.on('exit', () => flushWrite());
|
|
141
|
+
|
|
142
|
+
// beforeExit: 异步清理窗口。所有 setImmediate / Promise 微任务完成时触发。
|
|
143
|
+
// 比 'exit' 更早,能在进程自然结束时先清理资源(杀 OpenCode server、刷状态)
|
|
144
|
+
process.on('beforeExit', () => {
|
|
145
|
+
flushWrite();
|
|
146
|
+
try { globalThis.__opencodeServer?.kill?.(); } catch {}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 未捕获异常:写日志 + 尝试刷盘 + 退出(让父进程决定是否重启)
|
|
150
|
+
process.on('uncaughtException', (err) => {
|
|
151
|
+
console.error('[FATAL] uncaughtException:', err);
|
|
152
|
+
try {
|
|
153
|
+
const crashLog = join(STATE_DIR, 'crash.log');
|
|
154
|
+
const line = `[${new Date().toISOString()}] ${err.stack || err.message}\n`;
|
|
155
|
+
appendFileSync(crashLog, line);
|
|
156
|
+
} catch {}
|
|
157
|
+
flushWrite();
|
|
158
|
+
try { globalThis.__opencodeServer?.kill?.(); } catch {}
|
|
159
|
+
// 给 logger 一点时间刷盘,然后退出(非 0 表示异常,父进程会重启)
|
|
160
|
+
setTimeout(() => process.exit(1), 100);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
process.on('unhandledRejection', (reason) => {
|
|
164
|
+
console.error('[ERROR] unhandledRejection:', reason);
|
|
165
|
+
// 不退出:unhandled rejection 不一定致命
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// SIGTERM/SIGINT: 优雅关停(PM2 重启、Ctrl+C、kill <pid>)
|
|
169
|
+
// beforeExit 在 kill -9 / 异常崩溃时不可靠,必须独立处理信号
|
|
170
|
+
function gracefulShutdown(signal) {
|
|
171
|
+
console.log(`[shutdown] received ${signal}`);
|
|
172
|
+
try { flushWrite(); } catch (e) { console.error('[shutdown] flushWrite failed:', e); }
|
|
173
|
+
try { globalThis.__opencodeServer?.kill?.(); } catch (e) { console.error('[shutdown] kill OpenCode failed:', e); }
|
|
174
|
+
// 杀掉所有 agent 子进程(claude-code/copilot/codex/opencode plugins)
|
|
175
|
+
// 不杀的话,bot 重启后它们会变孤儿进程
|
|
176
|
+
try {
|
|
177
|
+
const killed = killAllAgentProcesses(1000);
|
|
178
|
+
if (killed.length > 0) {
|
|
179
|
+
console.log(`[shutdown] killed ${killed.length} agent subprocess(es):`,
|
|
180
|
+
killed.map(k => `${k.agentName}@${k.threadId}`).join(', '));
|
|
181
|
+
}
|
|
182
|
+
} catch (e) { console.error('[shutdown] killAllAgentProcesses failed:', e); }
|
|
183
|
+
// 给 logger / agent-registry / agent children 一点时间清理
|
|
184
|
+
setTimeout(() => process.exit(0), 500);
|
|
185
|
+
}
|
|
186
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
187
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
188
|
+
// Windows 没有 SIGTERM 但 npm start 转发 Ctrl+C 事件走 SIGINT
|
|
189
|
+
// SIGBREAK 是 Windows Ctrl+Break(PM2 偶尔发)
|
|
190
|
+
process.on('SIGBREAK', () => gracefulShutdown('SIGBREAK'));
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// In-memory runtime stats — exposed via /info command for self-diagnostics.
|
|
2
|
+
// Reset only on process restart. Cheap to read, safe to write from any handler.
|
|
3
|
+
|
|
4
|
+
const counters = {
|
|
5
|
+
startedAt: Date.now(),
|
|
6
|
+
messagesReceived: 0,
|
|
7
|
+
messagesSent: 0,
|
|
8
|
+
errorsByCode: {}, // error type → count (last 24h, capped)
|
|
9
|
+
opencodeRestarts: 0,
|
|
10
|
+
retries: 0,
|
|
11
|
+
authRejections: 0,
|
|
12
|
+
commandsByType: {}, // command name → count
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Increment a named counter. Safe to call from any handler.
|
|
17
|
+
* @param {string} name
|
|
18
|
+
* @param {number} [by=1]
|
|
19
|
+
*/
|
|
20
|
+
export function incr(name, by = 1) {
|
|
21
|
+
if (typeof counters[name] !== 'number') counters[name] = 0;
|
|
22
|
+
counters[name] += by;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Increment a nested counter (e.g., errorsByCode['AbortError']).
|
|
27
|
+
* @param {string} group
|
|
28
|
+
* @param {string} key
|
|
29
|
+
*/
|
|
30
|
+
export function incrKey(group, key) {
|
|
31
|
+
if (!counters[group] || typeof counters[group] !== 'object') counters[group] = {};
|
|
32
|
+
counters[group][key] = (counters[group][key] || 0) + 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Snapshot of all counters + computed uptime/memory.
|
|
37
|
+
* Cheap to call. Returns a fresh object each time so callers can mutate freely.
|
|
38
|
+
*/
|
|
39
|
+
export function snapshot() {
|
|
40
|
+
const mem = process.memoryUsage();
|
|
41
|
+
return {
|
|
42
|
+
...counters,
|
|
43
|
+
uptimeSec: Math.round((Date.now() - counters.startedAt) / 1000),
|
|
44
|
+
memoryMB: {
|
|
45
|
+
rss: Math.round(mem.rss / 1024 / 1024),
|
|
46
|
+
heapUsed: Math.round(mem.heapUsed / 1024 / 1024),
|
|
47
|
+
heapTotal: Math.round(mem.heapTotal / 1024 / 1024),
|
|
48
|
+
external: Math.round(mem.external / 1024 / 1024),
|
|
49
|
+
},
|
|
50
|
+
nodeVersion: process.version,
|
|
51
|
+
pid: process.pid,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Render a human-readable status block for /info reply.
|
|
57
|
+
* @param {object} [extra] - additional fields to include (e.g., bot version, agent children count)
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function formatInfo(extra = {}) {
|
|
61
|
+
const s = snapshot();
|
|
62
|
+
const fmt = (n) => n.toLocaleString();
|
|
63
|
+
const lines = [
|
|
64
|
+
`📊 opencode-remote 状态`,
|
|
65
|
+
``,
|
|
66
|
+
`⏱️ 启动时间: ${new Date(s.startedAt).toISOString().slice(0, 19)}Z`,
|
|
67
|
+
`⏳ 运行: ${formatDuration(s.uptimeSec)}`,
|
|
68
|
+
`🆔 PID: ${s.pid} · Node ${s.nodeVersion}`,
|
|
69
|
+
``,
|
|
70
|
+
`📨 收到消息: ${fmt(s.messagesReceived)}`,
|
|
71
|
+
`📤 已回复: ${fmt(s.messagesSent)}`,
|
|
72
|
+
`🔄 OpenCode 重启: ${fmt(s.opencodeRestarts)}`,
|
|
73
|
+
`♻️ 重试次数: ${fmt(s.retries)}`,
|
|
74
|
+
`🚫 auth 拒绝: ${fmt(s.authRejections)}`,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
if (Object.keys(s.commandsByType).length > 0) {
|
|
78
|
+
lines.push('', '📋 命令统计:');
|
|
79
|
+
const top = Object.entries(s.commandsByType)
|
|
80
|
+
.sort((a, b) => b[1] - a[1])
|
|
81
|
+
.slice(0, 5);
|
|
82
|
+
for (const [cmd, count] of top) {
|
|
83
|
+
lines.push(` /${cmd}: ${fmt(count)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Object.keys(s.errorsByCode).length > 0) {
|
|
88
|
+
lines.push('', '⚠️ 错误类型:');
|
|
89
|
+
const top = Object.entries(s.errorsByCode)
|
|
90
|
+
.sort((a, b) => b[1] - a[1])
|
|
91
|
+
.slice(0, 5);
|
|
92
|
+
for (const [err, count] of top) {
|
|
93
|
+
lines.push(` ${err}: ${fmt(count)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
lines.push(
|
|
98
|
+
'',
|
|
99
|
+
`💾 内存: heap ${s.memoryMB.heapUsed}/${s.memoryMB.heapTotal}MB · rss ${s.memoryMB.rss}MB`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (extra.version) lines.push(`📦 版本: ${extra.version}`);
|
|
103
|
+
if (typeof extra.activeThreads === 'number') lines.push(`💬 活跃线程: ${fmt(extra.activeThreads)}`);
|
|
104
|
+
if (typeof extra.agentChildren === 'number') lines.push(`🧒 agent 子进程: ${fmt(extra.agentChildren)}`);
|
|
105
|
+
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function formatDuration(sec) {
|
|
110
|
+
if (sec < 60) return `${sec}秒`;
|
|
111
|
+
if (sec < 3600) return `${Math.floor(sec / 60)}分${sec % 60}秒`;
|
|
112
|
+
const h = Math.floor(sec / 3600);
|
|
113
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
114
|
+
return `${h}小时${m}分`;
|
|
115
|
+
}
|
package/dist/feishu/adapter.js
CHANGED
package/dist/feishu/bot.js
CHANGED
|
@@ -3,6 +3,7 @@ import { initOpenCode } from '../opencode/client.js';
|
|
|
3
3
|
import { getAuthStatus } from '../core/auth.js';
|
|
4
4
|
import { createFeishuAdapter } from './adapter.js';
|
|
5
5
|
import { handleMessage } from './handler.js';
|
|
6
|
+
import { LRUSessionMap } from '../core/lru.js';
|
|
6
7
|
|
|
7
8
|
let feishuClient = null;
|
|
8
9
|
let wsClient = null;
|
|
@@ -45,7 +46,8 @@ export async function startFeishuBot(botConfig) {
|
|
|
45
46
|
appId: config.feishuAppId,
|
|
46
47
|
appSecret: config.feishuAppSecret,
|
|
47
48
|
});
|
|
48
|
-
openCodeSessions = new
|
|
49
|
+
openCodeSessions = new LRUSessionMap({ maxSize: 100, ttlMs: 30 * 60 * 1000, name: 'feishu-sessions' });
|
|
50
|
+
setInterval(() => openCodeSessions.cleanup(), 5 * 60 * 1000).unref();
|
|
49
51
|
console.log('🔧 正在初始化 OpenCode...');
|
|
50
52
|
try {
|
|
51
53
|
await initOpenCode();
|
|
@@ -84,7 +86,7 @@ export async function startFeishuBot(botConfig) {
|
|
|
84
86
|
return { code: 0 };
|
|
85
87
|
}
|
|
86
88
|
const ctx = feishuEventToContext(data);
|
|
87
|
-
handleMessage(adapter, ctx, text, openCodeSessions).catch(error => {
|
|
89
|
+
handleMessage(adapter, ctx, text, openCodeSessions, 'feishu').catch(error => {
|
|
88
90
|
console.error('处理飞书消息失败:', error);
|
|
89
91
|
});
|
|
90
92
|
return { code: 0 };
|
package/dist/feishu/commands.js
CHANGED
|
@@ -7,10 +7,10 @@ import { registry } from '../core/registry.js';
|
|
|
7
7
|
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
8
|
import { join } from 'path';
|
|
9
9
|
|
|
10
|
-
async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
10
|
+
async function handleCommand(adapter, ctx, platform, command, arg, openCodeSessions) {
|
|
11
11
|
switch (command) {
|
|
12
12
|
case 'start': {
|
|
13
|
-
const result = claimOwnership(
|
|
13
|
+
const result = claimOwnership(platform, ctx.userId);
|
|
14
14
|
if (result.success) {
|
|
15
15
|
if (result.message === 'claimed') {
|
|
16
16
|
await adapter.reply(ctx.threadId, `🔐 **安全设置完成!**
|
package/dist/feishu/handler.js
CHANGED
|
@@ -1,180 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createSession, sendMessage, checkConnection, listOpenCodeSessions, resumeSession } from '../opencode/client.js';
|
|
3
|
-
import { isAuthorized, hasOwner } from '../core/auth.js';
|
|
4
|
-
import { detectCommand, EXPERT_SYSTEM_PROMPT } from '../core/router.js';
|
|
1
|
+
import { createHandler } from '../core/handler.js';
|
|
5
2
|
import { handleCommand } from './commands.js';
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
const handler = createHandler({
|
|
5
|
+
handleCommand,
|
|
6
|
+
replyTo: (userId, text, fallbackAdapter) => fallbackAdapter.reply(userId, text),
|
|
7
|
+
wrapAdapterForShared: (adapter) => adapter,
|
|
8
|
+
});
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (arg === 'off' || arg === 'reset' || arg === '关闭') {
|
|
14
|
-
await adapter.reply(ctx.threadId, '⏹️ 自定义 prompt 已清除');
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
if (arg) {
|
|
18
|
-
expertPrompt = arg;
|
|
19
|
-
} else {
|
|
20
|
-
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
21
|
-
}
|
|
22
|
-
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (expertTriggers.some(t => text.trim().toLowerCase().includes(t))) {
|
|
27
|
-
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const parsed = detectCommand(text);
|
|
31
|
-
if (parsed) {
|
|
32
|
-
await handleCommand(adapter, ctx, parsed.name, parsed.arg, openCodeSessions);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
if (!isAuthorized('feishu', ctx.userId)) {
|
|
36
|
-
if (!hasOwner('feishu')) {
|
|
37
|
-
await adapter.reply(ctx.threadId, `🔐 **需要认证**
|
|
38
|
-
|
|
39
|
-
此 bot 尚未绑定。
|
|
40
|
-
|
|
41
|
-
请发送 /start 进行首次认证。`);
|
|
42
|
-
} else {
|
|
43
|
-
await adapter.reply(ctx.threadId, `🚫 **拒绝访问**
|
|
44
|
-
|
|
45
|
-
你无权使用此 bot。`);
|
|
46
|
-
}
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
const connected = await checkConnection();
|
|
50
|
-
if (!connected) {
|
|
51
|
-
await adapter.reply(ctx.threadId, `❌ OpenCode 离线
|
|
52
|
-
|
|
53
|
-
无法连接 OpenCode 服务。
|
|
54
|
-
|
|
55
|
-
🔄 /retry — 重试连接`);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
await forwardToOpenCode(adapter, ctx, text, openCodeSessions);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt) {
|
|
62
|
-
await adapter.sendTypingIndicator(ctx.threadId);
|
|
63
|
-
let openCodeSession = openCodeSessions.get(ctx.threadId);
|
|
64
|
-
if (!openCodeSession) {
|
|
65
|
-
console.log(`[feishu-forward] no in-memory session, trying to resume most recent...`);
|
|
66
|
-
try {
|
|
67
|
-
const sessions = await listOpenCodeSessions();
|
|
68
|
-
if (sessions.length > 0) {
|
|
69
|
-
const latest = sessions.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))[0];
|
|
70
|
-
const resumed = await resumeSession(latest.id);
|
|
71
|
-
if (resumed) {
|
|
72
|
-
openCodeSession = resumed;
|
|
73
|
-
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
74
|
-
console.log(`[feishu-forward] resumed session ${latest.id.slice(0, 8)}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
} catch (e) {
|
|
78
|
-
console.log(`[feishu-forward] failed to resume: ${e.message}`);
|
|
79
|
-
}
|
|
80
|
-
if (!openCodeSession) {
|
|
81
|
-
openCodeSession = await createSession(ctx.threadId, `Feishu ${ctx.threadId}`);
|
|
82
|
-
if (!openCodeSession) {
|
|
83
|
-
await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话');
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let scopedText = text;
|
|
91
|
-
|
|
92
|
-
if (expertPrompt) {
|
|
93
|
-
scopedText = `${expertPrompt}\n\n${scopedText}`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
console.log(`📤 Forwarding to OpenCode: ${text.substring(0, 80)}...`);
|
|
97
|
-
try {
|
|
98
|
-
let response = await sendMessage(openCodeSession, scopedText, {
|
|
99
|
-
idleThreshold: expertPrompt ? 30 : 10,
|
|
100
|
-
onEvent: (event) => {
|
|
101
|
-
if (event.type === 'tool.call') {
|
|
102
|
-
const props = event.properties || {};
|
|
103
|
-
const toolName = props.name || props.tool_name || 'unknown';
|
|
104
|
-
const input = props.input || {};
|
|
105
|
-
let toolDesc = `🔧 执行工具: ${toolName}`;
|
|
106
|
-
if (input.path) {
|
|
107
|
-
toolDesc += `\n📁 ${input.path}`;
|
|
108
|
-
}
|
|
109
|
-
if (input.command) {
|
|
110
|
-
toolDesc += `\n💻 ${input.command}`;
|
|
111
|
-
}
|
|
112
|
-
adapter.reply(ctx.threadId, toolDesc).catch(() => {});
|
|
113
|
-
console.log(`[feishu-tool] Executing: ${toolName}`);
|
|
114
|
-
}
|
|
115
|
-
if (event.type && !event.type.includes('delta')) {
|
|
116
|
-
console.log(`📡 Feishu Event: ${event.type}`);
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
onTextDelta: () => {},
|
|
120
|
-
}, ctx.threadId);
|
|
121
|
-
if (!response || typeof response !== 'string') {
|
|
122
|
-
await adapter.reply(ctx.threadId, '...');
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const trimmedResponse = response.trim();
|
|
126
|
-
if (!trimmedResponse) {
|
|
127
|
-
await adapter.reply(ctx.threadId, '...');
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
// 超时/错误 → 清除 session 让下次重建
|
|
131
|
-
if (trimmedResponse.startsWith('⏰') || trimmedResponse.startsWith('❌')) {
|
|
132
|
-
console.error('[feishu-forward] Error response:', trimmedResponse);
|
|
133
|
-
openCodeSessions.delete(ctx.threadId);
|
|
134
|
-
await adapter.reply(ctx.threadId, trimmedResponse);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
if (trimmedResponse.endsWith(':') || trimmedResponse.endsWith('...')) {
|
|
138
|
-
console.log('[feishu-forward] 检测到不完整响应,等待补充...');
|
|
139
|
-
await new Promise(r => setTimeout(r, 5000));
|
|
140
|
-
try {
|
|
141
|
-
const msgsResult = await openCodeSession.client.session.messages({
|
|
142
|
-
sessionID: openCodeSession.sessionId,
|
|
143
|
-
limit: 5,
|
|
144
|
-
});
|
|
145
|
-
if (!msgsResult.error && msgsResult.data && msgsResult.data.length > 0) {
|
|
146
|
-
for (let i = msgsResult.data.length - 1; i >= 0; i--) {
|
|
147
|
-
const msg = msgsResult.data[i];
|
|
148
|
-
if (msg.info?.role === 'assistant' && msg.parts) {
|
|
149
|
-
const textParts = msg.parts.filter(p => p.type === 'text' && p.text).map(p => p.text);
|
|
150
|
-
if (textParts.length > 0) {
|
|
151
|
-
const newResponse = textParts.join('\n').trim();
|
|
152
|
-
if (newResponse && newResponse !== trimmedResponse) {
|
|
153
|
-
response = newResponse;
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
} catch (e) {
|
|
161
|
-
console.log('[feishu-forward] 无法检查额外响应');
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
const responseMsgs = splitMessage(response);
|
|
165
|
-
for (const m of responseMsgs) {
|
|
166
|
-
const trimmed = m.trim();
|
|
167
|
-
if (!trimmed) continue;
|
|
168
|
-
try {
|
|
169
|
-
await adapter.reply(ctx.threadId, m);
|
|
170
|
-
} catch (replyErr) {
|
|
171
|
-
console.error('[feishu-forward] 回复失败:', replyErr.message);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
} catch (error) {
|
|
175
|
-
console.error('❌ Feishu 错误:', error);
|
|
176
|
-
await adapter.reply(ctx.threadId, `❌ 错误: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export { handleMessage, forwardToOpenCode };
|
|
10
|
+
export const handleMessage = handler.handleMessage;
|
|
11
|
+
export const forwardToOpenCode = handler.forwardToOpenCode;
|