@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
package/dist/core/lru.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// LRU + TTL Map for session storage
|
|
2
|
+
// - Caps total size to prevent memory leaks
|
|
3
|
+
// - Evicts least recently used when over capacity
|
|
4
|
+
// - Auto-expires entries that haven't been touched in TTL ms
|
|
5
|
+
export class LRUSessionMap {
|
|
6
|
+
constructor({ maxSize = 100, ttlMs = 30 * 60 * 1000, name = 'sessions' } = {}) {
|
|
7
|
+
this.maxSize = maxSize;
|
|
8
|
+
this.ttlMs = ttlMs;
|
|
9
|
+
this.name = name;
|
|
10
|
+
this._map = new Map(); // key -> { value, lastUsed }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
_isExpired(entry) {
|
|
14
|
+
return Date.now() - entry.lastUsed > this.ttlMs;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
_evictExpired() {
|
|
18
|
+
for (const [k, v] of this._map.entries()) {
|
|
19
|
+
if (this._isExpired(v)) this._map.delete(k);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(key) {
|
|
24
|
+
const entry = this._map.get(key);
|
|
25
|
+
if (!entry) return undefined;
|
|
26
|
+
if (this._isExpired(entry)) {
|
|
27
|
+
this._map.delete(key);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
entry.lastUsed = Date.now();
|
|
31
|
+
// 移到队尾 (LRU)
|
|
32
|
+
this._map.delete(key);
|
|
33
|
+
this._map.set(key, entry);
|
|
34
|
+
return entry.value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
set(key, value) {
|
|
38
|
+
if (this._map.has(key)) this._map.delete(key);
|
|
39
|
+
this._map.set(key, { value, lastUsed: Date.now() });
|
|
40
|
+
// 超上限淘汰最旧
|
|
41
|
+
while (this._map.size > this.maxSize) {
|
|
42
|
+
const oldest = this._map.keys().next().value;
|
|
43
|
+
this._map.delete(oldest);
|
|
44
|
+
console.log(`[lru:${this.name}] evicted ${oldest} (size > ${this.maxSize})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
delete(key) {
|
|
49
|
+
return this._map.delete(key);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
has(key) {
|
|
53
|
+
const entry = this._map.get(key);
|
|
54
|
+
if (!entry) return false;
|
|
55
|
+
if (this._isExpired(entry)) {
|
|
56
|
+
this._map.delete(key);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get size() {
|
|
63
|
+
return this._map.size;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
keys() {
|
|
67
|
+
this._evictExpired();
|
|
68
|
+
return this._map.keys();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
values() {
|
|
72
|
+
this._evictExpired();
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const v of this._map.values()) out.push(v.value);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
entries() {
|
|
79
|
+
this._evictExpired();
|
|
80
|
+
const out = [];
|
|
81
|
+
for (const [k, v] of this._map.entries()) out.push([k, v.value]);
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
clear() {
|
|
86
|
+
this._map.clear();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 定期清理 (调用方负责 setInterval)
|
|
90
|
+
cleanup() {
|
|
91
|
+
const before = this._map.size;
|
|
92
|
+
this._evictExpired();
|
|
93
|
+
const after = this._map.size;
|
|
94
|
+
if (before !== after) {
|
|
95
|
+
console.log(`[lru:${this.name}] cleaned ${before - after} expired entries`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -53,7 +53,7 @@ export const TEMPLATES = {
|
|
|
53
53
|
botStarted: () => formatNotification({
|
|
54
54
|
type: 'started',
|
|
55
55
|
title: 'OpenCode Remote Control ready',
|
|
56
|
-
actions: ['💬 Send a prompt to start', '/help — commands', '/
|
|
56
|
+
actions: ['💬 Send a prompt to start', '/help — commands', '/diagnose — diagnostics']
|
|
57
57
|
}),
|
|
58
58
|
sessionExpired: () => formatNotification({
|
|
59
59
|
type: 'expired',
|
|
@@ -81,7 +81,7 @@ export const TEMPLATES = {
|
|
|
81
81
|
type: 'error',
|
|
82
82
|
title: 'OpenCode is offline',
|
|
83
83
|
details: 'Cannot connect to OpenCode server.',
|
|
84
|
-
actions: ['🔄 /retry — check again', '/
|
|
84
|
+
actions: ['🔄 /retry — check again', '/diagnose — diagnostics']
|
|
85
85
|
}),
|
|
86
86
|
thinking: () => formatNotification({
|
|
87
87
|
type: 'loading',
|
package/dist/core/qiniu.js
CHANGED
|
@@ -238,10 +238,10 @@ export function findBuildOutputs(projectDir, maxDepth = 5) {
|
|
|
238
238
|
time: stat.mtime.getTime(),
|
|
239
239
|
relativePath: fullPath.replace(projectDir, '').replace(/^[\\\/]/, '')
|
|
240
240
|
});
|
|
241
|
-
} catch (e) {}
|
|
241
|
+
} catch (e) { console.debug('[qiniu] stat error:', e.message); }
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
-
} catch (e) {}
|
|
244
|
+
} catch (e) { console.debug('[qiniu] scan error:', e.message); }
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
for (const pattern of searchDirPatterns) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Retry helper — exponential backoff for transient errors
|
|
2
|
+
const TRANSIENT_PATTERNS = [
|
|
3
|
+
'AbortError', 'aborted',
|
|
4
|
+
'ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE',
|
|
5
|
+
'fetch failed', 'socket hang up',
|
|
6
|
+
'rate limit', '429', '502', '503', '504',
|
|
7
|
+
'Free usage exceeded', 'quota exceeded',
|
|
8
|
+
'retry attempt', 'retrying in',
|
|
9
|
+
'insufficient_quota', 'Payment Required',
|
|
10
|
+
'timeout', 'Timeout',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export function isTransientError(err) {
|
|
14
|
+
if (!err) return false;
|
|
15
|
+
const msg = (err.message || String(err)).toLowerCase();
|
|
16
|
+
return TRANSIENT_PATTERNS.some(p => msg.includes(p.toLowerCase()));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Retry an async function with exponential backoff.
|
|
21
|
+
* Only retries on transient errors. Resolves on first success.
|
|
22
|
+
* @template T
|
|
23
|
+
* @param {() => Promise<T>} fn
|
|
24
|
+
* @param {object} [opts]
|
|
25
|
+
* @param {number} [opts.maxAttempts=3]
|
|
26
|
+
* @param {number} [opts.baseDelayMs=1000]
|
|
27
|
+
* @param {number} [opts.maxDelayMs=8000]
|
|
28
|
+
* @param {(err: any, attempt: number, nextDelay: number) => void} [opts.onRetry]
|
|
29
|
+
* @returns {Promise<T>}
|
|
30
|
+
*/
|
|
31
|
+
export async function retryTransient(fn, opts = {}) {
|
|
32
|
+
const { maxAttempts = 3, baseDelayMs = 1000, maxDelayMs = 8000, onRetry } = opts;
|
|
33
|
+
let lastErr;
|
|
34
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
35
|
+
try {
|
|
36
|
+
return await fn();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
lastErr = err;
|
|
39
|
+
if (attempt >= maxAttempts || !isTransientError(err)) throw err;
|
|
40
|
+
const delay = Math.min(maxDelayMs, baseDelayMs * Math.pow(2, attempt - 1));
|
|
41
|
+
if (onRetry) onRetry(err, attempt, delay);
|
|
42
|
+
await new Promise(r => setTimeout(r, delay));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw lastErr;
|
|
46
|
+
}
|
package/dist/core/router.js
CHANGED
|
@@ -1,70 +1,32 @@
|
|
|
1
1
|
// Message router - full command definitions shared across all platforms
|
|
2
2
|
import { registry } from './registry.js';
|
|
3
|
-
import { initOpenCode, checkConnection,
|
|
3
|
+
import { initOpenCode, checkConnection, setThreadModel, getThreadModel, getRecentModels, setRawDebug, isRawDebug } from '../opencode/client.js';
|
|
4
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
|
-
}
|
|
5
|
+
import { existsSync } from 'fs';
|
|
16
6
|
|
|
17
7
|
export const COMMAND_ALIASES = {
|
|
18
8
|
start: ['start'],
|
|
19
9
|
help: ['help', 'h', '?'],
|
|
20
|
-
status: ['status'],
|
|
21
10
|
reset: ['reset'],
|
|
22
11
|
restart: ['restart'],
|
|
23
|
-
sessions: ['sessions', 'sw'],
|
|
24
|
-
delsessions: ['delsessions', 'del'],
|
|
25
|
-
loop: ['loop'],
|
|
26
|
-
edit: ['edit'],
|
|
27
12
|
diagnose: ['diagnose'],
|
|
28
|
-
refresh: ['refresh'],
|
|
29
|
-
copy: ['copy'],
|
|
30
|
-
revert: ['revert'],
|
|
31
|
-
upload: ['upload'],
|
|
32
|
-
delete: ['delete'],
|
|
33
13
|
oc: ['oc'],
|
|
34
14
|
cc: ['cc'],
|
|
35
15
|
cx: ['cx'],
|
|
36
16
|
copilot: ['copilot'],
|
|
37
|
-
agents: ['agents'],
|
|
38
17
|
model: ['model'],
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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步',
|
|
18
|
+
raw: ['raw'],
|
|
19
|
+
think: ['think'],
|
|
20
|
+
share: ['share'],
|
|
21
|
+
bind: ['bind'],
|
|
22
|
+
push: ['push'],
|
|
23
|
+
who: ['who'],
|
|
24
|
+
deploy: ['deploy', 'gitpush'],
|
|
25
|
+
auto: ['auto'],
|
|
26
|
+
lab: ['lab'],
|
|
27
|
+
esc: ['esc', 'abort', 'stop'],
|
|
28
|
+
status: ['status'],
|
|
29
|
+
info: ['info', 'stats', 'health'],
|
|
68
30
|
};
|
|
69
31
|
|
|
70
32
|
export const EXPERT_SYSTEM_PROMPT = `你是一个专家评审系统。用户消息含触发词(z/c/叫全部专家/专家点评)时启动评审,前后可带具体问题则聚焦该问题。
|
|
@@ -154,28 +116,26 @@ export const EXPERT_SYSTEM_PROMPT = `你是一个专家评审系统。用户消
|
|
|
154
116
|
const COMMAND_HELP = {
|
|
155
117
|
start: '认领所有权',
|
|
156
118
|
help: '显示帮助',
|
|
157
|
-
status: '连接状态',
|
|
158
119
|
reset: '重置会话',
|
|
159
120
|
restart: '重启 Bot',
|
|
160
|
-
sessions: '浏览会话',
|
|
161
|
-
delsessions: '删除会话',
|
|
162
|
-
loop: '循环任务',
|
|
163
|
-
edit: '编辑消息',
|
|
164
121
|
diagnose: '系统诊断',
|
|
165
|
-
refresh: '刷新上下文',
|
|
166
|
-
copy: '复制回复',
|
|
167
|
-
revert: '撤销消息',
|
|
168
|
-
upload: '上传文件',
|
|
169
|
-
delete: '删除上传文件',
|
|
170
122
|
oc: '使用 OpenCode',
|
|
171
123
|
cc: '使用 Claude Code',
|
|
172
124
|
cx: '使用 Codex',
|
|
173
125
|
copilot: '使用 Copilot',
|
|
174
|
-
agents: '查看 Agent',
|
|
175
126
|
model: '切换模型',
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
127
|
+
raw: '开启/关闭 RAW 调试输出',
|
|
128
|
+
think: '开启/关闭思考过程显示',
|
|
129
|
+
share: '共享会话管理',
|
|
130
|
+
bind: '绑定新 Bot 用户',
|
|
131
|
+
push: '推送消息给其他 Bot 用户',
|
|
132
|
+
who: '查看在线用户',
|
|
133
|
+
deploy: '推送代码到所有 Git 镜像',
|
|
134
|
+
auto: '自主开发模式',
|
|
135
|
+
lab: '实验室(状态/队列/历史等)',
|
|
136
|
+
esc: '中断当前活跃任务',
|
|
137
|
+
status: '查看 OpenCode 会话状态',
|
|
138
|
+
info: 'bot 状态/统计/版本/内存',
|
|
179
139
|
};
|
|
180
140
|
|
|
181
141
|
const COMMAND_MAP = {};
|
|
@@ -204,23 +164,31 @@ export function startTypingPing(adapter, threadId) {
|
|
|
204
164
|
|
|
205
165
|
export function getHelpText() {
|
|
206
166
|
const lines = ['📖 指令\n'];
|
|
167
|
+
const hasLab = existsSync('bridge/bin/lab.mjs');
|
|
207
168
|
const groups = [
|
|
208
|
-
['
|
|
209
|
-
['
|
|
210
|
-
['
|
|
211
|
-
['
|
|
212
|
-
['
|
|
213
|
-
|
|
169
|
+
['start', 'help'], // 系统
|
|
170
|
+
['reset', 'restart', 'diagnose', 'esc', 'status', 'info'], // 会话
|
|
171
|
+
['oc', 'cc', 'cx', 'copilot'], // Agent
|
|
172
|
+
['model', 'raw', 'think'], // 配置
|
|
173
|
+
['share', 'bind', 'push', 'who'], // 协作
|
|
174
|
+
hasLab
|
|
175
|
+
? ['deploy', 'auto', 'lab']
|
|
176
|
+
: ['deploy', 'auto'], // 专家+部署+自主
|
|
214
177
|
];
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
178
|
+
let first = true;
|
|
179
|
+
for (const group of groups) {
|
|
180
|
+
if (!first) lines.push('');
|
|
181
|
+
first = false;
|
|
182
|
+
for (const cmd of group) {
|
|
183
|
+
if (!COMMAND_ALIASES[cmd]) continue;
|
|
218
184
|
const aliases = COMMAND_ALIASES[cmd];
|
|
219
|
-
const
|
|
185
|
+
const shortAlias = aliases.slice(1).find(a => a.length <= 2);
|
|
186
|
+
const aliasStr = shortAlias ? ` (${shortAlias})` : '';
|
|
220
187
|
lines.push(` /${cmd}${aliasStr} — ${COMMAND_HELP[cmd] || cmd}`);
|
|
221
188
|
}
|
|
222
|
-
lines.push('');
|
|
223
189
|
}
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push('🤖 专家评审: 发"z"或"叫全部专家"即可');
|
|
224
192
|
lines.push('💬 直接发消息给 AI!');
|
|
225
193
|
return lines.join('\n');
|
|
226
194
|
}
|
|
@@ -231,7 +199,8 @@ export function detectCommand(text) {
|
|
|
231
199
|
return { name: 'help', arg: '' };
|
|
232
200
|
}
|
|
233
201
|
if (/^[.。\/]/.test(trimmed)) {
|
|
234
|
-
|
|
202
|
+
let cmd = trimmed.slice(1).trim();
|
|
203
|
+
if (cmd.startsWith('/')) cmd = cmd.slice(1).trim();
|
|
235
204
|
const parts = cmd.split(/\s+/);
|
|
236
205
|
const name = COMMAND_MAP[parts[0].toLowerCase()];
|
|
237
206
|
if (name) {
|
|
@@ -264,155 +233,14 @@ export function parseMessage(text) {
|
|
|
264
233
|
return { type: 'default', prompt: trimmed };
|
|
265
234
|
}
|
|
266
235
|
|
|
267
|
-
function formatTimeAgo(timestamp) {
|
|
268
|
-
const diff = Date.now() - timestamp;
|
|
269
|
-
const seconds = Math.floor(diff / 1000);
|
|
270
|
-
if (seconds < 60) return `${seconds}秒前`;
|
|
271
|
-
const minutes = Math.floor(seconds / 60);
|
|
272
|
-
if (minutes < 60) return `${minutes}分钟前`;
|
|
273
|
-
const hours = Math.floor(minutes / 60);
|
|
274
|
-
if (hours < 24) return `${hours}小时前`;
|
|
275
|
-
return `${Math.floor(hours / 24)}天前`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
async function getSessionsList() {
|
|
279
|
-
const opencode = await initOpenCode();
|
|
280
|
-
if (!opencode) return null;
|
|
281
|
-
const result = await opencode.client.session.list();
|
|
282
|
-
if (result.error || !result.data) return [];
|
|
283
|
-
return result.data.sort((a, b) => (b.time?.updated || b.updated_at || 0) - (a.time?.updated || a.updated_at || 0));
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
async function getSessionMessages(sessionId) {
|
|
287
|
-
const opencode = await initOpenCode();
|
|
288
|
-
if (!opencode) return null;
|
|
289
|
-
const result = await opencode.client.session.messages({ path: { id: sessionId } });
|
|
290
|
-
if (result.error) return null;
|
|
291
|
-
return result.data || [];
|
|
292
|
-
}
|
|
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
|
-
|
|
354
236
|
export async function routeMessage(parsed, ctx) {
|
|
355
237
|
const threadId = ctx.threadId;
|
|
356
|
-
if (demoModeMap.has(threadId) && parsed.type === 'command' && DEMO_RESPONSES[parsed.command]) {
|
|
357
|
-
return DEMO_RESPONSES[parsed.command];
|
|
358
|
-
}
|
|
359
238
|
switch (parsed.type) {
|
|
360
239
|
case 'command': {
|
|
361
240
|
switch (parsed.command) {
|
|
362
241
|
case 'help':
|
|
363
242
|
return getHelpText();
|
|
364
243
|
|
|
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
|
-
|
|
395
|
-
case 'agents': {
|
|
396
|
-
const agents = registry.listAgents();
|
|
397
|
-
const lines = ['🤖 可用 AI Agent:'];
|
|
398
|
-
for (const name of agents) {
|
|
399
|
-
const agent = registry.findAgent(name);
|
|
400
|
-
const aliases = agent?.aliases || [];
|
|
401
|
-
const available = await agent?.isAvailable().catch(() => false);
|
|
402
|
-
const status = available ? '✅' : '❌';
|
|
403
|
-
const aliasStr = aliases.length > 0 ? ` (${aliases.join(', ')})` : '';
|
|
404
|
-
lines.push(`${status} ${name}${aliasStr}`);
|
|
405
|
-
}
|
|
406
|
-
lines.push('');
|
|
407
|
-
lines.push('切换: /oc /cc /cx /copilot');
|
|
408
|
-
return lines.join('\n');
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
case 'status': {
|
|
412
|
-
const connected = await checkConnection();
|
|
413
|
-
return `${connected ? '✅' : '❌'} OpenCode ${connected ? '在线' : '离线'}`;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
244
|
case 'start':
|
|
417
245
|
return '🚀 准备就绪,发送消息给 OpenCode 开始工作';
|
|
418
246
|
|
|
@@ -423,80 +251,6 @@ export async function routeMessage(parsed, ctx) {
|
|
|
423
251
|
case 'restart':
|
|
424
252
|
return '🔄 重启信号已发送,bot 即将重启...';
|
|
425
253
|
|
|
426
|
-
case 'sessions': {
|
|
427
|
-
const sessions = await getSessionsList();
|
|
428
|
-
if (!sessions || sessions.length === 0) return '📭 暂无会话';
|
|
429
|
-
let msg = '📂 最近会话:\n\n';
|
|
430
|
-
sessions.slice(0, 10).forEach((s, i) => {
|
|
431
|
-
const title = s.title || '无标题';
|
|
432
|
-
const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
|
|
433
|
-
msg += `${i + 1}. ${title} (${time})\n`;
|
|
434
|
-
});
|
|
435
|
-
return msg;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
case 'delsessions': {
|
|
439
|
-
const sessions = await getSessionsList();
|
|
440
|
-
if (!sessions || sessions.length === 0) return '📭 暂无会话可删除';
|
|
441
|
-
let msg = '🗑️ 选择要删除的会话:\n\n';
|
|
442
|
-
sessions.slice(0, 10).forEach((s, i) => {
|
|
443
|
-
msg += `${i + 1}. ${s.title || '无标题'}\n`;
|
|
444
|
-
});
|
|
445
|
-
msg += '\n(在当前平台无法交互式选择,请使用 WeChat)';
|
|
446
|
-
return msg;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
case 'loop':
|
|
450
|
-
if (parsed.arg === 'off' || parsed.arg === 'stop') return '⏹️ 循环任务已停止';
|
|
451
|
-
if (parsed.arg === 'status') return '🔄 循环任务状态(在微信中查看详情)';
|
|
452
|
-
return '🔄 循环任务已启动(完整控制请使用 WeChat)';
|
|
453
|
-
|
|
454
|
-
case 'refresh': {
|
|
455
|
-
if (!ctx.opencodeSessionId) return '❌ 没有活跃的会话';
|
|
456
|
-
const opencode = await initOpenCode();
|
|
457
|
-
if (!opencode) return '❌ 无法连接 OpenCode';
|
|
458
|
-
try {
|
|
459
|
-
await opencode.client.session.compact({ path: { id: ctx.opencodeSessionId } });
|
|
460
|
-
const result = await opencode.client.session.summarize({ path: { id: ctx.opencodeSessionId } });
|
|
461
|
-
return result.error ? '⚠️ 压缩完成,但摘要生成失败' : '✅ 会话已刷新';
|
|
462
|
-
} catch (e) {
|
|
463
|
-
console.error('[refresh] Error:', e.message);
|
|
464
|
-
return `❌ 刷新失败: ${e.message}`;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
case 'copy': {
|
|
469
|
-
if (!ctx.opencodeSessionId) return '❌ 没有活跃的会话';
|
|
470
|
-
const msgs = await getSessionMessages(ctx.opencodeSessionId);
|
|
471
|
-
if (!msgs || msgs.length === 0) return '❌ 无法获取消息';
|
|
472
|
-
const aiMsg = msgs.filter(m => m.info?.role === 'assistant').slice(-1)[0];
|
|
473
|
-
if (!aiMsg) return '❌ 未找到 AI 回复';
|
|
474
|
-
let content = '';
|
|
475
|
-
if (aiMsg.parts) {
|
|
476
|
-
for (const part of aiMsg.parts) {
|
|
477
|
-
if (part.type === 'text') content += part.text + '\n';
|
|
478
|
-
if (part.type === 'code') content += '```' + (part.language || '') + '\n' + part.code + '\n```\n';
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
return content ? `📋 最新回复:\n\n${content.substring(0, 2000)}` : '❌ 没有可复制的内容';
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
case 'revert': {
|
|
485
|
-
if (!ctx.opencodeSessionId) return '❌ 没有活跃的会话';
|
|
486
|
-
const opencode = await initOpenCode();
|
|
487
|
-
if (!opencode) return '❌ 无法连接 OpenCode';
|
|
488
|
-
if (parsed.arg === 'undo') {
|
|
489
|
-
const ok = await opencode.client.session.unrevert?.({ path: { id: ctx.opencodeSessionId } });
|
|
490
|
-
return ok ? '↩️ 已恢复撤销的内容' : '❌ 恢复失败';
|
|
491
|
-
}
|
|
492
|
-
const msgs = await getSessionMessages(ctx.opencodeSessionId);
|
|
493
|
-
if (!msgs) return '❌ 无法获取消息';
|
|
494
|
-
const lastAssistant = msgs.filter(m => m.info?.role === 'assistant' && m.time?.created).slice(-1)[0];
|
|
495
|
-
if (!lastAssistant) return '📭 没有可撤销的消息';
|
|
496
|
-
const ok = await opencode.client.session.revert({ path: { id: ctx.opencodeSessionId }, body: { messageID: lastAssistant.id } });
|
|
497
|
-
return ok ? '↩️ 已撤销最近的消息' : '❌ 撤销失败';
|
|
498
|
-
}
|
|
499
|
-
|
|
500
254
|
case 'model': {
|
|
501
255
|
try {
|
|
502
256
|
const opencode = await initOpenCode();
|
|
@@ -507,11 +261,11 @@ export async function routeMessage(parsed, ctx) {
|
|
|
507
261
|
|
|
508
262
|
// Search mode: /model <keyword>
|
|
509
263
|
if (!arg.includes('/')) {
|
|
510
|
-
const result = await opencode.client.
|
|
511
|
-
if (result.error || !result.data?.
|
|
264
|
+
const result = await opencode.client.config.providers();
|
|
265
|
+
if (result.error || !result.data?.providers) return '❌ 无法获取模型列表';
|
|
512
266
|
const q = arg.toLowerCase();
|
|
513
267
|
const matches = [];
|
|
514
|
-
for (const p of result.data.
|
|
268
|
+
for (const p of result.data.providers) {
|
|
515
269
|
for (const mid of Object.keys(p.models || {})) {
|
|
516
270
|
const label = `${p.id}/${mid}`;
|
|
517
271
|
if (label.toLowerCase().includes(q)) {
|
|
@@ -591,6 +345,18 @@ export async function routeMessage(parsed, ctx) {
|
|
|
591
345
|
return (response || '无响应') + notification;
|
|
592
346
|
}
|
|
593
347
|
|
|
348
|
+
case 'raw': {
|
|
349
|
+
const val = parsed.arg?.trim().toLowerCase();
|
|
350
|
+
if (val === 'on' || val === '1' || val === 'true') {
|
|
351
|
+
setRawDebug(true);
|
|
352
|
+
return '📄 RAW 输出已开启';
|
|
353
|
+
} else if (val === 'off' || val === '0' || val === 'false') {
|
|
354
|
+
setRawDebug(false);
|
|
355
|
+
return '📄 RAW 输出已关闭';
|
|
356
|
+
}
|
|
357
|
+
return `📄 RAW 输出当前: ${isRawDebug() ? '🟢 ON' : '🔴 OFF'}\n用法: /raw on 或 /raw off`;
|
|
358
|
+
}
|
|
359
|
+
|
|
594
360
|
default:
|
|
595
361
|
return '❓ 未知指令';
|
|
596
362
|
}
|