@yvhitxcel/opencode-remote 0.16.0 → 0.16.2
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/core/notifications.js +11 -0
- package/dist/core/router.js +139 -1
- package/dist/feishu/commands.js +28 -1
- package/dist/opencode/client.js +2 -0
- package/dist/plugins/agents/claude-code/index.js +46 -4
- package/dist/plugins/agents/codex/index.js +23 -1
- package/dist/plugins/agents/copilot/index.js +23 -1
- package/dist/plugins/agents/opencode/index.js +27 -5
- package/dist/telegram/adapter.js +22 -2
- package/dist/telegram/bot.js +18 -1
- package/dist/weixin/commands.js +28 -1
- package/package.json +1 -1
|
@@ -101,6 +101,17 @@ export const TEMPLATES = {
|
|
|
101
101
|
details: 'Changes were automatically rejected.',
|
|
102
102
|
}),
|
|
103
103
|
};
|
|
104
|
+
// Task completion notification with timing
|
|
105
|
+
export function formatTaskCompletion(taskName, startTime, extra) {
|
|
106
|
+
const elapsed = Date.now() - startTime;
|
|
107
|
+
const seconds = Math.floor(elapsed / 1000);
|
|
108
|
+
const timeStr = seconds >= 60 ? `${Math.floor(seconds / 60)}分${seconds % 60}秒` : `${seconds}秒`;
|
|
109
|
+
const lines = [`✅ 任务完成: ${taskName}`, '', `⏱️ 耗时: ${timeStr}`];
|
|
110
|
+
if (extra?.files && extra.files > 0) lines.push(`📄 修改文件: ${extra.files} 个`);
|
|
111
|
+
if (extra?.iterations && extra.iterations > 0) lines.push(`🔄 迭代次数: ${extra.iterations}`);
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
114
|
+
|
|
104
115
|
// Split message for Telegram's 4096 char limit
|
|
105
116
|
export function splitMessage(text, maxLength = 4000) {
|
|
106
117
|
if (text.length <= maxLength) {
|
package/dist/core/router.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
// Message router - full command definitions shared across all platforms
|
|
2
2
|
import { registry } from './registry.js';
|
|
3
3
|
import { initOpenCode, listProviders, updateGlobalModel, checkConnection, resumeSession, shareSession } from '../opencode/client.js';
|
|
4
|
+
import { formatTaskCompletion } from './notifications.js';
|
|
5
|
+
|
|
6
|
+
const demoModeMap = new Map();
|
|
7
|
+
|
|
8
|
+
export function setDemoMode(threadId, enabled) {
|
|
9
|
+
if (enabled) demoModeMap.set(threadId, true);
|
|
10
|
+
else demoModeMap.delete(threadId);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isDemoMode(threadId) {
|
|
14
|
+
return demoModeMap.has(threadId);
|
|
15
|
+
}
|
|
4
16
|
|
|
5
17
|
export const COMMAND_ALIASES = {
|
|
6
18
|
start: ['start'],
|
|
@@ -25,6 +37,34 @@ export const COMMAND_ALIASES = {
|
|
|
25
37
|
agents: ['agents'],
|
|
26
38
|
model: ['model'],
|
|
27
39
|
expert: ['expert', 'z', 'Z', 'review'],
|
|
40
|
+
tutorial: ['tutorial', 'guide', 'walkthrough'],
|
|
41
|
+
demo: ['demo', 'sandbox', 'preview'],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const DEMO_RESPONSES = {
|
|
45
|
+
start: '🚀 准备就绪,发送消息给 OpenCode 开始工作\n\n💡 这是演示模式,所有命令显示模拟输出',
|
|
46
|
+
get help() { return getHelpText(); },
|
|
47
|
+
status: '✅ OpenCode 在线\n✅ 七牛云 已配置\n✅ 会话: abc12345\n📁 项目目录: /home/user/my-project',
|
|
48
|
+
reset: '🔄 会话已重置,下次发送消息将创建新会话',
|
|
49
|
+
restart: '🔄 重启信号已发送,bot 即将重启...',
|
|
50
|
+
sessions: '📂 最近会话:\n\n1. Telegram 会话 (2分钟前)\n2. 微信开发会话 (15分钟前)\n3. 专家评审 (1小时前)',
|
|
51
|
+
delsessions: '🗑️ 选择要删除的会话(回复编号):\n\n1. Telegram 会话\n2. 微信开发会话\n\n回复编号删除',
|
|
52
|
+
loop: '🔄 循环任务已启动\n指令: 智能模式\n限制: 最多10次迭代或30分钟\n\n发送 /loop off 停止',
|
|
53
|
+
diagnose: '🔍 诊断报告\n\nOpenCode: ✅\n七牛云: ✅\nTelegram: ✅\n飞书: ❌ 未配置\n会话: ✅',
|
|
54
|
+
refresh: '✅ 会话已刷新',
|
|
55
|
+
copy: '📋 最新回复:\n\n这是 AI 的示例回复内容,演示 /copy 命令的功能。',
|
|
56
|
+
revert: '↩️ 已撤销最近的消息\n\n发送 /revert undo 恢复',
|
|
57
|
+
upload: '⬆️ 用法: /upload <文件路径>\n\n当前项目构建产物:\n📦 build/app.apk (12.5 MB)',
|
|
58
|
+
delete: '🗑️ 用法: /delete <key>\n\n示例: /delete uploads/1234567890-app.apk',
|
|
59
|
+
model: '🧠 可用模型:\n\nOpenAI (openai):\n gpt-4o\n gpt-4o-mini\n o3-mini\n\nAnthropic (anthropic):\n claude-sonnet-4-20250514\n\n用法: /model <provider/model>',
|
|
60
|
+
agents: '🤖 可用 AI Agent:\n\n✅ opencode\n✅ claude-code\n✅ codex\n❌ copilot\n\n切换: /oc /cc /cx /copilot',
|
|
61
|
+
oc: '✅ 已切换到 OpenCode\n\n💬 发送消息给 OpenCode 开始工作',
|
|
62
|
+
cc: '✅ 已切换到 Claude Code',
|
|
63
|
+
cx: '✅ 已切换到 Codex',
|
|
64
|
+
copilot: '✅ 已切换到 GitHub Copilot',
|
|
65
|
+
edit: '✏️ 用法: /edit <消息编号>\n\n选择要修改的消息,然后发送修正后的内容。',
|
|
66
|
+
expert: '🧠 专家评审模式已启动\n\n14 位 AI 专家正在分析您的项目...\n\n架构师、安全研究员、测试工程师、VC/投资人等角色将依次给出评审意见。',
|
|
67
|
+
tutorial: '📚 教程已启动\n发送 /tutorial 1 开始第1步',
|
|
28
68
|
};
|
|
29
69
|
|
|
30
70
|
export const EXPERT_SYSTEM_PROMPT = `你是一个专家评审系统。用户消息含触发词(z/c/叫全部专家/专家点评)时启动评审,前后可带具体问题则聚焦该问题。
|
|
@@ -134,6 +174,8 @@ const COMMAND_HELP = {
|
|
|
134
174
|
agents: '查看 Agent',
|
|
135
175
|
model: '切换模型',
|
|
136
176
|
expert: '专家评审(z/叫全部专家)',
|
|
177
|
+
tutorial: '交互式教程(step-by-step 上手)',
|
|
178
|
+
demo: '沙箱模式(无需配置体验全部命令)',
|
|
137
179
|
};
|
|
138
180
|
|
|
139
181
|
const COMMAND_MAP = {};
|
|
@@ -249,13 +291,107 @@ async function getSessionMessages(sessionId) {
|
|
|
249
291
|
return result.data || [];
|
|
250
292
|
}
|
|
251
293
|
|
|
294
|
+
export const TUTORIAL_STEPS = [
|
|
295
|
+
{
|
|
296
|
+
step: 1,
|
|
297
|
+
title: '💬 发送第一条消息',
|
|
298
|
+
desc: '直接发一条消息给 bot,比如:"帮我写一个 Hello World 程序"\nAI 会自动接收并在你的电脑上执行。',
|
|
299
|
+
action: '现在试试:输入 "你好" 或 "帮我写一个 Python 程序"',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
step: 2,
|
|
303
|
+
title: '📊 查看状态',
|
|
304
|
+
desc: '发送 /status 查看 OpenCode 是否在线、当前会话信息、运行中的任务。',
|
|
305
|
+
action: '试试:发送 /status',
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
step: 3,
|
|
309
|
+
title: '📋 复制 AI 回复',
|
|
310
|
+
desc: 'AI 回复了长篇代码?用 /copy 一键复制最新 AI 回复的内容。',
|
|
311
|
+
action: '试试:发送 /copy',
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
step: 4,
|
|
315
|
+
title: '🤖 切换 AI 模型',
|
|
316
|
+
desc: '不同模型擅长不同任务。用 /model 查看可用模型,/model provider/model 切换。',
|
|
317
|
+
action: '试试:发送 /model 查看列表',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
step: 5,
|
|
321
|
+
title: '🧠 召唤专家评审',
|
|
322
|
+
desc: '发送 /z 启动专家评审模式,14 位 AI 专家分析你的项目,自动出修复方案并执行。',
|
|
323
|
+
action: '试试:发送 /z,然后发送 z',
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
step: 6,
|
|
327
|
+
title: '🔄 循环任务',
|
|
328
|
+
desc: '让 AI 持续工作。发送 /loop 启动循环任务,AI 会反复推进项目。\n停止:/loop off',
|
|
329
|
+
action: '试试:发送 /loop 检查测试覆盖率',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
step: 7,
|
|
333
|
+
title: '🔍 系统诊断',
|
|
334
|
+
desc: '出问题了?/diagnose 一键检查 OpenCode、七牛云、各平台连接状态。',
|
|
335
|
+
action: '试试:发送 /diagnose',
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
step: 8,
|
|
339
|
+
title: '🎉 全部搞定',
|
|
340
|
+
desc: '你已经掌握了所有核心功能!\n下一步建议:\n• /help 查看全部 22 条命令\n• 设置你的项目目录并开始真正的开发\n• 尝试多 Agent 切换:/cc 用 Claude Code,/cx 用 Codex',
|
|
341
|
+
action: '',
|
|
342
|
+
},
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
function getTutorialText(step) {
|
|
346
|
+
const s = TUTORIAL_STEPS[step - 1];
|
|
347
|
+
if (!s) return getTutorialText(1);
|
|
348
|
+
let msg = `📚 教程 · 第 ${s.step}/${TUTORIAL_STEPS.length} 步\n━━━━━━━━━━━━━━━━\n\n${s.title}\n\n${s.desc}\n\n`;
|
|
349
|
+
if (s.action) msg += `👉 ${s.action}`;
|
|
350
|
+
msg += `\n\n回复 /tutorial${step < TUTORIAL_STEPS.length ? ` 继续第${step + 1}步\n发送 /tutorial ${step + 1}` : ''} 进入下一步`;
|
|
351
|
+
return msg;
|
|
352
|
+
}
|
|
353
|
+
|
|
252
354
|
export async function routeMessage(parsed, ctx) {
|
|
355
|
+
const threadId = ctx.threadId;
|
|
356
|
+
if (demoModeMap.has(threadId) && parsed.type === 'command' && DEMO_RESPONSES[parsed.command]) {
|
|
357
|
+
return DEMO_RESPONSES[parsed.command];
|
|
358
|
+
}
|
|
253
359
|
switch (parsed.type) {
|
|
254
360
|
case 'command': {
|
|
255
361
|
switch (parsed.command) {
|
|
256
362
|
case 'help':
|
|
257
363
|
return getHelpText();
|
|
258
364
|
|
|
365
|
+
case 'tutorial': {
|
|
366
|
+
const stepNum = parseInt(parsed.arg, 10);
|
|
367
|
+
const step = !isNaN(stepNum) && stepNum >= 1 && stepNum <= TUTORIAL_STEPS.length ? stepNum : 1;
|
|
368
|
+
return getTutorialText(step);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
case 'demo': {
|
|
372
|
+
const arg = (parsed.arg || '').trim().toLowerCase();
|
|
373
|
+
if (arg === 'off' || arg === 'exit' || arg === 'stop') {
|
|
374
|
+
setDemoMode(threadId, false);
|
|
375
|
+
return '⏹️ 已退出沙箱模式\n\n现在所有命令将正常连接 OpenCode 执行。';
|
|
376
|
+
}
|
|
377
|
+
setDemoMode(threadId, true);
|
|
378
|
+
let msg = '🎮 **沙箱模式已启动**\n\n';
|
|
379
|
+
msg += '在此模式下,所有命令返回模拟输出,无需连接 OpenCode。\n\n';
|
|
380
|
+
msg += '可用命令:\n';
|
|
381
|
+
const groups = [
|
|
382
|
+
['🟢 常用', ['/help', '/start', '/status', '/reset']],
|
|
383
|
+
['🔄 任务', ['/loop', '/refresh', '/diagnose']],
|
|
384
|
+
['🤖 AI', ['/model', '/agents', '/oc', '/cc']],
|
|
385
|
+
['📂 会话', ['/sessions', '/delsessions', '/copy', '/revert']],
|
|
386
|
+
['⬆️ 文件', ['/upload', '/delete']],
|
|
387
|
+
];
|
|
388
|
+
for (const [title, cmds] of groups) {
|
|
389
|
+
msg += `\n${title}\n ${cmds.join(' ')}\n`;
|
|
390
|
+
}
|
|
391
|
+
msg += '\n试试发送上面的命令体验效果!\n发送 /demo off 退出沙箱模式';
|
|
392
|
+
return msg;
|
|
393
|
+
}
|
|
394
|
+
|
|
259
395
|
case 'agents': {
|
|
260
396
|
const agents = registry.listAgents();
|
|
261
397
|
const lines = ['🤖 可用 AI Agent:'];
|
|
@@ -411,8 +547,10 @@ export async function routeMessage(parsed, ctx) {
|
|
|
411
547
|
if (!agent) return '❌ OpenCode agent not found';
|
|
412
548
|
const available = await agent.isAvailable().catch(() => false);
|
|
413
549
|
if (!available) return '❌ OpenCode 不可用';
|
|
550
|
+
const taskStart = Date.now();
|
|
414
551
|
const response = await agent.sendPrompt(ctx.threadId || 'expert-review', EXPERT_SYSTEM_PROMPT + '\n\n用户问题:' + (parsed.arg || '请评审当前项目'), []);
|
|
415
|
-
|
|
552
|
+
const notification = response ? '' : `\n\n${formatTaskCompletion('专家评审', taskStart)}`;
|
|
553
|
+
return (response || '无响应') + notification;
|
|
416
554
|
}
|
|
417
555
|
|
|
418
556
|
default:
|
package/dist/feishu/commands.js
CHANGED
|
@@ -3,7 +3,7 @@ import { splitMessage } from '../core/notifications.js';
|
|
|
3
3
|
import { EMOJI } from '../core/types.js';
|
|
4
4
|
import { initOpenCode, createSession, sendMessage, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, listProviders, updateGlobalModel } from '../opencode/client.js';
|
|
5
5
|
import { claimOwnership } from '../core/auth.js';
|
|
6
|
-
import { COMMAND_ALIASES, detectCommand, getHelpText } from '../core/router.js';
|
|
6
|
+
import { COMMAND_ALIASES, detectCommand, getHelpText, DEMO_RESPONSES, setDemoMode, isDemoMode } from '../core/router.js';
|
|
7
7
|
import { registry } from '../core/registry.js';
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
9
9
|
import { join, basename } from 'path';
|
|
@@ -78,6 +78,18 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
78
78
|
case 'help':
|
|
79
79
|
await adapter.reply(ctx.threadId, getHelpText());
|
|
80
80
|
return true;
|
|
81
|
+
case 'tutorial': {
|
|
82
|
+
const { TUTORIAL_STEPS } = await import('../core/router.js');
|
|
83
|
+
const stepNum = parseInt(arg, 10);
|
|
84
|
+
const step = !isNaN(stepNum) && stepNum >= 1 && stepNum <= TUTORIAL_STEPS.length ? stepNum : 1;
|
|
85
|
+
const s = TUTORIAL_STEPS[step - 1];
|
|
86
|
+
let msg = `📚 教程 · 第 ${s.step}/${TUTORIAL_STEPS.length} 步\n━━━━━━━━━━━━━━━━\n\n${s.title}\n\n${s.desc}\n\n`;
|
|
87
|
+
if (s.action) msg += `👉 ${s.action}`;
|
|
88
|
+
msg += `\n\n回复 /tutorial${step < TUTORIAL_STEPS.length ? ` 继续第${step + 1}步` : ''} 进入下一步`;
|
|
89
|
+
const msgs = splitMessage(msg);
|
|
90
|
+
for (const m of msgs) await adapter.reply(ctx.threadId, m);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
81
93
|
case 'agents': {
|
|
82
94
|
const agents = registry.listAgents();
|
|
83
95
|
const lines = ['🤖 可用 AI Agent:'];
|
|
@@ -548,6 +560,21 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
548
560
|
|
|
549
561
|
|
|
550
562
|
|
|
563
|
+
case 'demo': {
|
|
564
|
+
const argText = (arg || '').trim().toLowerCase();
|
|
565
|
+
if (argText === 'off' || argText === 'exit' || argText === 'stop') {
|
|
566
|
+
setDemoMode(ctx.threadId, false);
|
|
567
|
+
await adapter.reply(ctx.threadId, '⏹️ 已退出沙箱模式');
|
|
568
|
+
return true;
|
|
569
|
+
}
|
|
570
|
+
setDemoMode(ctx.threadId, true);
|
|
571
|
+
let msg = '🎮 沙箱模式已启动\n\n在此模式下所有命令返回模拟输出,无需连接 OpenCode。\n\n';
|
|
572
|
+
msg += '试试发送: /help /status /model /agents /loop /copy\n';
|
|
573
|
+
msg += '发送 /demo off 退出';
|
|
574
|
+
await adapter.reply(ctx.threadId, msg);
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
551
578
|
case 'diagnose': {
|
|
552
579
|
const { checkConnection } = await import('../opencode/client.js');
|
|
553
580
|
const diag = ['🔍 诊断报告\n'];
|
package/dist/opencode/client.js
CHANGED
|
@@ -398,6 +398,7 @@ export async function sendMessage(session, message, callbacks) {
|
|
|
398
398
|
let responseText = '';
|
|
399
399
|
let hasToolActivity = false;
|
|
400
400
|
let idleSince = 0; // 最后一次收到新内容的时间戳
|
|
401
|
+
let lastStatus = '';
|
|
401
402
|
|
|
402
403
|
while (Date.now() - startTime < TIMEOUT_MS) {
|
|
403
404
|
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
@@ -454,6 +455,7 @@ export async function sendMessage(session, message, callbacks) {
|
|
|
454
455
|
if (latestStatus === 'thinking' || latestStatus === 'pending_tool') {
|
|
455
456
|
idleSince = Date.now();
|
|
456
457
|
}
|
|
458
|
+
if (latestStatus) lastStatus = latestStatus;
|
|
457
459
|
if (latestStatus && latestStatus !== lastReportedStatus) {
|
|
458
460
|
lastReportedStatus = latestStatus;
|
|
459
461
|
console.log(`[AI状态] ${latestStatus}`);
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { platform } from 'os';
|
|
4
4
|
|
|
5
|
+
const LIUV_CRASH_PATTERNS = [
|
|
6
|
+
'Assertion failed',
|
|
7
|
+
'UV_HANDLE_CLOSING',
|
|
8
|
+
'src\\win\\async.c',
|
|
9
|
+
'libuv',
|
|
10
|
+
];
|
|
11
|
+
|
|
5
12
|
export class ClaudeCodeAgentAdapter {
|
|
6
13
|
name = 'claude-code';
|
|
7
14
|
aliases = ['cc', 'claude'];
|
|
@@ -19,9 +26,7 @@ export class ClaudeCodeAgentAdapter {
|
|
|
19
26
|
const projectDir = options.projectDir;
|
|
20
27
|
const contextualPrompt = this.buildContextualPrompt(prompt, history);
|
|
21
28
|
|
|
22
|
-
// 构建命令参数
|
|
23
29
|
const args = ['--print', contextualPrompt];
|
|
24
|
-
// `cwd` is set via spawn opts below, no need for --project flag
|
|
25
30
|
|
|
26
31
|
return this.callClaude(args, projectDir);
|
|
27
32
|
}
|
|
@@ -32,13 +37,40 @@ export class ClaudeCodeAgentAdapter {
|
|
|
32
37
|
return `Previous:\n${historyText}\n\n${prompt}`;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
isCrashNoise(line) {
|
|
41
|
+
return LIUV_CRASH_PATTERNS.some(p => line.includes(p));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
extractErrorMessage(stdout, stderr, code) {
|
|
45
|
+
// 先检查 stdout:--print 模式把错误也输出到 stdout
|
|
46
|
+
const stdoutLines = stdout.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
|
47
|
+
const stdoutErrors = stdoutLines.filter(l => !this.isCrashNoise(l));
|
|
48
|
+
if (stdoutErrors.length > 0) {
|
|
49
|
+
return stdoutErrors.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 再检查 stderr
|
|
53
|
+
const stderrLines = stderr.trim().split('\n').map(l => l.trim()).filter(Boolean);
|
|
54
|
+
const stderrReal = stderrLines.filter(l => !this.isCrashNoise(l));
|
|
55
|
+
if (stderrReal.length > 0) {
|
|
56
|
+
return stderrReal.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 如果全是崩溃噪音,尝试从任一流中找 Error 关键词
|
|
60
|
+
const all = [...stdoutLines, ...stderrLines];
|
|
61
|
+
const firstRelevant = all.find(l => /Error|error|ERROR|^\d{3}/.test(l));
|
|
62
|
+
if (firstRelevant) return firstRelevant;
|
|
63
|
+
|
|
64
|
+
// 兜底
|
|
65
|
+
return `进程异常退出 (code: ${code})`;
|
|
66
|
+
}
|
|
67
|
+
|
|
35
68
|
callClaude(args, projectDir) {
|
|
36
69
|
return new Promise((resolve) => {
|
|
37
70
|
const opts = {
|
|
38
71
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
39
72
|
shell: true,
|
|
40
73
|
};
|
|
41
|
-
// 如果指定了项目目录,在该目录下执行
|
|
42
74
|
if (projectDir) {
|
|
43
75
|
opts.cwd = projectDir;
|
|
44
76
|
}
|
|
@@ -57,7 +89,17 @@ export class ClaudeCodeAgentAdapter {
|
|
|
57
89
|
proc.on('close', (code) => {
|
|
58
90
|
clearTimeout(timeout);
|
|
59
91
|
console.log(`[claude-code] Process exited with code ${code}, stdout=${stdout.length} bytes, stderr=${stderr.length} bytes`);
|
|
60
|
-
|
|
92
|
+
|
|
93
|
+
if (code === 0) {
|
|
94
|
+
resolve(stdout.trim());
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const errorMsg = this.extractErrorMessage(stdout, stderr, code);
|
|
99
|
+
console.log(`[claude-code] Process failed, raw stderr:\n${stderr.trim().slice(-1000)}`);
|
|
100
|
+
console.log(`[claude-code] Error detail:\n${errorMsg}`);
|
|
101
|
+
|
|
102
|
+
resolve(`❌ Claude Code 错误 (exit code ${code}): ${errorMsg}`);
|
|
61
103
|
});
|
|
62
104
|
proc.on('error', (err) => {
|
|
63
105
|
clearTimeout(timeout);
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { platform } from 'os';
|
|
4
4
|
|
|
5
|
+
const CRASH_PATTERNS = [
|
|
6
|
+
'Assertion failed',
|
|
7
|
+
'UV_HANDLE_CLOSING',
|
|
8
|
+
'src\\win\\async.c',
|
|
9
|
+
'libuv',
|
|
10
|
+
];
|
|
11
|
+
|
|
5
12
|
export class CodexAgentAdapter {
|
|
6
13
|
name = 'codex';
|
|
7
14
|
aliases = ['cx'];
|
|
@@ -25,6 +32,16 @@ export class CodexAgentAdapter {
|
|
|
25
32
|
const historyText = history.map(msg => `[${msg.role}]: ${msg.content}`).join('\n\n');
|
|
26
33
|
return `Context:\n${historyText}\n\n${prompt}`;
|
|
27
34
|
}
|
|
35
|
+
|
|
36
|
+
extractErrorMessage(stdout, stderr) {
|
|
37
|
+
const lines = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
|
|
38
|
+
.map(l => l.trim()).filter(Boolean)
|
|
39
|
+
.filter(l => !CRASH_PATTERNS.some(p => l.includes(p)));
|
|
40
|
+
if (lines.length > 0) return lines.join('\n');
|
|
41
|
+
const first = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
|
|
42
|
+
.find(l => /Error|error|ERROR|^\d{3}/.test(l));
|
|
43
|
+
return first || null;
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
callCodex(prompt) {
|
|
30
47
|
return new Promise((resolve) => {
|
|
@@ -37,7 +54,12 @@ export class CodexAgentAdapter {
|
|
|
37
54
|
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
38
55
|
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
39
56
|
proc.on('close', (code) => {
|
|
40
|
-
|
|
57
|
+
if (code === 0) {
|
|
58
|
+
resolve(stdout.trim());
|
|
59
|
+
} else {
|
|
60
|
+
const detail = this.extractErrorMessage(stdout, stderr);
|
|
61
|
+
resolve(`❌ Codex 错误${detail ? `: ${detail}` : ''}`);
|
|
62
|
+
}
|
|
41
63
|
});
|
|
42
64
|
});
|
|
43
65
|
}
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { platform } from 'os';
|
|
4
4
|
|
|
5
|
+
const CRASH_PATTERNS = [
|
|
6
|
+
'Assertion failed',
|
|
7
|
+
'UV_HANDLE_CLOSING',
|
|
8
|
+
'src\\win\\async.c',
|
|
9
|
+
'libuv',
|
|
10
|
+
];
|
|
11
|
+
|
|
5
12
|
export class CopilotAgentAdapter {
|
|
6
13
|
name = 'copilot';
|
|
7
14
|
aliases = ['copilot-cli', 'copilot'];
|
|
@@ -25,6 +32,16 @@ export class CopilotAgentAdapter {
|
|
|
25
32
|
const historyText = history.map(msg => `[${msg.role}]: ${msg.content}`).join('\n\n');
|
|
26
33
|
return `Context:\n${historyText}\n\n${prompt}`;
|
|
27
34
|
}
|
|
35
|
+
|
|
36
|
+
extractErrorMessage(stdout, stderr) {
|
|
37
|
+
const lines = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
|
|
38
|
+
.map(l => l.trim()).filter(Boolean)
|
|
39
|
+
.filter(l => !CRASH_PATTERNS.some(p => l.includes(p)));
|
|
40
|
+
if (lines.length > 0) return lines.join('\n');
|
|
41
|
+
const first = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
|
|
42
|
+
.find(l => /Error|error|ERROR|^\d{3}/.test(l));
|
|
43
|
+
return first || null;
|
|
44
|
+
}
|
|
28
45
|
|
|
29
46
|
callCopilot(prompt) {
|
|
30
47
|
return new Promise((resolve) => {
|
|
@@ -37,7 +54,12 @@ export class CopilotAgentAdapter {
|
|
|
37
54
|
proc.stdout?.on('data', (data) => { stdout += data.toString(); });
|
|
38
55
|
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
39
56
|
proc.on('close', (code) => {
|
|
40
|
-
|
|
57
|
+
if (code === 0) {
|
|
58
|
+
resolve(stdout.trim());
|
|
59
|
+
} else {
|
|
60
|
+
const detail = this.extractErrorMessage(stdout, stderr);
|
|
61
|
+
resolve(`❌ Copilot 错误${detail ? `: ${detail}` : ''}`);
|
|
62
|
+
}
|
|
41
63
|
});
|
|
42
64
|
});
|
|
43
65
|
}
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import { spawn } from 'child_process';
|
|
3
3
|
import { platform } from 'os';
|
|
4
4
|
|
|
5
|
+
const CRASH_PATTERNS = [
|
|
6
|
+
'Assertion failed',
|
|
7
|
+
'UV_HANDLE_CLOSING',
|
|
8
|
+
'src\\win\\async.c',
|
|
9
|
+
'libuv',
|
|
10
|
+
];
|
|
11
|
+
|
|
5
12
|
export class OpenCodeAgentAdapter {
|
|
6
13
|
name = 'opencode';
|
|
7
14
|
aliases = ['oc', 'opencodeai'];
|
|
@@ -28,17 +35,28 @@ export class OpenCodeAgentAdapter {
|
|
|
28
35
|
return `Previous conversation:\n${historyText}\n\nCurrent request: ${prompt}`;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
extractErrorMessage(stdout, stderr) {
|
|
39
|
+
const lines = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
|
|
40
|
+
.map(l => l.trim()).filter(Boolean)
|
|
41
|
+
.filter(l => !CRASH_PATTERNS.some(p => l.includes(p)));
|
|
42
|
+
|
|
43
|
+
if (lines.length > 0) return lines.join('\n');
|
|
44
|
+
const first = [...stdout.trim().split('\n'), ...stderr.trim().split('\n')]
|
|
45
|
+
.find(l => /Error|error|ERROR|^\d{3}/.test(l));
|
|
46
|
+
return first || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
31
49
|
callOpenCode(prompt) {
|
|
32
50
|
return new Promise((resolve) => {
|
|
33
51
|
const proc = spawn('opencode', ['run', '--format', 'json', prompt], {
|
|
34
52
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
53
|
shell: true,
|
|
36
54
|
});
|
|
37
|
-
|
|
55
|
+
|
|
38
56
|
let stdout = '';
|
|
39
57
|
let stderr = '';
|
|
40
58
|
let fullText = '';
|
|
41
|
-
|
|
59
|
+
|
|
42
60
|
proc.stdout?.on('data', (data) => {
|
|
43
61
|
stdout += data.toString();
|
|
44
62
|
const lines = stdout.split('\n');
|
|
@@ -51,12 +69,16 @@ export class OpenCodeAgentAdapter {
|
|
|
51
69
|
} catch {}
|
|
52
70
|
}
|
|
53
71
|
});
|
|
54
|
-
|
|
72
|
+
|
|
55
73
|
proc.stderr?.on('data', (data) => { stderr += data.toString(); });
|
|
56
|
-
|
|
74
|
+
|
|
57
75
|
proc.on('close', (code) => {
|
|
58
76
|
if (code !== 0) {
|
|
59
|
-
|
|
77
|
+
const detail = this.extractErrorMessage(stdout, stderr);
|
|
78
|
+
const hint = detail
|
|
79
|
+
? `: ${detail}`
|
|
80
|
+
: '。请运行 `opencode auth login` 配置认证。';
|
|
81
|
+
resolve(`❌ OpenCode 错误${hint}`);
|
|
60
82
|
} else {
|
|
61
83
|
resolve(fullText || '完成');
|
|
62
84
|
}
|
package/dist/telegram/adapter.js
CHANGED
|
@@ -29,10 +29,30 @@ export class TelegramAdapter {
|
|
|
29
29
|
|
|
30
30
|
onMessage(handler) { this.messageHandler = handler; }
|
|
31
31
|
|
|
32
|
-
async sendMessage(threadId, text) {
|
|
32
|
+
async sendMessage(threadId, text, opts = {}) {
|
|
33
33
|
if (!this.bot) throw new Error('Telegram adapter not started');
|
|
34
34
|
const chunks = splitMessage(text, { maxLength: 4000, addContinuationMarker: false });
|
|
35
|
-
for (const chunk of chunks) await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'Markdown' });
|
|
35
|
+
for (const chunk of chunks) await this.bot.api.sendMessage(threadId, chunk, { parse_mode: 'Markdown', ...opts });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async sendCommandMenu(threadId, title) {
|
|
39
|
+
if (!this.bot) return;
|
|
40
|
+
const groups = [
|
|
41
|
+
['🟢 常用', ['/help', '/status', '/start', '/reset']],
|
|
42
|
+
['🔄 任务', ['/loop', '/refresh', '/restart']],
|
|
43
|
+
['🤖 AI', ['/model', '/agents', '/oc', '/cc']],
|
|
44
|
+
['🧠 专家', ['/tutorial', '/z', '/diagnose']],
|
|
45
|
+
['📂 会话', ['/sessions', '/delsessions', '/copy', '/revert']],
|
|
46
|
+
['⬆️ 文件', ['/upload', '/delete']],
|
|
47
|
+
];
|
|
48
|
+
const keyboard = [];
|
|
49
|
+
for (const [, cmds] of groups) {
|
|
50
|
+
const row = cmds.map(cmd => ({ text: cmd, callback_data: `cmd:${cmd.slice(1)}` }));
|
|
51
|
+
keyboard.push(row);
|
|
52
|
+
}
|
|
53
|
+
await this.bot.api.sendMessage(threadId, title || '📱 选择指令:', {
|
|
54
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
55
|
+
});
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
async sendTyping(threadId, isTyping) {
|
package/dist/telegram/bot.js
CHANGED
|
@@ -44,6 +44,15 @@ export async function startBot() {
|
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
+
telegramAdapter.bot.on('callback_query:data', async (ctx) => {
|
|
48
|
+
if (!ctx.callbackQuery.data.startsWith('cmd:')) return;
|
|
49
|
+
const cmd = ctx.callbackQuery.data.slice(4);
|
|
50
|
+
try {
|
|
51
|
+
await ctx.answerCallbackQuery({ text: `执行: /${cmd}` });
|
|
52
|
+
const msg = await telegramAdapter.bot.api.sendMessage(ctx.chat.id, `/${cmd}`);
|
|
53
|
+
} catch (e) { console.error('[Telegram] callback error:', e.message); }
|
|
54
|
+
});
|
|
55
|
+
|
|
47
56
|
telegramAdapter.bot.start().catch((err) => {
|
|
48
57
|
if (telegramAdapter.isRunning) console.error('[Telegram] Polling error:', err);
|
|
49
58
|
});
|
|
@@ -85,6 +94,7 @@ export async function startBot() {
|
|
|
85
94
|
}
|
|
86
95
|
opencodeSessionId = session.sessionId;
|
|
87
96
|
|
|
97
|
+
const taskStart = Date.now();
|
|
88
98
|
const response = await sendToOpenCode(session, parsed.prompt, {
|
|
89
99
|
onTextDelta: () => {},
|
|
90
100
|
onEvent: (event) => {
|
|
@@ -101,6 +111,8 @@ export async function startBot() {
|
|
|
101
111
|
if (chunk.trim()) await telegramAdapter.sendMessage(message.threadId, chunk);
|
|
102
112
|
}
|
|
103
113
|
}
|
|
114
|
+
const { formatTaskCompletion } = await import('../core/notifications.js');
|
|
115
|
+
await telegramAdapter.sendMessage(message.threadId, formatTaskCompletion('AI 任务', taskStart));
|
|
104
116
|
return;
|
|
105
117
|
}
|
|
106
118
|
|
|
@@ -111,7 +123,12 @@ export async function startBot() {
|
|
|
111
123
|
|
|
112
124
|
await telegramAdapter.sendTyping(message.threadId, false);
|
|
113
125
|
if (typeof result === 'string') {
|
|
114
|
-
|
|
126
|
+
if (parsed.type === 'command' && (parsed.command === 'help' || parsed.command === 'start')) {
|
|
127
|
+
await telegramAdapter.sendMessage(message.threadId, result);
|
|
128
|
+
await telegramAdapter.sendCommandMenu(message.threadId, '📱 快速选择指令:');
|
|
129
|
+
} else {
|
|
130
|
+
await telegramAdapter.sendMessage(message.threadId, result);
|
|
131
|
+
}
|
|
115
132
|
} else if (result) {
|
|
116
133
|
let full = '';
|
|
117
134
|
for await (const chunk of result) full += chunk;
|
package/dist/weixin/commands.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { detectCommand, COMMAND_ALIASES, getHelpText } from '../core/router.js';
|
|
1
|
+
import { detectCommand, COMMAND_ALIASES, getHelpText, DEMO_RESPONSES, setDemoMode, isDemoMode } from '../core/router.js';
|
|
2
2
|
import { getOrCreateSession, saveSessionMapping, sessionManager } from '../core/session.js';
|
|
3
3
|
import { splitMessage } from '../core/notifications.js';
|
|
4
4
|
import { initOpenCode, checkConnection, abortSession, resumeSession, revertSessionMessage, unrevertSession, listProviders, updateGlobalModel } from '../opencode/client.js';
|
|
@@ -93,6 +93,18 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
93
93
|
case 'help':
|
|
94
94
|
await adapter.reply(ctx.threadId, getHelpText());
|
|
95
95
|
return true;
|
|
96
|
+
case 'tutorial': {
|
|
97
|
+
const { TUTORIAL_STEPS } = await import('../core/router.js');
|
|
98
|
+
const stepNum = parseInt(arg, 10);
|
|
99
|
+
const step = !isNaN(stepNum) && stepNum >= 1 && stepNum <= TUTORIAL_STEPS.length ? stepNum : 1;
|
|
100
|
+
const s = TUTORIAL_STEPS[step - 1];
|
|
101
|
+
let msg = `📚 教程 · 第 ${s.step}/${TUTORIAL_STEPS.length} 步\n━━━━━━━━━━━━━━━━\n\n${s.title}\n\n${s.desc}\n\n`;
|
|
102
|
+
if (s.action) msg += `👉 ${s.action}`;
|
|
103
|
+
msg += `\n\n回复 /tutorial${step < TUTORIAL_STEPS.length ? ` 继续第${step + 1}步` : ''} 进入下一步`;
|
|
104
|
+
const msgs = splitMessage(msg);
|
|
105
|
+
for (const m of msgs) await adapter.reply(ctx.threadId, m);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
96
108
|
case 'status': {
|
|
97
109
|
const connected = await checkConnection();
|
|
98
110
|
const running = session.taskStartTime ? Math.round((Date.now() - session.taskStartTime) / 1000) : 0;
|
|
@@ -698,6 +710,21 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
698
710
|
|
|
699
711
|
|
|
700
712
|
|
|
713
|
+
case 'demo': {
|
|
714
|
+
const argText = (arg || '').trim().toLowerCase();
|
|
715
|
+
if (argText === 'off' || argText === 'exit' || argText === 'stop') {
|
|
716
|
+
setDemoMode(ctx.threadId, false);
|
|
717
|
+
await adapter.reply(ctx.threadId, '⏹️ 已退出沙箱模式');
|
|
718
|
+
return true;
|
|
719
|
+
}
|
|
720
|
+
setDemoMode(ctx.threadId, true);
|
|
721
|
+
let msg = '🎮 沙箱模式已启动\n\n在此模式下所有命令返回模拟输出,无需连接 OpenCode。\n\n';
|
|
722
|
+
msg += '试试发送: /help /status /model /agents /loop /copy\n';
|
|
723
|
+
msg += '发送 /demo off 退出';
|
|
724
|
+
await adapter.reply(ctx.threadId, msg);
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
|
|
701
728
|
case 'diagnose': {
|
|
702
729
|
const { checkConnection } = await import('../opencode/client.js');
|
|
703
730
|
const diag = ['🔍 诊断报告\n'];
|
package/package.json
CHANGED