@yvhitxcel/opencode-remote 0.16.3 → 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/autonomous/decisions.js +73 -0
- package/dist/autonomous/index.js +141 -0
- package/dist/cli.js +121 -19
- 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 +143 -0
- package/dist/core/handler.js +293 -0
- package/dist/core/log.js +92 -0
- package/dist/core/lru.js +98 -0
- package/dist/core/notifications.js +2 -2
- package/dist/core/qiniu.js +2 -2
- package/dist/core/retry.js +46 -0
- package/dist/core/router.js +62 -296
- 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 -4
- package/dist/feishu/commands.js +28 -397
- package/dist/feishu/handler.js +9 -369
- package/dist/opencode/client.js +172 -168
- package/dist/patch_spawn.js +1 -0
- package/dist/plugins/agents/claude-code/index.js +59 -47
- 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 +38 -12
- package/dist/telegram/adapter.js +22 -9
- package/dist/telegram/bot.js +1 -6
- package/dist/weixin/adapter.js +37 -15
- package/dist/weixin/api.js +47 -19
- package/dist/weixin/bot.js +172 -83
- package/dist/weixin/commands.js +476 -597
- package/dist/weixin/handler.js +27 -541
- package/dist/weixin/user-adapter-map.js +12 -0
- package/package.json +5 -3
- package/dist/core/session.js +0 -403
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
// Claude Code CLI agent adapter
|
|
1
|
+
// Claude Code CLI agent adapter — --print + explicit context prompt
|
|
2
|
+
// @ts-nocheck — spawn options type differs between @types/node versions
|
|
2
3
|
import { spawn } from 'child_process';
|
|
3
4
|
import { platform } from 'os';
|
|
5
|
+
import { tmpdir } from 'os';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { registerAgentProcess, unregisterAgentProcess } from '../../../core/agent-registry.js';
|
|
4
8
|
|
|
5
9
|
const LIUV_CRASH_PATTERNS = [
|
|
6
10
|
'Assertion failed',
|
|
@@ -23,18 +27,13 @@ export class ClaudeCodeAgentAdapter {
|
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
26
|
-
const
|
|
27
|
-
const contextualPrompt =
|
|
28
|
-
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
buildContextualPrompt(prompt, history) {
|
|
35
|
-
if (!history || history.length === 0) return prompt;
|
|
36
|
-
const historyText = history.map(msg => `[${msg.role}]: ${msg.content}`).join('\n\n');
|
|
37
|
-
return `Previous:\n${historyText}\n\n${prompt}`;
|
|
30
|
+
const threadId = options.threadId;
|
|
31
|
+
const contextualPrompt = buildContextualPrompt(prompt, history);
|
|
32
|
+
// 跑在临时目录,避免 claude 扫描项目源码崩在 misc-lib.mjs
|
|
33
|
+
const safeCwd = join(tmpdir(), 'opencode-remote-claude');
|
|
34
|
+
const { mkdirSync, existsSync } = await import('fs');
|
|
35
|
+
if (!existsSync(safeCwd)) mkdirSync(safeCwd, { recursive: true });
|
|
36
|
+
return this.callClaude(['--print', contextualPrompt], safeCwd, threadId);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
isCrashNoise(line) {
|
|
@@ -42,70 +41,83 @@ export class ClaudeCodeAgentAdapter {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
extractErrorMessage(stdout, stderr, code) {
|
|
45
|
-
// 先检查 stdout:--print 模式把错误也输出到 stdout
|
|
46
44
|
const stdoutLines = stdout.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
|
47
45
|
const stdoutErrors = stdoutLines.filter(l => !this.isCrashNoise(l));
|
|
48
|
-
if (stdoutErrors.length > 0)
|
|
49
|
-
return stdoutErrors.join('\n');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// 再检查 stderr
|
|
46
|
+
if (stdoutErrors.length > 0) return stdoutErrors.join('\n');
|
|
53
47
|
const stderrLines = stderr.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
|
54
48
|
const stderrReal = stderrLines.filter(l => !this.isCrashNoise(l));
|
|
55
|
-
if (stderrReal.length > 0)
|
|
56
|
-
return stderrReal.join('\n');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// 如果全是崩溃噪音,尝试从任一流中找 Error 关键词
|
|
49
|
+
if (stderrReal.length > 0) return stderrReal.join('\n');
|
|
60
50
|
const all = [...stdoutLines, ...stderrLines];
|
|
61
51
|
const firstRelevant = all.find(l => /Error|error|ERROR|^\d{3}/.test(l));
|
|
62
52
|
if (firstRelevant) return firstRelevant;
|
|
63
|
-
|
|
64
|
-
// 兜底
|
|
65
53
|
return `进程异常退出 (code: ${code})`;
|
|
66
54
|
}
|
|
67
55
|
|
|
68
|
-
callClaude(args,
|
|
56
|
+
callClaude(args, safeCwd, threadId) {
|
|
69
57
|
return new Promise((resolve) => {
|
|
70
58
|
const opts = {
|
|
71
59
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
72
60
|
shell: true,
|
|
61
|
+
cwd: safeCwd,
|
|
73
62
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
63
|
+
// shell:true on Windows cmd.exe (and /bin/sh on POSIX) interprets several chars
|
|
64
|
+
// as command syntax. Strip them so user message content can't trigger command
|
|
65
|
+
// splitting, redirection, or quoting issues:
|
|
66
|
+
// \n \r — command separator (cmd.exe), treated as command end
|
|
67
|
+
// & | < > ^ — command separator / pipe / redirect / escape
|
|
68
|
+
// " ' ` — quote chars (could break out of intended argument)
|
|
69
|
+
// We strip rather than escape to avoid platform-specific escape syntax.
|
|
70
|
+
const safeArgs = args.map(a => {
|
|
71
|
+
if (typeof a !== 'string') return a;
|
|
72
|
+
return a
|
|
73
|
+
.replace(/[\r\n]+/g, ' ')
|
|
74
|
+
.replace(/[&|<>^"`]/g, '')
|
|
75
|
+
.replace(/\s+/g, ' ')
|
|
76
|
+
.trim();
|
|
77
|
+
});
|
|
78
|
+
console.log(`[claude-code] ${safeArgs.join(' ')}`);
|
|
79
|
+
const proc = spawn('claude', safeArgs, opts);
|
|
80
|
+
if (threadId) registerAgentProcess(threadId, proc, 'claude-code');
|
|
80
81
|
let stdout = '';
|
|
81
82
|
let stderr = '';
|
|
83
|
+
let killed = false;
|
|
82
84
|
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
83
85
|
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
84
86
|
|
|
87
|
+
const TIMEOUT_MS = parseInt(process.env.OPENCODE_TIMEOUT || '600', 10) * 1000;
|
|
85
88
|
const timeout = setTimeout(() => {
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
killed = true;
|
|
90
|
+
console.warn(`[claude-code] Timeout ${TIMEOUT_MS / 1000}s`);
|
|
91
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
92
|
+
resolve(`⏰ Claude Code 超时 (${TIMEOUT_MS / 1000}s)`);
|
|
93
|
+
}, TIMEOUT_MS);
|
|
88
94
|
|
|
89
95
|
proc.on('close', (code) => {
|
|
90
96
|
clearTimeout(timeout);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
97
|
+
if (threadId) unregisterAgentProcess(threadId);
|
|
98
|
+
if (killed) return;
|
|
99
|
+
console.log(`[claude-code] exit ${code}, ${stdout.length} bytes`);
|
|
100
|
+
if (code === 0) { resolve(stdout.trim()); return; }
|
|
98
101
|
const errorMsg = this.extractErrorMessage(stdout, stderr, code);
|
|
99
|
-
|
|
100
|
-
console.log(`[claude-code] Error detail:\n${errorMsg}`);
|
|
101
|
-
|
|
102
|
-
resolve(`❌ Claude Code 错误 (exit code ${code}): ${errorMsg}`);
|
|
102
|
+
resolve(`❌ Claude Code 错误 (exit ${code}): ${errorMsg}`);
|
|
103
103
|
});
|
|
104
104
|
proc.on('error', (err) => {
|
|
105
105
|
clearTimeout(timeout);
|
|
106
|
-
|
|
106
|
+
if (threadId) unregisterAgentProcess(threadId);
|
|
107
107
|
resolve(`❌ Claude Code 启动失败: ${err.message}`);
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
|
+
|
|
113
|
+
function buildContextualPrompt(prompt, history) {
|
|
114
|
+
if (!history || history.length === 0) return prompt;
|
|
115
|
+
const lines = history.slice(-10).map(m => {
|
|
116
|
+
const label = m.role === 'user' ? 'User' : 'Assistant';
|
|
117
|
+
return `${label}: ${m.content}`;
|
|
118
|
+
}).join('\n');
|
|
119
|
+
return `[Previous conversation — for context only, answer the LATEST question below]\n\n${lines}\n\n[Latest question]\n${prompt}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Exported for tests
|
|
123
|
+
export { buildContextualPrompt };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// OpenAI Codex CLI agent adapter
|
|
2
|
+
// @ts-nocheck — spawn options type differs between @types/node versions
|
|
2
3
|
import { spawn } from 'child_process';
|
|
3
4
|
import { platform } from 'os';
|
|
5
|
+
import { registerAgentProcess, unregisterAgentProcess } from '../../../core/agent-registry.js';
|
|
4
6
|
|
|
5
7
|
const CRASH_PATTERNS = [
|
|
6
8
|
'Assertion failed',
|
|
@@ -22,15 +24,18 @@ export class CodexAgentAdapter {
|
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
async sendPrompt(_sessionId, prompt, history) {
|
|
27
|
+
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
26
28
|
const contextualPrompt = this.buildContextualPrompt(prompt, history);
|
|
27
|
-
return this.callCodex(contextualPrompt);
|
|
29
|
+
return this.callCodex(contextualPrompt, options.threadId);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
buildContextualPrompt(prompt, history) {
|
|
31
33
|
if (!history || history.length === 0) return prompt;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
+
const lines = history.slice(-10).map(msg => {
|
|
35
|
+
const label = msg.role === 'user' ? 'User' : 'AI';
|
|
36
|
+
return `${label}: ${msg.content}`;
|
|
37
|
+
}).join('\n');
|
|
38
|
+
return `[Previous conversation — for context only, answer the LATEST question below]\n\n${lines}\n\n[Latest question]\n${prompt}`;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
extractErrorMessage(stdout, stderr) {
|
|
@@ -43,17 +48,38 @@ export class CodexAgentAdapter {
|
|
|
43
48
|
return first || null;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
callCodex(prompt, threadId) {
|
|
47
52
|
return new Promise((resolve) => {
|
|
48
|
-
|
|
53
|
+
// shell:true on Windows cmd.exe (and /bin/sh on POSIX) interprets several chars
|
|
54
|
+
// as command syntax. Strip them so user message content can't trigger command
|
|
55
|
+
// splitting, redirection, or quoting issues.
|
|
56
|
+
const safePrompt = typeof prompt === 'string'
|
|
57
|
+
? prompt
|
|
58
|
+
.replace(/[\r\n]+/g, ' ')
|
|
59
|
+
.replace(/[&|<>^"`]/g, '')
|
|
60
|
+
.replace(/\s+/g, ' ').trim()
|
|
61
|
+
: prompt;
|
|
62
|
+
const proc = spawn('codex', ['exec', safePrompt], {
|
|
49
63
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
64
|
shell: true,
|
|
51
65
|
});
|
|
66
|
+
if (threadId) registerAgentProcess(threadId, proc, 'codex');
|
|
52
67
|
let stdout = '';
|
|
53
68
|
let stderr = '';
|
|
69
|
+
let killed = false;
|
|
54
70
|
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
55
71
|
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
72
|
+
const TIMEOUT_MS = parseInt(process.env.OPENCODE_TIMEOUT || '600', 10) * 1000;
|
|
73
|
+
const timeout = setTimeout(() => {
|
|
74
|
+
killed = true;
|
|
75
|
+
console.warn(`[codex] Timeout after ${TIMEOUT_MS / 1000}s, killing process`);
|
|
76
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
77
|
+
resolve(`⏰ Codex 超时 (${TIMEOUT_MS / 1000}s),任务已终止`);
|
|
78
|
+
}, TIMEOUT_MS);
|
|
56
79
|
proc.on('close', (code) => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
if (threadId) unregisterAgentProcess(threadId);
|
|
82
|
+
if (killed) return;
|
|
57
83
|
if (code === 0) {
|
|
58
84
|
resolve(stdout.trim());
|
|
59
85
|
} else {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// GitHub Copilot CLI agent adapter
|
|
2
|
+
// @ts-nocheck — spawn options type differs between @types/node versions
|
|
2
3
|
import { spawn } from 'child_process';
|
|
3
4
|
import { platform } from 'os';
|
|
5
|
+
import { registerAgentProcess, unregisterAgentProcess } from '../../../core/agent-registry.js';
|
|
4
6
|
|
|
5
7
|
const CRASH_PATTERNS = [
|
|
6
8
|
'Assertion failed',
|
|
@@ -22,15 +24,18 @@ export class CopilotAgentAdapter {
|
|
|
22
24
|
});
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
async sendPrompt(_sessionId, prompt, history) {
|
|
27
|
+
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
26
28
|
const contextualPrompt = this.buildContextualPrompt(prompt, history);
|
|
27
|
-
return this.callCopilot(contextualPrompt);
|
|
29
|
+
return this.callCopilot(contextualPrompt, options.threadId);
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
buildContextualPrompt(prompt, history) {
|
|
31
33
|
if (!history || history.length === 0) return prompt;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
+
const lines = history.slice(-10).map(msg => {
|
|
35
|
+
const label = msg.role === 'user' ? 'User' : 'AI';
|
|
36
|
+
return `${label}: ${msg.content}`;
|
|
37
|
+
}).join('\n');
|
|
38
|
+
return `[Previous conversation — for context only, answer the LATEST question below]\n\n${lines}\n\n[Latest question]\n${prompt}`;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
extractErrorMessage(stdout, stderr) {
|
|
@@ -43,17 +48,38 @@ export class CopilotAgentAdapter {
|
|
|
43
48
|
return first || null;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
callCopilot(prompt, threadId) {
|
|
47
52
|
return new Promise((resolve) => {
|
|
48
|
-
|
|
53
|
+
// shell:true on Windows cmd.exe (and /bin/sh on POSIX) interprets several chars
|
|
54
|
+
// as command syntax. Strip them so user message content can't trigger command
|
|
55
|
+
// splitting, redirection, or quoting issues.
|
|
56
|
+
const safePrompt = typeof prompt === 'string'
|
|
57
|
+
? prompt
|
|
58
|
+
.replace(/[\r\n]+/g, ' ')
|
|
59
|
+
.replace(/[&|<>^"`]/g, '')
|
|
60
|
+
.replace(/\s+/g, ' ').trim()
|
|
61
|
+
: prompt;
|
|
62
|
+
const proc = spawn('copilot', ['-p', safePrompt], {
|
|
49
63
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
50
64
|
shell: true,
|
|
51
65
|
});
|
|
66
|
+
if (threadId) registerAgentProcess(threadId, proc, 'copilot');
|
|
52
67
|
let stdout = '';
|
|
53
68
|
let stderr = '';
|
|
69
|
+
let killed = false;
|
|
54
70
|
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
55
71
|
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
72
|
+
const TIMEOUT_MS = parseInt(process.env.OPENCODE_TIMEOUT || '600', 10) * 1000;
|
|
73
|
+
const timeout = setTimeout(() => {
|
|
74
|
+
killed = true;
|
|
75
|
+
console.warn(`[copilot] Timeout after ${TIMEOUT_MS / 1000}s, killing process`);
|
|
76
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
77
|
+
resolve(`⏰ Copilot 超时 (${TIMEOUT_MS / 1000}s),任务已终止`);
|
|
78
|
+
}, TIMEOUT_MS);
|
|
56
79
|
proc.on('close', (code) => {
|
|
80
|
+
clearTimeout(timeout);
|
|
81
|
+
if (threadId) unregisterAgentProcess(threadId);
|
|
82
|
+
if (killed) return;
|
|
57
83
|
if (code === 0) {
|
|
58
84
|
resolve(stdout.trim());
|
|
59
85
|
} else {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// OpenCode CLI agent adapter
|
|
2
|
+
// @ts-nocheck — spawn options type differs between @types/node versions
|
|
2
3
|
import { spawn } from 'child_process';
|
|
3
4
|
import { platform } from 'os';
|
|
5
|
+
import { registerAgentProcess, unregisterAgentProcess } from '../../../core/agent-registry.js';
|
|
4
6
|
|
|
5
7
|
const CRASH_PATTERNS = [
|
|
6
8
|
'Assertion failed',
|
|
@@ -23,18 +25,20 @@ export class OpenCodeAgentAdapter {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
28
|
+
const threadId = options.threadId;
|
|
26
29
|
const contextualPrompt = this.buildContextualPrompt(prompt, history);
|
|
27
|
-
return this.callOpenCode(contextualPrompt);
|
|
30
|
+
return this.callOpenCode(contextualPrompt, threadId);
|
|
28
31
|
}
|
|
29
|
-
|
|
32
|
+
|
|
30
33
|
buildContextualPrompt(prompt, history) {
|
|
31
34
|
if (!history || history.length === 0) return prompt;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
|
|
35
|
+
const lines = history.slice(-10).map(msg => {
|
|
36
|
+
const label = msg.role === 'user' ? 'User' : 'AI';
|
|
37
|
+
return `${label}: ${msg.content}`;
|
|
38
|
+
}).join('\n');
|
|
39
|
+
return `[Previous conversation — for context only, answer the LATEST question below]\n\n${lines}\n\n[Latest question]\n${prompt}`;
|
|
36
40
|
}
|
|
37
|
-
|
|
41
|
+
|
|
38
42
|
extractErrorMessage(stdout, stderr) {
|
|
39
43
|
const lines = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
|
|
40
44
|
.map(l => l.trim()).filter(Boolean)
|
|
@@ -45,18 +49,38 @@ export class OpenCodeAgentAdapter {
|
|
|
45
49
|
.find(l => /Error|error|ERROR|^\d{3}/.test(l));
|
|
46
50
|
return first || null;
|
|
47
51
|
}
|
|
48
|
-
|
|
49
|
-
callOpenCode(prompt) {
|
|
52
|
+
|
|
53
|
+
callOpenCode(prompt, threadId) {
|
|
50
54
|
return new Promise((resolve) => {
|
|
51
|
-
|
|
55
|
+
// shell:true on Windows cmd.exe (and /bin/sh on POSIX) interprets several chars
|
|
56
|
+
// as command syntax. Strip them so user message content can't trigger command
|
|
57
|
+
// splitting, redirection, or quoting issues.
|
|
58
|
+
const safePrompt = typeof prompt === 'string'
|
|
59
|
+
? prompt
|
|
60
|
+
.replace(/[\r\n]+/g, ' ')
|
|
61
|
+
.replace(/[&|<>^"`]/g, '')
|
|
62
|
+
.replace(/\s+/g, ' ')
|
|
63
|
+
.trim()
|
|
64
|
+
: prompt;
|
|
65
|
+
const proc = spawn('opencode', ['run', '--format', 'json', safePrompt], {
|
|
52
66
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
67
|
shell: true,
|
|
54
68
|
});
|
|
69
|
+
if (threadId) registerAgentProcess(threadId, proc, 'opencode');
|
|
55
70
|
|
|
56
71
|
let stdout = '';
|
|
57
72
|
let stderr = '';
|
|
58
73
|
let fullText = '';
|
|
59
74
|
let resigned = false;
|
|
75
|
+
let killed = false;
|
|
76
|
+
|
|
77
|
+
const TIMEOUT_MS = parseInt(process.env.OPENCODE_TIMEOUT || '600', 10) * 1000;
|
|
78
|
+
const timeout = setTimeout(() => {
|
|
79
|
+
killed = true;
|
|
80
|
+
console.warn(`[opencode-agent] Timeout after ${TIMEOUT_MS / 1000}s, killing process`);
|
|
81
|
+
try { proc.kill('SIGKILL'); } catch {}
|
|
82
|
+
resolve(`⏰ OpenCode 超时 (${TIMEOUT_MS / 1000}s),任务已终止`);
|
|
83
|
+
}, TIMEOUT_MS);
|
|
60
84
|
|
|
61
85
|
const STUCK_PATTERNS = [
|
|
62
86
|
'Free usage exceeded', 'quota exceeded', 'rate limit',
|
|
@@ -88,7 +112,7 @@ export class OpenCodeAgentAdapter {
|
|
|
88
112
|
try {
|
|
89
113
|
const event = JSON.parse(line);
|
|
90
114
|
if (event.text) fullText += event.text;
|
|
91
|
-
} catch {}
|
|
115
|
+
} catch (e) { console.debug('[opencode-agent] stdout parse:', e.message); }
|
|
92
116
|
}
|
|
93
117
|
});
|
|
94
118
|
|
|
@@ -98,7 +122,9 @@ export class OpenCodeAgentAdapter {
|
|
|
98
122
|
});
|
|
99
123
|
|
|
100
124
|
proc.on('close', (code) => {
|
|
101
|
-
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
if (threadId) unregisterAgentProcess(threadId);
|
|
127
|
+
if (resigned || killed) return;
|
|
102
128
|
if (code !== 0) {
|
|
103
129
|
const detail = this.extractErrorMessage(stdout, stderr);
|
|
104
130
|
const hint = detail
|
package/dist/telegram/adapter.js
CHANGED
|
@@ -38,15 +38,12 @@ export class TelegramAdapter {
|
|
|
38
38
|
async sendCommandMenu(threadId, title) {
|
|
39
39
|
if (!this.bot) return;
|
|
40
40
|
const groups = [
|
|
41
|
-
['
|
|
42
|
-
['
|
|
43
|
-
['
|
|
44
|
-
['🧠 专家', ['/tutorial', '/z', '/diagnose']],
|
|
45
|
-
['📂 会话', ['/sessions', '/delsessions', '/copy', '/revert']],
|
|
46
|
-
['⬆️ 文件', ['/upload', '/delete']],
|
|
41
|
+
['/help', '/start', '/reset', '/diagnose'],
|
|
42
|
+
['/restart', '/model', '/oc', '/cc'],
|
|
43
|
+
['/cx', '/copilot'],
|
|
47
44
|
];
|
|
48
45
|
const keyboard = [];
|
|
49
|
-
for (const
|
|
46
|
+
for (const cmds of groups) {
|
|
50
47
|
const row = cmds.map(cmd => ({ text: cmd, callback_data: `cmd:${cmd.slice(1)}` }));
|
|
51
48
|
keyboard.push(row);
|
|
52
49
|
}
|
|
@@ -55,14 +52,30 @@ export class TelegramAdapter {
|
|
|
55
52
|
});
|
|
56
53
|
}
|
|
57
54
|
|
|
55
|
+
// BotAdapter 接口方法
|
|
56
|
+
async reply(threadId, text) { return this.sendMessage(threadId, text); }
|
|
57
|
+
async sendTypingIndicator(threadId) { return this.sendTyping(threadId, true); }
|
|
58
|
+
async sendTypingEnd(threadId) { return this.sendTyping(threadId, false); }
|
|
59
|
+
async updateMessage(threadId, messageId, text) {
|
|
60
|
+
if (!this.bot || !messageId) return;
|
|
61
|
+
try { await this.bot.api.editMessageText(threadId, Number(messageId), text); } catch (e) { console.warn('[telegram] updateMessage failed:', e.message); }
|
|
62
|
+
}
|
|
63
|
+
async deleteMessage(threadId, messageId) {
|
|
64
|
+
if (!this.bot || !messageId) return;
|
|
65
|
+
try { await this.bot.api.deleteMessage(threadId, Number(messageId)); }
|
|
66
|
+
catch (e) { console.debug('[telegram] deleteMessage failed:', e.message); }
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
async sendTyping(threadId, isTyping) {
|
|
59
70
|
if (!this.bot) return;
|
|
60
71
|
if (isTyping) {
|
|
61
|
-
try { await this.bot.api.sendChatAction(threadId, 'typing'); }
|
|
72
|
+
try { await this.bot.api.sendChatAction(threadId, 'typing'); }
|
|
73
|
+
catch (e) { console.debug('[telegram] sendChatAction failed:', e.message); }
|
|
62
74
|
const existing = this.typingIntervals.get(threadId);
|
|
63
75
|
if (existing) clearInterval(existing);
|
|
64
76
|
const interval = setInterval(async () => {
|
|
65
|
-
try { await this.bot.api.sendChatAction(threadId, 'typing'); }
|
|
77
|
+
try { await this.bot.api.sendChatAction(threadId, 'typing'); }
|
|
78
|
+
catch (e) { console.debug('[telegram] typing-tick failed:', e.message); }
|
|
66
79
|
}, 4000);
|
|
67
80
|
this.typingIntervals.set(threadId, interval);
|
|
68
81
|
} else {
|
package/dist/telegram/bot.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { registry } from '../core/registry.js';
|
|
2
|
-
import { sessionManager } from '../core/session.js';
|
|
3
2
|
import { initOpenCode, createSession, sendMessage as sendToOpenCode, checkConnection } from '../opencode/client.js';
|
|
4
3
|
import { parseMessage, routeMessage } from '../core/router.js';
|
|
5
4
|
import { telegramAdapter } from './adapter.js';
|
|
5
|
+
import { splitMessage } from '../utils/message-split.js';
|
|
6
6
|
|
|
7
7
|
export async function startBot() {
|
|
8
8
|
const { loadConfig } = await import('../core/config.js');
|
|
@@ -17,7 +17,6 @@ export async function startBot() {
|
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
await sessionManager.start();
|
|
21
20
|
await registry.loadBuiltInPlugins();
|
|
22
21
|
await telegramAdapter.start(config);
|
|
23
22
|
|
|
@@ -68,10 +67,6 @@ export async function startBot() {
|
|
|
68
67
|
if (parsed.type === 'command' && parsed.command === 'reset') {
|
|
69
68
|
openCodeSessions.delete(message.threadId);
|
|
70
69
|
opencodeSessionId = null;
|
|
71
|
-
try {
|
|
72
|
-
const session = await sessionManager.getExistingSession(platform, channelId, message.threadId);
|
|
73
|
-
if (session) await sessionManager.resetConversation(platform, channelId, message.threadId);
|
|
74
|
-
} catch (e) { console.warn('[Telegram] Reset error:', e.message); }
|
|
75
70
|
}
|
|
76
71
|
|
|
77
72
|
if (parsed.type === 'default') {
|
package/dist/weixin/adapter.js
CHANGED
|
@@ -7,13 +7,35 @@ function createWeixinAdapter(baseUrl, token, botId) {
|
|
|
7
7
|
const processedMessages = new Map();
|
|
8
8
|
const DEDUP_WINDOW_MS = 30_000;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
// 定期清理 contextTokens 和 typingTickets,防内存泄漏
|
|
11
|
+
const CLEANUP_INTERVAL_MS = 30 * 60 * 1000;
|
|
12
|
+
let cleanupTimer = null;
|
|
13
|
+
function startCleanup() {
|
|
14
|
+
if (cleanupTimer) return;
|
|
15
|
+
cleanupTimer = setInterval(() => {
|
|
16
|
+
const cutoff = Date.now() - CLEANUP_INTERVAL_MS;
|
|
17
|
+
for (const [k, v] of contextTokens) { if (typeof v !== 'object' || !v._ts || v._ts < cutoff) contextTokens.delete(k); }
|
|
18
|
+
for (const [k, v] of typingTickets) { if (typeof v !== 'object' || !v._ts || v._ts < cutoff) typingTickets.delete(k); }
|
|
19
|
+
if (processedMessages.size > 1000) {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
for (const [id, ts] of processedMessages.entries()) {
|
|
22
|
+
if (now - ts > DEDUP_WINDOW_MS) processedMessages.delete(id);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}, CLEANUP_INTERVAL_MS);
|
|
26
|
+
if (cleanupTimer.unref) cleanupTimer.unref();
|
|
27
|
+
}
|
|
28
|
+
startCleanup();
|
|
29
|
+
|
|
30
|
+
function isDuplicate(messageId, contentKey) {
|
|
31
|
+
// 优先用内容去重: 微信两条消息可能 messageId 不同但内容相同
|
|
32
|
+
const key = contentKey || messageId;
|
|
33
|
+
if (!key) return false;
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const seenAt = processedMessages.get(key);
|
|
36
|
+
if (seenAt && now - seenAt < DEDUP_WINDOW_MS) return true;
|
|
37
|
+
processedMessages.set(key, now);
|
|
15
38
|
if (processedMessages.size > 1000) {
|
|
16
|
-
const now = Date.now();
|
|
17
39
|
for (const [id, ts] of processedMessages.entries()) {
|
|
18
40
|
if (now - ts > DEDUP_WINDOW_MS) processedMessages.delete(id);
|
|
19
41
|
}
|
|
@@ -29,7 +51,8 @@ function createWeixinAdapter(baseUrl, token, botId) {
|
|
|
29
51
|
_token: token,
|
|
30
52
|
_botId: botId,
|
|
31
53
|
async reply(threadId, text) {
|
|
32
|
-
let
|
|
54
|
+
let entry = contextTokens.get(threadId);
|
|
55
|
+
let contextToken = entry?.value || entry;
|
|
33
56
|
let retryCount = 0;
|
|
34
57
|
const maxRetries = 2;
|
|
35
58
|
|
|
@@ -40,7 +63,7 @@ function createWeixinAdapter(baseUrl, token, botId) {
|
|
|
40
63
|
const r = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: undefined });
|
|
41
64
|
contextToken = r.context_token || r.typing_ticket;
|
|
42
65
|
if (contextToken) {
|
|
43
|
-
contextTokens.set(threadId, contextToken);
|
|
66
|
+
contextTokens.set(threadId, { value: contextToken, _ts: Date.now() });
|
|
44
67
|
console.log(`[Weixin] Got contextToken: ${contextToken.slice(0, 8)}...`);
|
|
45
68
|
} else if (r.errcode === -14) {
|
|
46
69
|
console.log(`[Weixin] Session timeout, retrying with fresh token...`);
|
|
@@ -88,24 +111,24 @@ function createWeixinAdapter(baseUrl, token, botId) {
|
|
|
88
111
|
throw err;
|
|
89
112
|
},
|
|
90
113
|
async sendTypingIndicator(threadId) {
|
|
91
|
-
const
|
|
92
|
-
let ticket =
|
|
114
|
+
const entry = typingTickets.get(threadId);
|
|
115
|
+
let ticket = entry?.value || entry;
|
|
93
116
|
|
|
94
117
|
if (!ticket) {
|
|
95
118
|
try {
|
|
96
|
-
const r = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: contextTokens.get(threadId) });
|
|
119
|
+
const r = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: (contextTokens.get(threadId) || {}).value });
|
|
97
120
|
if (r.errcode === -14) {
|
|
98
121
|
contextTokens.delete(threadId);
|
|
99
122
|
typingTickets.delete(threadId);
|
|
100
123
|
const freshConfig = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: undefined });
|
|
101
124
|
ticket = freshConfig.typing_ticket;
|
|
102
125
|
if (freshConfig.context_token) {
|
|
103
|
-
contextTokens.set(threadId, freshConfig.context_token);
|
|
126
|
+
contextTokens.set(threadId, { value: freshConfig.context_token, _ts: Date.now() });
|
|
104
127
|
}
|
|
105
128
|
} else {
|
|
106
129
|
ticket = r.typing_ticket;
|
|
107
130
|
}
|
|
108
|
-
if (ticket) typingTickets.set(threadId, ticket);
|
|
131
|
+
if (ticket) typingTickets.set(threadId, { value: ticket, _ts: Date.now() });
|
|
109
132
|
} catch { console.debug('[typing] getConfig failed'); }
|
|
110
133
|
}
|
|
111
134
|
if (ticket) {
|
|
@@ -118,7 +141,7 @@ function createWeixinAdapter(baseUrl, token, botId) {
|
|
|
118
141
|
try {
|
|
119
142
|
const freshConfig = await getConfig({ baseUrl, token, ilinkUserId: threadId, contextToken: undefined });
|
|
120
143
|
if (freshConfig.typing_ticket) {
|
|
121
|
-
typingTickets.set(threadId, freshConfig.typing_ticket);
|
|
144
|
+
typingTickets.set(threadId, { value: freshConfig.typing_ticket, _ts: Date.now() });
|
|
122
145
|
await sendTyping({ baseUrl, token, body: { ilink_user_id: threadId, typing_ticket: freshConfig.typing_ticket, status: 1 } });
|
|
123
146
|
}
|
|
124
147
|
} catch { console.debug('[typing] retry getConfig failed'); }
|
|
@@ -132,4 +155,3 @@ function createWeixinAdapter(baseUrl, token, botId) {
|
|
|
132
155
|
}
|
|
133
156
|
|
|
134
157
|
export { createWeixinAdapter };
|
|
135
|
-
export default createWeixinAdapter;
|