@yvhitxcel/opencode-remote 0.16.2 → 0.17.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/dist/autonomous/decisions.js +73 -0
- package/dist/autonomous/index.js +141 -0
- package/dist/cli.js +1 -10
- package/dist/core/config.js +1 -1
- package/dist/core/git-push.js +120 -0
- package/dist/core/notifications.js +2 -2
- package/dist/core/router.js +97 -305
- package/dist/feishu/bot.js +0 -2
- package/dist/feishu/commands.js +82 -417
- package/dist/feishu/handler.js +26 -217
- package/dist/opencode/client.js +167 -145
- package/dist/plugins/agents/claude-code/index.js +6 -2
- package/dist/plugins/agents/opencode/index.js +40 -10
- package/dist/telegram/adapter.js +3 -6
- package/dist/telegram/bot.js +1 -6
- package/dist/weixin/api.js +9 -2
- package/dist/weixin/bot.js +123 -69
- package/dist/weixin/commands.js +361 -584
- package/dist/weixin/handler.js +170 -422
- package/dist/weixin/user-adapter-map.js +1 -0
- package/package.json +1 -1
- package/dist/core/session.js +0 -403
package/dist/opencode/client.js
CHANGED
|
@@ -11,6 +11,57 @@ import { homedir } from 'os';
|
|
|
11
11
|
const CONFIG_DIR = join(homedir(), '.opencode-remote');
|
|
12
12
|
const CONFIG_FILE = join(CONFIG_DIR, '.env');
|
|
13
13
|
|
|
14
|
+
const threadModels = new Map();
|
|
15
|
+
const recentModels = [];
|
|
16
|
+
let rawDebugEnabled = false;
|
|
17
|
+
let thinkVisibleEnabled = false;
|
|
18
|
+
|
|
19
|
+
export function setRawDebug(enabled) {
|
|
20
|
+
rawDebugEnabled = enabled;
|
|
21
|
+
console.log(`[rawDebug] ${enabled ? 'ON' : 'OFF'}`);
|
|
22
|
+
}
|
|
23
|
+
export function isRawDebug() {
|
|
24
|
+
return rawDebugEnabled || process.env.DEBUG_RAW === '1';
|
|
25
|
+
}
|
|
26
|
+
export function setThinkVisible(enabled) {
|
|
27
|
+
thinkVisibleEnabled = enabled;
|
|
28
|
+
console.log(`[think] ${enabled ? 'ON' : 'OFF'}`);
|
|
29
|
+
}
|
|
30
|
+
export function isThinkVisible() {
|
|
31
|
+
return thinkVisibleEnabled;
|
|
32
|
+
}
|
|
33
|
+
export function setThreadModel(threadId, modelStr) {
|
|
34
|
+
if (!modelStr || !modelStr.includes('/')) {
|
|
35
|
+
threadModels.delete(threadId);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const parts = modelStr.split('/');
|
|
39
|
+
const entry = { providerID: parts[0], modelID: parts.slice(1).join('/') };
|
|
40
|
+
threadModels.set(threadId, entry);
|
|
41
|
+
pushRecent(entry);
|
|
42
|
+
return entry;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getThreadModel(threadId) {
|
|
46
|
+
return threadModels.get(threadId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getRecentModels() {
|
|
50
|
+
return [...recentModels];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function pushRecentModel(entry) {
|
|
54
|
+
pushRecent(entry);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function pushRecent(entry) {
|
|
58
|
+
const key = `${entry.providerID}/${entry.modelID}`;
|
|
59
|
+
const idx = recentModels.findIndex(e => `${e.providerID}/${e.modelID}` === key);
|
|
60
|
+
if (idx !== -1) recentModels.splice(idx, 1);
|
|
61
|
+
recentModels.unshift(entry);
|
|
62
|
+
if (recentModels.length > 5) recentModels.length = 5;
|
|
63
|
+
}
|
|
64
|
+
|
|
14
65
|
// Find opencode.exe binary
|
|
15
66
|
function findOpenCodeExe() {
|
|
16
67
|
const isWindows = platform() === 'win32';
|
|
@@ -174,7 +225,6 @@ let opencodeInstance = null;
|
|
|
174
225
|
let opencodeServer = null;
|
|
175
226
|
let lastStdoutTime = 0;
|
|
176
227
|
let lastStdoutLine = '';
|
|
177
|
-
let lastReportedStatus = '';
|
|
178
228
|
const PORTS_TO_TRY = [4096, 4097, 4098];
|
|
179
229
|
|
|
180
230
|
// TCP-level port probe: true = occupied, false = free
|
|
@@ -190,7 +240,7 @@ function probeTCP(port, timeoutMs = 2000) {
|
|
|
190
240
|
}
|
|
191
241
|
|
|
192
242
|
async function tryConnectPort(port, timeoutMs = 5000) {
|
|
193
|
-
const { createOpencodeClient } = await import('@opencode-ai/sdk');
|
|
243
|
+
const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
|
|
194
244
|
const client = createOpencodeClient({ baseUrl: `http://localhost:${port}` });
|
|
195
245
|
const result = await Promise.race([
|
|
196
246
|
client.session.list(),
|
|
@@ -255,7 +305,7 @@ export async function initOpenCode() {
|
|
|
255
305
|
opencodeServer.on('exit', (code) => console.log(`[opencode] exited with code ${code}`));
|
|
256
306
|
|
|
257
307
|
// Wait for server to be ready
|
|
258
|
-
const { createOpencodeClient } = await import('@opencode-ai/sdk');
|
|
308
|
+
const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
|
|
259
309
|
for (let i = 0; i < 15; i++) {
|
|
260
310
|
await new Promise(r => setTimeout(r, 1000));
|
|
261
311
|
try {
|
|
@@ -318,7 +368,7 @@ export async function createSession(_threadId, title = `Remote control session`)
|
|
|
318
368
|
const opencode = await initOpenCode();
|
|
319
369
|
try {
|
|
320
370
|
const createResult = await opencode.client.session.create({
|
|
321
|
-
|
|
371
|
+
title,
|
|
322
372
|
});
|
|
323
373
|
if (createResult.error) {
|
|
324
374
|
console.error('Failed to create session:', createResult.error);
|
|
@@ -329,7 +379,7 @@ export async function createSession(_threadId, title = `Remote control session`)
|
|
|
329
379
|
let shareUrl;
|
|
330
380
|
if (process.env.SHARE_SESSIONS === 'true') {
|
|
331
381
|
const shareResult = await opencode.client.session.share({
|
|
332
|
-
|
|
382
|
+
sessionID: sessionId,
|
|
333
383
|
});
|
|
334
384
|
if (!shareResult.error && shareResult.data?.share?.url) {
|
|
335
385
|
shareUrl = shareResult.data.share.url;
|
|
@@ -349,14 +399,13 @@ export async function createSession(_threadId, title = `Remote control session`)
|
|
|
349
399
|
}
|
|
350
400
|
}
|
|
351
401
|
// Send message - use promptAsync then poll for response
|
|
352
|
-
export async function sendMessage(session, message, callbacks) {
|
|
353
|
-
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
354
|
-
|
|
355
|
-
|
|
402
|
+
export async function sendMessage(session, message, callbacks, threadId) {
|
|
403
|
+
const TIMEOUT_MS = 5 * 60 * 1000;
|
|
404
|
+
|
|
356
405
|
try {
|
|
357
406
|
// Verify session is valid first
|
|
358
407
|
try {
|
|
359
|
-
const sessionCheck = await session.client.session.get({
|
|
408
|
+
const sessionCheck = await session.client.session.get({ sessionID: session.sessionId });
|
|
360
409
|
if (sessionCheck.error) {
|
|
361
410
|
console.error('[sendMessage] Session error:', sessionCheck.error);
|
|
362
411
|
return '❌ 会话无效,请发送 /restart 重启';
|
|
@@ -365,22 +414,16 @@ export async function sendMessage(session, message, callbacks) {
|
|
|
365
414
|
console.error('[sendMessage] Session check failed:', e.message);
|
|
366
415
|
return '❌ 会话连接失败,请发送 /restart 重启';
|
|
367
416
|
}
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
let lastMsgId = null;
|
|
371
|
-
let msgCountBefore = 0;
|
|
372
|
-
try {
|
|
373
|
-
const msgsBefore = await session.client.session.messages({ path: { id: session.sessionId } });
|
|
374
|
-
if (msgsBefore.data?.length > 0) {
|
|
375
|
-
lastMsgId = msgsBefore.data[msgsBefore.data.length - 1].info?.id;
|
|
376
|
-
msgCountBefore = msgsBefore.data.length;
|
|
377
|
-
}
|
|
378
|
-
} catch { /* ignore */ }
|
|
379
|
-
|
|
380
|
-
// Send message using promptAsync (non-blocking)
|
|
417
|
+
|
|
418
|
+
// Build prompt body
|
|
381
419
|
const promptBody = {
|
|
382
420
|
parts: [{ type: 'text', text: message }]
|
|
383
421
|
};
|
|
422
|
+
// Inject local model preference if set
|
|
423
|
+
if (threadId && threadModels.has(threadId)) {
|
|
424
|
+
session.model = threadModels.get(threadId);
|
|
425
|
+
pushRecent(session.model);
|
|
426
|
+
}
|
|
384
427
|
// Per-message model override if set on session
|
|
385
428
|
if (session.model?.providerID && session.model?.modelID) {
|
|
386
429
|
promptBody.model = {
|
|
@@ -388,124 +431,101 @@ export async function sendMessage(session, message, callbacks) {
|
|
|
388
431
|
modelID: session.model.modelID,
|
|
389
432
|
};
|
|
390
433
|
}
|
|
391
|
-
const sendResult = await session.client.session.promptAsync({
|
|
392
|
-
path: { id: session.sessionId },
|
|
393
|
-
body: promptBody,
|
|
394
|
-
});
|
|
395
434
|
|
|
396
|
-
//
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
let hasToolActivity = false;
|
|
400
|
-
let idleSince = 0; // 最后一次收到新内容的时间戳
|
|
401
|
-
let lastStatus = '';
|
|
435
|
+
// Stream the response via session.prompt (POST /session/{sessionID}/message)
|
|
436
|
+
const abortController = new AbortController();
|
|
437
|
+
const timeoutId = setTimeout(() => abortController.abort(), TIMEOUT_MS);
|
|
402
438
|
|
|
403
|
-
|
|
404
|
-
|
|
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
|
+
});
|
|
405
448
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
});
|
|
449
|
+
if (response.error) {
|
|
450
|
+
return `❌ 发送失败: ${response.error}`;
|
|
451
|
+
}
|
|
410
452
|
|
|
411
|
-
|
|
412
|
-
|
|
453
|
+
const stream = response.data;
|
|
454
|
+
if (!stream) {
|
|
455
|
+
return '❌ 未收到响应流';
|
|
456
|
+
}
|
|
413
457
|
|
|
414
|
-
|
|
458
|
+
const reader = stream.getReader();
|
|
459
|
+
const decoder = new TextDecoder();
|
|
460
|
+
let rawJson = '';
|
|
461
|
+
let responseText = '';
|
|
415
462
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
break;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
if (hasToolActivity) break;
|
|
427
|
-
}
|
|
463
|
+
while (true) {
|
|
464
|
+
const { done, value } = await reader.read();
|
|
465
|
+
if (done) break;
|
|
466
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
467
|
+
if (!chunk) continue;
|
|
468
|
+
rawJson += chunk;
|
|
469
|
+
}
|
|
428
470
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
471
|
+
// Parse the full response JSON
|
|
472
|
+
if (isRawDebug()) console.log('[RAW]', rawJson);
|
|
473
|
+
try {
|
|
474
|
+
const parsed = JSON.parse(rawJson);
|
|
475
|
+
const t = parsed.info?.tokens || {};
|
|
476
|
+
const time = parsed.info?.time || {};
|
|
477
|
+
const elapsed = time.completed && time.created ? `${(time.completed - time.created) / 1000}s` : '?';
|
|
478
|
+
const cacheRead = t.cache?.read || 0;
|
|
479
|
+
const cacheWrite = t.cache?.write || 0;
|
|
480
|
+
const cacheRate = cacheRead + cacheWrite > 0 ? `${(cacheRead / (cacheRead + cacheWrite) * 100).toFixed(0)}%` : '-';
|
|
481
|
+
console.log(`[RESPONSE] ${parsed.info?.providerID}/${parsed.info?.modelID} │ ${elapsed} │ tokens=${t.total || '?'} (in=${t.input} out=${t.output} rsn=${t.reasoning}) │ cache ${cacheRate} │ finish=${parsed.info?.finish || '?'}`);
|
|
482
|
+
const meta = { modelID: parsed.info?.modelID, providerID: parsed.info?.providerID, tokens: t, parts: parsed.parts };
|
|
483
|
+
callbacks?.onResponseMeta?.(meta);
|
|
484
|
+
if (parsed.parts) {
|
|
485
|
+
for (const part of parsed.parts) {
|
|
486
|
+
if (part.type === 'text' && part.text) {
|
|
487
|
+
responseText += part.text;
|
|
488
|
+
callbacks?.onNewContent?.(part.text);
|
|
489
|
+
callbacks?.onTextDelta?.(part.text);
|
|
490
|
+
}
|
|
491
|
+
if (part.type === 'reasoning' && part.text) {
|
|
492
|
+
const cleaned = part.text.replace(/\n/g, ' ').trim();
|
|
493
|
+
console.log(`[REASONING] ${cleaned.slice(0, 300)}`);
|
|
494
|
+
if (thinkVisibleEnabled) {
|
|
495
|
+
responseText += `\n🤔 思考: ${cleaned}\n━━━━━━━━━━━━━━━━━━\n`;
|
|
496
|
+
callbacks?.onNewContent?.(`\n🤔 思考: ${cleaned}\n━━━━━━━━━━━━━━━━━━\n`);
|
|
439
497
|
}
|
|
440
498
|
}
|
|
441
499
|
}
|
|
442
|
-
const fullText = newParts.join('\n');
|
|
443
|
-
if (fullText && fullText !== responseText) {
|
|
444
|
-
const delta = fullText.slice(responseText.length);
|
|
445
|
-
responseText = fullText;
|
|
446
|
-
callbacks?.onTextDelta?.(delta);
|
|
447
|
-
callbacks?.onNewContent?.(delta);
|
|
448
|
-
idleSince = Date.now();
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
500
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
const latestStatus = msgsResult.data?.length ? msgsResult.data[msgsResult.data.length - 1]?.info?.status : '';
|
|
455
|
-
if (latestStatus === 'thinking' || latestStatus === 'pending_tool') {
|
|
456
|
-
idleSince = Date.now();
|
|
457
|
-
}
|
|
458
|
-
if (latestStatus) lastStatus = latestStatus;
|
|
459
|
-
if (latestStatus && latestStatus !== lastReportedStatus) {
|
|
460
|
-
lastReportedStatus = latestStatus;
|
|
461
|
-
console.log(`[AI状态] ${latestStatus}`);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// 有回复后:等 30 秒无新内容且 AI 不忙才退出
|
|
465
|
-
if (responseText && Date.now() - idleSince > 30000) {
|
|
466
|
-
break;
|
|
501
|
+
if (!responseText && parsed.info?.finish) {
|
|
502
|
+
responseText = '[empty response]';
|
|
467
503
|
}
|
|
468
504
|
} catch (e) {
|
|
469
|
-
console.
|
|
505
|
+
console.error('[sendMessage] Failed to parse response:', e.message);
|
|
506
|
+
console.log('[RAW]', rawJson.slice(0, 1000));
|
|
507
|
+
responseText = rawJson;
|
|
470
508
|
}
|
|
509
|
+
|
|
510
|
+
callbacks?.onStatusChange?.({ type: 'idle' });
|
|
511
|
+
return responseText;
|
|
512
|
+
|
|
513
|
+
} finally {
|
|
514
|
+
clearTimeout(timeoutId);
|
|
471
515
|
}
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
console.warn('
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
const finalMsgs = await session.client.session.messages({ path: { id: session.sessionId }, query: { limit: 50 } });
|
|
478
|
-
if (finalMsgs.data?.length) {
|
|
479
|
-
for (let i = finalMsgs.data.length - 1; i >= 0; i--) {
|
|
480
|
-
const msg = finalMsgs.data[i];
|
|
481
|
-
if (msg.info?.role === 'assistant' && msg.parts) {
|
|
482
|
-
const textParts = msg.parts.filter(p => p.type === 'text' && p.text).map(p => p.text);
|
|
483
|
-
if (textParts.length > 0) {
|
|
484
|
-
responseText = textParts.join('\n');
|
|
485
|
-
break;
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
} catch { /* ignore */ }
|
|
491
|
-
|
|
492
|
-
if (!responseText) {
|
|
493
|
-
return '⏰ 请求超时,请重试';
|
|
494
|
-
}
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (error.name === 'AbortError') {
|
|
518
|
+
console.warn('[sendMessage] 5min timeout, aborting stream');
|
|
519
|
+
return '⏰ 请求超时,请重试';
|
|
495
520
|
}
|
|
496
|
-
|
|
497
|
-
callbacks?.onStatusChange?.({ type: 'idle', hasToolActivity });
|
|
498
|
-
return responseText;
|
|
499
|
-
}
|
|
500
|
-
catch (error) {
|
|
501
|
-
console.error('Error sending message:', error);
|
|
521
|
+
console.error('[sendMessage] Error:', error);
|
|
502
522
|
return `❌ Error: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
|
503
523
|
}
|
|
504
524
|
}
|
|
505
525
|
export async function getSession(session) {
|
|
506
526
|
try {
|
|
507
527
|
const result = await session.client.session.get({
|
|
508
|
-
|
|
528
|
+
sessionID: session.sessionId
|
|
509
529
|
});
|
|
510
530
|
if (result.error) {
|
|
511
531
|
return null;
|
|
@@ -519,7 +539,7 @@ export async function getSession(session) {
|
|
|
519
539
|
export async function shareSession(session) {
|
|
520
540
|
try {
|
|
521
541
|
const result = await session.client.session.share({
|
|
522
|
-
|
|
542
|
+
sessionID: session.sessionId
|
|
523
543
|
});
|
|
524
544
|
if (result.error || !result.data?.share?.url) {
|
|
525
545
|
return null;
|
|
@@ -545,7 +565,7 @@ export async function checkConnection() {
|
|
|
545
565
|
export async function abortSession(session) {
|
|
546
566
|
try {
|
|
547
567
|
await session.client.session.abort({
|
|
548
|
-
|
|
568
|
+
sessionID: session.sessionId
|
|
549
569
|
});
|
|
550
570
|
console.log(`🛑 Aborted session: ${session.sessionId}`);
|
|
551
571
|
return true;
|
|
@@ -558,7 +578,7 @@ export async function abortSession(session) {
|
|
|
558
578
|
export async function getSessionMessages(session, limit = 20) {
|
|
559
579
|
try {
|
|
560
580
|
const result = await session.client.session.messages({
|
|
561
|
-
|
|
581
|
+
sessionID: session.sessionId
|
|
562
582
|
});
|
|
563
583
|
if (result.error) {
|
|
564
584
|
return null;
|
|
@@ -574,7 +594,7 @@ export async function resumeSession(sessionId, title = 'Resumed session') {
|
|
|
574
594
|
try {
|
|
575
595
|
const opencode = await initOpenCode();
|
|
576
596
|
if (!opencode) return null;
|
|
577
|
-
const getResult = await opencode.client.session.get({
|
|
597
|
+
const getResult = await opencode.client.session.get({ sessionID: sessionId });
|
|
578
598
|
if (getResult.error) {
|
|
579
599
|
console.warn(`Session ${sessionId} not found`);
|
|
580
600
|
return null;
|
|
@@ -599,10 +619,9 @@ export async function listOpenCodeSessions() {
|
|
|
599
619
|
return sessions.map(s => ({
|
|
600
620
|
id: s.id,
|
|
601
621
|
title: s.title || 'Untitled',
|
|
602
|
-
status: s.status?.type || 'unknown',
|
|
603
622
|
directory: s.directory || '',
|
|
604
|
-
createdAt: s.created_at || 0,
|
|
605
|
-
lastActivity: s.updated_at || 0,
|
|
623
|
+
createdAt: s.created_at || s.time?.created || 0,
|
|
624
|
+
lastActivity: s.updated_at || s.time?.updated || 0,
|
|
606
625
|
}));
|
|
607
626
|
}
|
|
608
627
|
catch (error) {
|
|
@@ -612,7 +631,7 @@ export async function listOpenCodeSessions() {
|
|
|
612
631
|
}
|
|
613
632
|
export async function listOpenCodeSessionsFromServer(baseUrl) {
|
|
614
633
|
try {
|
|
615
|
-
const { createOpencodeClient } = await import('@opencode-ai/sdk');
|
|
634
|
+
const { createOpencodeClient } = await import('@opencode-ai/sdk/v2');
|
|
616
635
|
const client = createOpencodeClient({
|
|
617
636
|
baseUrl: baseUrl || 'http://localhost:4096',
|
|
618
637
|
});
|
|
@@ -624,10 +643,9 @@ export async function listOpenCodeSessionsFromServer(baseUrl) {
|
|
|
624
643
|
return sessions.map(s => ({
|
|
625
644
|
id: s.id,
|
|
626
645
|
title: s.title || 'Untitled',
|
|
627
|
-
status: s.status?.type || 'unknown',
|
|
628
646
|
directory: s.directory || '',
|
|
629
|
-
createdAt: s.created_at || 0,
|
|
630
|
-
lastActivity: s.updated_at || 0,
|
|
647
|
+
createdAt: s.created_at || s.time?.created || 0,
|
|
648
|
+
lastActivity: s.updated_at || s.time?.updated || 0,
|
|
631
649
|
}));
|
|
632
650
|
}
|
|
633
651
|
catch (error) {
|
|
@@ -640,7 +658,7 @@ export async function createOpenCodeSession(title = 'New session') {
|
|
|
640
658
|
const opencode = await initOpenCode();
|
|
641
659
|
if (!opencode) return null;
|
|
642
660
|
const result = await opencode.client.session.create({
|
|
643
|
-
|
|
661
|
+
title
|
|
644
662
|
});
|
|
645
663
|
if (result.error) {
|
|
646
664
|
return null;
|
|
@@ -664,7 +682,7 @@ export async function deleteOpenCodeSession(sessionId) {
|
|
|
664
682
|
const opencode = await initOpenCode();
|
|
665
683
|
if (!opencode) return false;
|
|
666
684
|
const result = await opencode.client.session.delete({
|
|
667
|
-
|
|
685
|
+
sessionID: sessionId
|
|
668
686
|
});
|
|
669
687
|
if (result.error) {
|
|
670
688
|
return false;
|
|
@@ -679,9 +697,9 @@ export async function deleteOpenCodeSession(sessionId) {
|
|
|
679
697
|
}
|
|
680
698
|
export async function renameOpenCodeSession(session, title) {
|
|
681
699
|
try {
|
|
682
|
-
const result = await session.client.session.
|
|
683
|
-
|
|
684
|
-
|
|
700
|
+
const result = await session.client.session.update({
|
|
701
|
+
sessionID: session.sessionId,
|
|
702
|
+
title,
|
|
685
703
|
});
|
|
686
704
|
if (result.error) {
|
|
687
705
|
return false;
|
|
@@ -699,9 +717,9 @@ export async function forkSession(sessionId, messageID, directory) {
|
|
|
699
717
|
const opencode = await initOpenCode();
|
|
700
718
|
if (!opencode) return null;
|
|
701
719
|
const result = await opencode.client.session.fork({
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
720
|
+
sessionID: sessionId,
|
|
721
|
+
messageID,
|
|
722
|
+
...(directory ? { directory } : {}),
|
|
705
723
|
});
|
|
706
724
|
if (result.error) {
|
|
707
725
|
console.warn(`Fork failed: ${result.error}`);
|
|
@@ -726,8 +744,9 @@ export async function revertSessionMessage(sessionId, messageID, partID) {
|
|
|
726
744
|
const opencode = await initOpenCode();
|
|
727
745
|
if (!opencode) return false;
|
|
728
746
|
const result = await opencode.client.session.revert({
|
|
729
|
-
|
|
730
|
-
|
|
747
|
+
sessionID: sessionId,
|
|
748
|
+
messageID,
|
|
749
|
+
partID,
|
|
731
750
|
});
|
|
732
751
|
if (result.error) {
|
|
733
752
|
console.warn(`Revert failed: ${result.error}`);
|
|
@@ -746,7 +765,7 @@ export async function unrevertSession(sessionId) {
|
|
|
746
765
|
const opencode = await initOpenCode();
|
|
747
766
|
if (!opencode) return false;
|
|
748
767
|
const result = await opencode.client.session.unrevert({
|
|
749
|
-
|
|
768
|
+
sessionID: sessionId
|
|
750
769
|
});
|
|
751
770
|
if (result.error) {
|
|
752
771
|
console.warn(`Unrevert failed: ${result.error}`);
|
|
@@ -765,9 +784,12 @@ export async function listProviders() {
|
|
|
765
784
|
try {
|
|
766
785
|
const opencode = await initOpenCode();
|
|
767
786
|
if (!opencode) return null;
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
787
|
+
// Config2.providers() → GET /config/providers (same as v1)
|
|
788
|
+
const result = await opencode.client.config.providers();
|
|
789
|
+
if (result.error) return null;
|
|
790
|
+
// v2 returns { providers: [...] }, v1 returns { all: [...] }
|
|
791
|
+
const data = result.data?.providers || result.data?.all || result.data || [];
|
|
792
|
+
return Array.isArray(data) ? data : null;
|
|
771
793
|
} catch (error) {
|
|
772
794
|
console.error('Failed to list providers:', error.message);
|
|
773
795
|
return null;
|
|
@@ -779,7 +801,7 @@ export async function updateGlobalModel(modelStr) {
|
|
|
779
801
|
const opencode = await initOpenCode();
|
|
780
802
|
if (!opencode) return false;
|
|
781
803
|
const result = await opencode.client.config.update({
|
|
782
|
-
|
|
804
|
+
config: { model: modelStr },
|
|
783
805
|
});
|
|
784
806
|
if (result.error) {
|
|
785
807
|
console.error('Failed to update model:', result.error);
|
|
@@ -24,9 +24,13 @@ export class ClaudeCodeAgentAdapter {
|
|
|
24
24
|
|
|
25
25
|
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
26
26
|
const projectDir = options.projectDir;
|
|
27
|
-
|
|
27
|
+
let cleanPrompt = prompt;
|
|
28
|
+
if (prompt.startsWith('-c')) {
|
|
29
|
+
cleanPrompt = prompt.slice(2).trim();
|
|
30
|
+
}
|
|
31
|
+
const contextualPrompt = this.buildContextualPrompt(cleanPrompt, history);
|
|
28
32
|
|
|
29
|
-
const args = ['--print', contextualPrompt];
|
|
33
|
+
const args = ['--print', '-c', contextualPrompt];
|
|
30
34
|
|
|
31
35
|
return this.callClaude(args, projectDir);
|
|
32
36
|
}
|
|
@@ -22,11 +22,15 @@ export class OpenCodeAgentAdapter {
|
|
|
22
22
|
});
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
async sendPrompt(_sessionId, prompt, history) {
|
|
26
|
-
|
|
25
|
+
async sendPrompt(_sessionId, prompt, history, options = {}) {
|
|
26
|
+
let cleanPrompt = prompt;
|
|
27
|
+
if (prompt.startsWith('-c')) {
|
|
28
|
+
cleanPrompt = prompt.slice(2).trim();
|
|
29
|
+
}
|
|
30
|
+
const contextualPrompt = this.buildContextualPrompt(cleanPrompt, history);
|
|
27
31
|
return this.callOpenCode(contextualPrompt);
|
|
28
32
|
}
|
|
29
|
-
|
|
33
|
+
|
|
30
34
|
buildContextualPrompt(prompt, history) {
|
|
31
35
|
if (!history || history.length === 0) return prompt;
|
|
32
36
|
const historyText = history
|
|
@@ -34,7 +38,7 @@ export class OpenCodeAgentAdapter {
|
|
|
34
38
|
.join('\n\n');
|
|
35
39
|
return `Previous conversation:\n${historyText}\n\nCurrent request: ${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)
|
|
@@ -48,7 +52,7 @@ export class OpenCodeAgentAdapter {
|
|
|
48
52
|
|
|
49
53
|
callOpenCode(prompt) {
|
|
50
54
|
return new Promise((resolve) => {
|
|
51
|
-
const proc = spawn('opencode', ['run', '--format', 'json', prompt], {
|
|
55
|
+
const proc = spawn('opencode', ['run', '--format', 'json', '-c', prompt], {
|
|
52
56
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
53
57
|
shell: true,
|
|
54
58
|
});
|
|
@@ -56,11 +60,33 @@ export class OpenCodeAgentAdapter {
|
|
|
56
60
|
let stdout = '';
|
|
57
61
|
let stderr = '';
|
|
58
62
|
let fullText = '';
|
|
63
|
+
let resigned = false;
|
|
64
|
+
|
|
65
|
+
const STUCK_PATTERNS = [
|
|
66
|
+
'Free usage exceeded', 'quota exceeded', 'rate limit',
|
|
67
|
+
'retrying in', 'retry attempt',
|
|
68
|
+
'429', '401', '403', '402', 'Payment Required',
|
|
69
|
+
'subscription required', 'insufficient_quota',
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
const checkStuck = (stderrText) => {
|
|
73
|
+
if (resigned) return;
|
|
74
|
+
for (const pattern of STUCK_PATTERNS) {
|
|
75
|
+
if (stderrText.toLowerCase().includes(pattern.toLowerCase())) {
|
|
76
|
+
resigned = true;
|
|
77
|
+
proc.kill();
|
|
78
|
+
const detail = this.extractErrorMessage('', stderrText);
|
|
79
|
+
resolve(`❌ OpenCode 无法继续: ${detail || pattern}`);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
59
85
|
|
|
60
86
|
proc.stdout?.on('data', (data) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
const chunk = data.toString();
|
|
88
|
+
stdout += chunk;
|
|
89
|
+
const lines = chunk.split('\n');
|
|
64
90
|
for (const line of lines) {
|
|
65
91
|
if (!line.trim()) continue;
|
|
66
92
|
try {
|
|
@@ -70,15 +96,19 @@ export class OpenCodeAgentAdapter {
|
|
|
70
96
|
}
|
|
71
97
|
});
|
|
72
98
|
|
|
73
|
-
proc.stderr?.on('data', (data) => {
|
|
99
|
+
proc.stderr?.on('data', (data) => {
|
|
100
|
+
stderr += data.toString();
|
|
101
|
+
checkStuck(stderr);
|
|
102
|
+
});
|
|
74
103
|
|
|
75
104
|
proc.on('close', (code) => {
|
|
105
|
+
if (resigned) return;
|
|
76
106
|
if (code !== 0) {
|
|
77
107
|
const detail = this.extractErrorMessage(stdout, stderr);
|
|
78
108
|
const hint = detail
|
|
79
109
|
? `: ${detail}`
|
|
80
110
|
: '。请运行 `opencode auth login` 配置认证。';
|
|
81
|
-
resolve(`❌ OpenCode
|
|
111
|
+
resolve(`❌ OpenCode 错误 (exit code ${code})${hint}`);
|
|
82
112
|
} else {
|
|
83
113
|
resolve(fullText || '完成');
|
|
84
114
|
}
|
package/dist/telegram/adapter.js
CHANGED
|
@@ -38,12 +38,9 @@ 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
46
|
for (const [, cmds] of groups) {
|
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') {
|