@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
package/dist/opencode/client.js
CHANGED
|
@@ -14,7 +14,7 @@ const CONFIG_FILE = join(CONFIG_DIR, '.env');
|
|
|
14
14
|
const threadModels = new Map();
|
|
15
15
|
const recentModels = [];
|
|
16
16
|
let rawDebugEnabled = false;
|
|
17
|
-
let thinkVisibleEnabled =
|
|
17
|
+
let thinkVisibleEnabled = true;
|
|
18
18
|
|
|
19
19
|
export function setRawDebug(enabled) {
|
|
20
20
|
rawDebugEnabled = enabled;
|
|
@@ -332,6 +332,18 @@ export async function initOpenCode() {
|
|
|
332
332
|
}
|
|
333
333
|
return null;
|
|
334
334
|
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Reset the cached OpenCode instance + server reference.
|
|
338
|
+
* Call before retrying initOpenCode() after an AbortError or fatal disconnect
|
|
339
|
+
* to force a fresh server spawn instead of returning the stale singleton.
|
|
340
|
+
*/
|
|
341
|
+
export function resetOpenCode() {
|
|
342
|
+
opencodeInstance = null;
|
|
343
|
+
opencodeServer = null;
|
|
344
|
+
console.log('[opencode] reset cached instance (forced fresh start next call)');
|
|
345
|
+
}
|
|
346
|
+
|
|
335
347
|
export async function verifyOpenCodeInstalled() {
|
|
336
348
|
return new Promise((resolve) => {
|
|
337
349
|
const isWindows = platform() === 'win32';
|
|
@@ -400,55 +412,58 @@ export async function createSession(_threadId, title = `Remote control session`)
|
|
|
400
412
|
}
|
|
401
413
|
// Send message - use promptAsync then poll for response
|
|
402
414
|
export async function sendMessage(session, message, callbacks, threadId) {
|
|
403
|
-
const TIMEOUT_MS =
|
|
415
|
+
const TIMEOUT_MS = parseInt(process.env.OPENCODE_TIMEOUT || '180', 10) * 1000;
|
|
404
416
|
|
|
417
|
+
// Verify session is valid first
|
|
405
418
|
try {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
console.error('[sendMessage] Session error:', sessionCheck.error);
|
|
411
|
-
return '❌ 会话无效,请发送 /restart 重启';
|
|
412
|
-
}
|
|
413
|
-
} catch (e) {
|
|
414
|
-
console.error('[sendMessage] Session check failed:', e.message);
|
|
415
|
-
return '❌ 会话连接失败,请发送 /restart 重启';
|
|
419
|
+
const sessionCheck = await session.client.session.get({ sessionID: session.sessionId });
|
|
420
|
+
if (sessionCheck.error) {
|
|
421
|
+
console.error('[sendMessage] Session error:', sessionCheck.error);
|
|
422
|
+
throw new Error(`session invalid: ${sessionCheck.error}`);
|
|
416
423
|
}
|
|
424
|
+
} catch (e) {
|
|
425
|
+
console.error('[sendMessage] Session check failed:', e.message);
|
|
426
|
+
throw new Error(`session check failed: ${e.message}`);
|
|
427
|
+
}
|
|
417
428
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
429
|
+
// Build prompt body
|
|
430
|
+
const promptBody = {
|
|
431
|
+
parts: [{ type: 'text', text: message }]
|
|
432
|
+
};
|
|
433
|
+
// Inject local model preference if set
|
|
434
|
+
if (threadId && threadModels.has(threadId)) {
|
|
435
|
+
session.model = threadModels.get(threadId);
|
|
436
|
+
pushRecent(session.model);
|
|
437
|
+
}
|
|
438
|
+
// Per-message model override if set on session
|
|
439
|
+
if (session.model?.providerID && session.model?.modelID) {
|
|
440
|
+
promptBody.model = {
|
|
441
|
+
providerID: session.model.providerID,
|
|
442
|
+
modelID: session.model.modelID,
|
|
421
443
|
};
|
|
422
|
-
|
|
423
|
-
if (threadId && threadModels.has(threadId)) {
|
|
424
|
-
session.model = threadModels.get(threadId);
|
|
425
|
-
pushRecent(session.model);
|
|
426
|
-
}
|
|
427
|
-
// Per-message model override if set on session
|
|
428
|
-
if (session.model?.providerID && session.model?.modelID) {
|
|
429
|
-
promptBody.model = {
|
|
430
|
-
providerID: session.model.providerID,
|
|
431
|
-
modelID: session.model.modelID,
|
|
432
|
-
};
|
|
433
|
-
}
|
|
444
|
+
}
|
|
434
445
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
try {
|
|
440
|
-
const response = await session.client.session.prompt({
|
|
441
|
-
sessionID: session.sessionId,
|
|
442
|
-
parts: promptBody.parts,
|
|
443
|
-
...(promptBody.model ? { model: promptBody.model } : {}),
|
|
444
|
-
}, {
|
|
445
|
-
parseAs: 'stream',
|
|
446
|
-
signal: abortController.signal,
|
|
447
|
-
});
|
|
446
|
+
// Stream the response via session.prompt (POST /session/{sessionID}/message)
|
|
447
|
+
const abortController = new AbortController();
|
|
448
|
+
const timeoutId = setTimeout(() => abortController.abort(), TIMEOUT_MS);
|
|
448
449
|
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
try {
|
|
451
|
+
const response = await session.client.session.prompt({
|
|
452
|
+
sessionID: session.sessionId,
|
|
453
|
+
parts: promptBody.parts,
|
|
454
|
+
...(promptBody.model ? { model: promptBody.model } : {}),
|
|
455
|
+
}, {
|
|
456
|
+
parseAs: 'stream',
|
|
457
|
+
signal: abortController.signal,
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
if (response.error) {
|
|
461
|
+
if (/abort/i.test(response.error)) {
|
|
462
|
+
console.warn('[sendMessage] SDK returned AbortError');
|
|
463
|
+
throw new Error(response.error || 'AbortError');
|
|
451
464
|
}
|
|
465
|
+
throw new Error(response.error);
|
|
466
|
+
}
|
|
452
467
|
|
|
453
468
|
const stream = response.data;
|
|
454
469
|
if (!stream) {
|
|
@@ -472,6 +487,11 @@ export async function sendMessage(session, message, callbacks, threadId) {
|
|
|
472
487
|
if (isRawDebug()) console.log('[RAW]', rawJson);
|
|
473
488
|
try {
|
|
474
489
|
const parsed = JSON.parse(rawJson);
|
|
490
|
+
// 顶层 error → 透传真实错误
|
|
491
|
+
if (parsed.error) {
|
|
492
|
+
const errMsg = typeof parsed.error === 'string' ? parsed.error : (parsed.error.message || JSON.stringify(parsed.error));
|
|
493
|
+
throw new Error(errMsg);
|
|
494
|
+
}
|
|
475
495
|
const t = parsed.info?.tokens || {};
|
|
476
496
|
const time = parsed.info?.time || {};
|
|
477
497
|
const elapsed = time.completed && time.created ? `${(time.completed - time.created) / 1000}s` : '?';
|
|
@@ -498,13 +518,23 @@ export async function sendMessage(session, message, callbacks, threadId) {
|
|
|
498
518
|
}
|
|
499
519
|
}
|
|
500
520
|
}
|
|
521
|
+
// info.error 字段 → 透传
|
|
522
|
+
if (!responseText && parsed.info?.error) {
|
|
523
|
+
throw new Error(parsed.info.error);
|
|
524
|
+
}
|
|
525
|
+
// 非正常结束 → 透传 finish 原因
|
|
526
|
+
if (!responseText && parsed.info?.finish && parsed.info.finish !== 'stop') {
|
|
527
|
+
throw new Error(`finish=${parsed.info.finish}`);
|
|
528
|
+
}
|
|
501
529
|
if (!responseText && parsed.info?.finish) {
|
|
502
|
-
|
|
530
|
+
throw new Error(`Empty response (finish=${parsed.info.finish}, tokens=${t.total || 0})`);
|
|
503
531
|
}
|
|
504
532
|
} catch (e) {
|
|
533
|
+
if (e.message && !e.message.startsWith('Unexpected')) throw e;
|
|
505
534
|
console.error('[sendMessage] Failed to parse response:', e.message);
|
|
506
535
|
console.log('[RAW]', rawJson.slice(0, 1000));
|
|
507
|
-
responseText = rawJson;
|
|
536
|
+
if (rawJson.trim()) responseText = rawJson;
|
|
537
|
+
else throw new Error('Empty response (no stream data)');
|
|
508
538
|
}
|
|
509
539
|
|
|
510
540
|
callbacks?.onStatusChange?.({ type: 'idle' });
|
|
@@ -513,14 +543,6 @@ export async function sendMessage(session, message, callbacks, threadId) {
|
|
|
513
543
|
} finally {
|
|
514
544
|
clearTimeout(timeoutId);
|
|
515
545
|
}
|
|
516
|
-
} catch (error) {
|
|
517
|
-
if (error.name === 'AbortError') {
|
|
518
|
-
console.warn('[sendMessage] 5min timeout, aborting stream');
|
|
519
|
-
return '⏰ 请求超时,请重试';
|
|
520
|
-
}
|
|
521
|
-
console.error('[sendMessage] Error:', error);
|
|
522
|
-
return `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
523
|
-
}
|
|
524
546
|
}
|
|
525
547
|
export async function getSession(session) {
|
|
526
548
|
try {
|
|
@@ -620,8 +642,8 @@ export async function listOpenCodeSessions() {
|
|
|
620
642
|
id: s.id,
|
|
621
643
|
title: s.title || 'Untitled',
|
|
622
644
|
directory: s.directory || '',
|
|
623
|
-
createdAt: s.
|
|
624
|
-
lastActivity: s.
|
|
645
|
+
createdAt: s.time?.created || 0,
|
|
646
|
+
lastActivity: s.time?.updated || 0,
|
|
625
647
|
}));
|
|
626
648
|
}
|
|
627
649
|
catch (error) {
|
|
@@ -644,8 +666,8 @@ export async function listOpenCodeSessionsFromServer(baseUrl) {
|
|
|
644
666
|
id: s.id,
|
|
645
667
|
title: s.title || 'Untitled',
|
|
646
668
|
directory: s.directory || '',
|
|
647
|
-
createdAt: s.
|
|
648
|
-
lastActivity: s.
|
|
669
|
+
createdAt: s.time?.created || 0,
|
|
670
|
+
lastActivity: s.time?.updated || 0,
|
|
649
671
|
}));
|
|
650
672
|
}
|
|
651
673
|
catch (error) {
|
package/dist/patch_spawn.js
CHANGED
|
@@ -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,22 +27,13 @@ export class ClaudeCodeAgentAdapter {
|
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const args = ['--print', '-c', contextualPrompt];
|
|
34
|
-
|
|
35
|
-
return this.callClaude(args, projectDir);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
buildContextualPrompt(prompt, history) {
|
|
39
|
-
if (!history || history.length === 0) return prompt;
|
|
40
|
-
const historyText = history.map(msg => `[${msg.role}]: ${msg.content}`).join('\n\n');
|
|
41
|
-
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);
|
|
42
37
|
}
|
|
43
38
|
|
|
44
39
|
isCrashNoise(line) {
|
|
@@ -46,70 +41,83 @@ export class ClaudeCodeAgentAdapter {
|
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
extractErrorMessage(stdout, stderr, code) {
|
|
49
|
-
// 先检查 stdout:--print 模式把错误也输出到 stdout
|
|
50
44
|
const stdoutLines = stdout.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
|
51
45
|
const stdoutErrors = stdoutLines.filter(l => !this.isCrashNoise(l));
|
|
52
|
-
if (stdoutErrors.length > 0)
|
|
53
|
-
return stdoutErrors.join('\n');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// 再检查 stderr
|
|
46
|
+
if (stdoutErrors.length > 0) return stdoutErrors.join('\n');
|
|
57
47
|
const stderrLines = stderr.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
|
58
48
|
const stderrReal = stderrLines.filter(l => !this.isCrashNoise(l));
|
|
59
|
-
if (stderrReal.length > 0)
|
|
60
|
-
return stderrReal.join('\n');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 如果全是崩溃噪音,尝试从任一流中找 Error 关键词
|
|
49
|
+
if (stderrReal.length > 0) return stderrReal.join('\n');
|
|
64
50
|
const all = [...stdoutLines, ...stderrLines];
|
|
65
51
|
const firstRelevant = all.find(l => /Error|error|ERROR|^\d{3}/.test(l));
|
|
66
52
|
if (firstRelevant) return firstRelevant;
|
|
67
|
-
|
|
68
|
-
// 兜底
|
|
69
53
|
return `进程异常退出 (code: ${code})`;
|
|
70
54
|
}
|
|
71
55
|
|
|
72
|
-
callClaude(args,
|
|
56
|
+
callClaude(args, safeCwd, threadId) {
|
|
73
57
|
return new Promise((resolve) => {
|
|
74
58
|
const opts = {
|
|
75
59
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
60
|
shell: true,
|
|
61
|
+
cwd: safeCwd,
|
|
77
62
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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');
|
|
84
81
|
let stdout = '';
|
|
85
82
|
let stderr = '';
|
|
83
|
+
let killed = false;
|
|
86
84
|
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
87
85
|
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
88
86
|
|
|
87
|
+
const TIMEOUT_MS = parseInt(process.env.OPENCODE_TIMEOUT || '600', 10) * 1000;
|
|
89
88
|
const timeout = setTimeout(() => {
|
|
90
|
-
|
|
91
|
-
|
|
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);
|
|
92
94
|
|
|
93
95
|
proc.on('close', (code) => {
|
|
94
96
|
clearTimeout(timeout);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
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; }
|
|
102
101
|
const errorMsg = this.extractErrorMessage(stdout, stderr, code);
|
|
103
|
-
|
|
104
|
-
console.log(`[claude-code] Error detail:\n${errorMsg}`);
|
|
105
|
-
|
|
106
|
-
resolve(`❌ Claude Code 错误 (exit code ${code}): ${errorMsg}`);
|
|
102
|
+
resolve(`❌ Claude Code 错误 (exit ${code}): ${errorMsg}`);
|
|
107
103
|
});
|
|
108
104
|
proc.on('error', (err) => {
|
|
109
105
|
clearTimeout(timeout);
|
|
110
|
-
|
|
106
|
+
if (threadId) unregisterAgentProcess(threadId);
|
|
111
107
|
resolve(`❌ Claude Code 启动失败: ${err.message}`);
|
|
112
108
|
});
|
|
113
109
|
});
|
|
114
110
|
}
|
|
115
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,20 +25,18 @@ export class OpenCodeAgentAdapter {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
const contextualPrompt = this.buildContextualPrompt(cleanPrompt, history);
|
|
31
|
-
return this.callOpenCode(contextualPrompt);
|
|
28
|
+
const threadId = options.threadId;
|
|
29
|
+
const contextualPrompt = this.buildContextualPrompt(prompt, history);
|
|
30
|
+
return this.callOpenCode(contextualPrompt, threadId);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
buildContextualPrompt(prompt, history) {
|
|
35
34
|
if (!history || history.length === 0) return prompt;
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
|
|
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}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
extractErrorMessage(stdout, stderr) {
|
|
@@ -50,17 +50,37 @@ export class OpenCodeAgentAdapter {
|
|
|
50
50
|
return first || null;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
callOpenCode(prompt) {
|
|
53
|
+
callOpenCode(prompt, threadId) {
|
|
54
54
|
return new Promise((resolve) => {
|
|
55
|
-
|
|
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], {
|
|
56
66
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
57
67
|
shell: true,
|
|
58
68
|
});
|
|
69
|
+
if (threadId) registerAgentProcess(threadId, proc, 'opencode');
|
|
59
70
|
|
|
60
71
|
let stdout = '';
|
|
61
72
|
let stderr = '';
|
|
62
73
|
let fullText = '';
|
|
63
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);
|
|
64
84
|
|
|
65
85
|
const STUCK_PATTERNS = [
|
|
66
86
|
'Free usage exceeded', 'quota exceeded', 'rate limit',
|
|
@@ -92,7 +112,7 @@ export class OpenCodeAgentAdapter {
|
|
|
92
112
|
try {
|
|
93
113
|
const event = JSON.parse(line);
|
|
94
114
|
if (event.text) fullText += event.text;
|
|
95
|
-
} catch {}
|
|
115
|
+
} catch (e) { console.debug('[opencode-agent] stdout parse:', e.message); }
|
|
96
116
|
}
|
|
97
117
|
});
|
|
98
118
|
|
|
@@ -102,7 +122,9 @@ export class OpenCodeAgentAdapter {
|
|
|
102
122
|
});
|
|
103
123
|
|
|
104
124
|
proc.on('close', (code) => {
|
|
105
|
-
|
|
125
|
+
clearTimeout(timeout);
|
|
126
|
+
if (threadId) unregisterAgentProcess(threadId);
|
|
127
|
+
if (resigned || killed) return;
|
|
106
128
|
if (code !== 0) {
|
|
107
129
|
const detail = this.extractErrorMessage(stdout, stderr);
|
|
108
130
|
const hint = detail
|
package/dist/telegram/adapter.js
CHANGED
|
@@ -43,7 +43,7 @@ export class TelegramAdapter {
|
|
|
43
43
|
['/cx', '/copilot'],
|
|
44
44
|
];
|
|
45
45
|
const keyboard = [];
|
|
46
|
-
for (const
|
|
46
|
+
for (const cmds of groups) {
|
|
47
47
|
const row = cmds.map(cmd => ({ text: cmd, callback_data: `cmd:${cmd.slice(1)}` }));
|
|
48
48
|
keyboard.push(row);
|
|
49
49
|
}
|
|
@@ -52,14 +52,30 @@ export class TelegramAdapter {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
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
|
+
|
|
55
69
|
async sendTyping(threadId, isTyping) {
|
|
56
70
|
if (!this.bot) return;
|
|
57
71
|
if (isTyping) {
|
|
58
|
-
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); }
|
|
59
74
|
const existing = this.typingIntervals.get(threadId);
|
|
60
75
|
if (existing) clearInterval(existing);
|
|
61
76
|
const interval = setInterval(async () => {
|
|
62
|
-
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); }
|
|
63
79
|
}, 4000);
|
|
64
80
|
this.typingIntervals.set(threadId, interval);
|
|
65
81
|
} else {
|