@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/feishu/commands.js
CHANGED
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
import { getOrCreateSession } from '../core/session.js';
|
|
2
1
|
import { splitMessage } from '../core/notifications.js';
|
|
3
2
|
import { EMOJI } from '../core/types.js';
|
|
4
|
-
import { initOpenCode,
|
|
3
|
+
import { initOpenCode, checkConnection, abortSession, setThreadModel, getThreadModel, getRecentModels, setRawDebug, isRawDebug } from '../opencode/client.js';
|
|
5
4
|
import { claimOwnership } from '../core/auth.js';
|
|
6
|
-
import {
|
|
5
|
+
import { getHelpText } from '../core/router.js';
|
|
7
6
|
import { registry } from '../core/registry.js';
|
|
8
|
-
import { existsSync,
|
|
9
|
-
import { join
|
|
10
|
-
|
|
11
|
-
function formatTimeAgo(timestamp) {
|
|
12
|
-
const diff = Date.now() - timestamp;
|
|
13
|
-
const seconds = Math.floor(diff / 1000);
|
|
14
|
-
if (seconds < 60) return `${seconds}秒前`;
|
|
15
|
-
const minutes = Math.floor(seconds / 60);
|
|
16
|
-
if (minutes < 60) return `${minutes}分钟前`;
|
|
17
|
-
const hours = Math.floor(minutes / 60);
|
|
18
|
-
if (hours < 24) return `${hours}小时前`;
|
|
19
|
-
return `${Math.floor(hours / 24)}天前`;
|
|
20
|
-
}
|
|
7
|
+
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
21
9
|
|
|
22
10
|
async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
23
|
-
const session = await getOrCreateSession(ctx.threadId, 'feishu');
|
|
24
11
|
switch (command) {
|
|
25
12
|
case 'start': {
|
|
26
13
|
const result = claimOwnership('feishu', ctx.userId);
|
|
@@ -43,19 +30,16 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
43
30
|
|
|
44
31
|
🚀 **准备就绪!**
|
|
45
32
|
💬 发送提示词开始编程
|
|
46
|
-
/help —
|
|
47
|
-
/status — 查看连接状态`);
|
|
33
|
+
/help — 查看所有指令`);
|
|
48
34
|
}
|
|
49
35
|
else {
|
|
50
36
|
await adapter.reply(ctx.threadId, `🚀 OpenCode 远程控制就绪
|
|
51
37
|
|
|
52
38
|
💬 发送消息给 OpenCode 开始工作
|
|
53
39
|
/help — 查看所有指令
|
|
54
|
-
/status — 查看连接状态
|
|
55
40
|
|
|
56
41
|
指令:
|
|
57
42
|
/start — 首次认证
|
|
58
|
-
/status — 查看连接
|
|
59
43
|
/reset — 重置会话
|
|
60
44
|
/approve — 同意变更
|
|
61
45
|
/reject — 拒绝变更
|
|
@@ -78,75 +62,81 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
78
62
|
case 'help':
|
|
79
63
|
await adapter.reply(ctx.threadId, getHelpText());
|
|
80
64
|
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
|
-
}
|
|
93
|
-
case 'agents': {
|
|
94
|
-
const agents = registry.listAgents();
|
|
95
|
-
const lines = ['🤖 可用 AI Agent:'];
|
|
96
|
-
for (const name of agents) {
|
|
97
|
-
const agent = registry.findAgent(name);
|
|
98
|
-
const aliases = agent?.aliases || [];
|
|
99
|
-
const available = await agent?.isAvailable().catch(() => false);
|
|
100
|
-
const status = available ? '✅' : '❌';
|
|
101
|
-
const aliasStr = aliases.length > 0 ? ` (${aliases.join(', ')})` : '';
|
|
102
|
-
lines.push(`${status} ${name}${aliasStr}`);
|
|
103
|
-
}
|
|
104
|
-
lines.push('');
|
|
105
|
-
lines.push('切换: /oc /cc /cx /copilot');
|
|
106
|
-
await adapter.reply(ctx.threadId, lines.join('\n'));
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
65
|
case 'model': {
|
|
110
66
|
try {
|
|
111
67
|
if (arg) {
|
|
112
68
|
const modelStr = arg.trim();
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
69
|
+
|
|
70
|
+
// Search mode: /model <keyword>
|
|
71
|
+
if (!modelStr.includes('/')) {
|
|
72
|
+
const opencode = await initOpenCode();
|
|
73
|
+
if (!opencode) {
|
|
74
|
+
await adapter.reply(ctx.threadId, '❌ OpenCode 不可用');
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
const result = await opencode.client.config.providers();
|
|
78
|
+
if (result.error || !result.data?.providers) {
|
|
79
|
+
await adapter.reply(ctx.threadId, '❌ 无法获取模型列表');
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
const q = modelStr.toLowerCase();
|
|
83
|
+
const matches = [];
|
|
84
|
+
for (const p of result.data.providers) {
|
|
85
|
+
for (const mid of Object.keys(p.models || {})) {
|
|
86
|
+
if (`${p.id}/${mid}`.toLowerCase().includes(q)) {
|
|
87
|
+
matches.push(`${p.id}/${mid}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (matches.length === 0) {
|
|
92
|
+
await adapter.reply(ctx.threadId, `🔍 未找到包含 "${modelStr}" 的模型`);
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
matches.sort();
|
|
96
|
+
let msg = `🔍 搜索 "${modelStr}" (${matches.length} 个):\n`;
|
|
97
|
+
for (const m of matches.slice(0, 30)) {
|
|
98
|
+
msg += ` ${m}\n`;
|
|
118
99
|
}
|
|
119
|
-
|
|
100
|
+
msg += '\n切换: /model <provider>/<modelID>';
|
|
101
|
+
const msgs = splitMessage(msg);
|
|
102
|
+
for (const m of msgs) await adapter.reply(ctx.threadId, m);
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const entry = setThreadModel(ctx.threadId, modelStr);
|
|
107
|
+
if (entry) {
|
|
108
|
+
await adapter.reply(ctx.threadId, `✅ 已切换模型至: ${entry.providerID}/${entry.modelID}`);
|
|
120
109
|
} else {
|
|
121
|
-
await adapter.reply(ctx.threadId, '❌
|
|
110
|
+
await adapter.reply(ctx.threadId, '❌ 格式错误,请使用: /model <provider>/<modelID>');
|
|
122
111
|
}
|
|
123
112
|
return true;
|
|
124
113
|
}
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
msg += ` ${p.id}/${mid}\n`;
|
|
114
|
+
const current = getThreadModel(ctx.threadId);
|
|
115
|
+
let msg = current
|
|
116
|
+
? `🧠 当前模型: ${current.providerID}/${current.modelID}\n\n`
|
|
117
|
+
: '';
|
|
118
|
+
|
|
119
|
+
const recent = getRecentModels();
|
|
120
|
+
if (recent.length > 0) {
|
|
121
|
+
msg += '最近使用:\n';
|
|
122
|
+
for (const r of recent) {
|
|
123
|
+
const mark = (current && r.providerID === current.providerID && r.modelID === current.modelID) ? ' ←' : '';
|
|
124
|
+
msg += ` ${r.providerID}/${r.modelID}${mark}\n`;
|
|
137
125
|
}
|
|
138
|
-
if (modelIds.length > 5) msg += ` ...还有 ${modelIds.length - 5} 个\n`;
|
|
139
126
|
msg += '\n';
|
|
140
127
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
128
|
+
if (!current) {
|
|
129
|
+
msg += '提示: 用 /model <关键词> 搜索模型,/model <provider>/<modelID> 切换\n';
|
|
130
|
+
} else {
|
|
131
|
+
msg += '用法: /model <关键词> — 搜索\n /model <provider>/<modelID> — 切换';
|
|
145
132
|
}
|
|
133
|
+
const msgs = splitMessage(msg);
|
|
134
|
+
for (const m of msgs) await adapter.reply(ctx.threadId, m);
|
|
135
|
+
return true;
|
|
146
136
|
} catch (e) {
|
|
147
137
|
await adapter.reply(ctx.threadId, `❌ 模型操作失败: ${e.message}`);
|
|
138
|
+
return true;
|
|
148
139
|
}
|
|
149
|
-
return true;
|
|
150
140
|
}
|
|
151
141
|
case 'oc':
|
|
152
142
|
case 'cc':
|
|
@@ -163,23 +153,18 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
163
153
|
await adapter.reply(ctx.threadId, `❌ ${agentName} 不可用`);
|
|
164
154
|
return true;
|
|
165
155
|
}
|
|
166
|
-
session.currentAgent = agentName;
|
|
167
156
|
if (!arg) {
|
|
168
157
|
await adapter.reply(ctx.threadId, `✅ 已切换到 ${agentName}`);
|
|
169
158
|
return true;
|
|
170
159
|
}
|
|
171
160
|
await adapter.sendTypingIndicator(ctx.threadId);
|
|
172
161
|
try {
|
|
173
|
-
const
|
|
174
|
-
const response = await agent.sendPrompt(session.id, arg, history, { projectDir: session.projectDir || globalThis.__autoProjectDir });
|
|
162
|
+
const response = await agent.sendPrompt(agentName, arg, [], { projectDir: globalThis.__autoProjectDir });
|
|
175
163
|
await adapter.sendTypingIndicator(ctx.threadId);
|
|
176
164
|
const chunks = splitMessage(response || '无响应');
|
|
177
165
|
for (const chunk of chunks) {
|
|
178
166
|
await adapter.reply(ctx.threadId, chunk);
|
|
179
167
|
}
|
|
180
|
-
session.commandHistory = session.commandHistory || [];
|
|
181
|
-
session.commandHistory.push({ role: 'user', content: arg });
|
|
182
|
-
session.commandHistory.push({ role: 'assistant', content: response });
|
|
183
168
|
} catch (error) {
|
|
184
169
|
await adapter.sendTypingIndicator(ctx.threadId);
|
|
185
170
|
await adapter.reply(ctx.threadId, `❌ 错误: ${error.message}`);
|
|
@@ -187,64 +172,16 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
187
172
|
return true;
|
|
188
173
|
}
|
|
189
174
|
case 'approve': {
|
|
190
|
-
|
|
191
|
-
if (!pending) {
|
|
192
|
-
await adapter.reply(ctx.threadId, '🤷 没有待审批的变更');
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
await adapter.reply(ctx.threadId, '✅ 已批准');
|
|
175
|
+
await adapter.reply(ctx.threadId, '🤷 没有待审批的变更');
|
|
196
176
|
return true;
|
|
197
177
|
}
|
|
198
178
|
case 'reject': {
|
|
199
|
-
|
|
200
|
-
if (!pending) {
|
|
201
|
-
await adapter.reply(ctx.threadId, '🤷 没有待拒绝的变更');
|
|
202
|
-
return true;
|
|
203
|
-
}
|
|
204
|
-
session.pendingApprovals.shift();
|
|
205
|
-
await adapter.reply(ctx.threadId, '❌ 已拒绝');
|
|
179
|
+
await adapter.reply(ctx.threadId, '🤷 没有待拒绝的变更');
|
|
206
180
|
return true;
|
|
207
181
|
}
|
|
208
182
|
|
|
209
183
|
case 'files': {
|
|
210
|
-
|
|
211
|
-
if (!pending || !pending.files?.length) {
|
|
212
|
-
await adapter.reply(ctx.threadId, '📄 此会话没有文件变更');
|
|
213
|
-
return true;
|
|
214
|
-
}
|
|
215
|
-
const fileList = pending.files.map(f => `• ${f.path} (+${f.additions}, -${f.deletions})`).join('\n');
|
|
216
|
-
await adapter.reply(ctx.threadId, `📄 已修改文件:\n${fileList}`);
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
case 'status': {
|
|
220
|
-
const openCodeConnected = await checkConnection();
|
|
221
|
-
const actualSession = openCodeSessions?.get(ctx.threadId) ||
|
|
222
|
-
(session.opencodeSessionId ? { sessionId: session.opencodeSessionId } : null);
|
|
223
|
-
const running = session.taskStartTime ? Math.round((Date.now() - session.taskStartTime) / 1000) : 0;
|
|
224
|
-
let msg = `${openCodeConnected ? '✅' : '❌'} OpenCode ${openCodeConnected ? '在线' : '离线'}\n\n`;
|
|
225
|
-
msg += `会话: ${actualSession?.sessionId?.slice(0, 8) || '无'}\n`;
|
|
226
|
-
if (running > 0) {
|
|
227
|
-
const m = Math.floor(running / 60);
|
|
228
|
-
const s = running % 60;
|
|
229
|
-
msg += `运行中: ${m}分${s}秒\n`;
|
|
230
|
-
}
|
|
231
|
-
if (session.currentTool) {
|
|
232
|
-
msg += `当前工具: ${session.currentTool}\n`;
|
|
233
|
-
}
|
|
234
|
-
if (session.modifiedFiles?.length > 0 || session.modifiedFiles?.size > 0) {
|
|
235
|
-
msg += `已修改: ${(session.modifiedFiles?.length || session.modifiedFiles?.size || 0)} 个文件\n`;
|
|
236
|
-
}
|
|
237
|
-
const projectDir = session.projectDir || globalThis.__autoProjectDir;
|
|
238
|
-
if (projectDir) {
|
|
239
|
-
msg += `项目目录: ${projectDir}\n`;
|
|
240
|
-
} else {
|
|
241
|
-
msg += `项目目录: 未设置\n`;
|
|
242
|
-
}
|
|
243
|
-
msg += `工作目录: ${process.cwd()}\n`;
|
|
244
|
-
if (session.originalProjectDir && session.originalProjectDir !== projectDir) {
|
|
245
|
-
msg += `原始目录: ${session.originalProjectDir}\n`;
|
|
246
|
-
}
|
|
247
|
-
await adapter.reply(ctx.threadId, msg);
|
|
184
|
+
await adapter.reply(ctx.threadId, '📄 此会话没有文件变更');
|
|
248
185
|
return true;
|
|
249
186
|
}
|
|
250
187
|
case 'reset': {
|
|
@@ -252,35 +189,6 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
252
189
|
if (oldSession) {
|
|
253
190
|
abortSession(oldSession).catch(() => {});
|
|
254
191
|
}
|
|
255
|
-
session.pendingApprovals = [];
|
|
256
|
-
session.opencodeSessionId = undefined;
|
|
257
|
-
session.loopMode = false;
|
|
258
|
-
session.loopPrompt = null;
|
|
259
|
-
session.projectDir = null;
|
|
260
|
-
session.currentAgent = null;
|
|
261
|
-
session.messages = [];
|
|
262
|
-
session.commandHistory = [];
|
|
263
|
-
session.taskStartTime = null;
|
|
264
|
-
session.currentTool = null;
|
|
265
|
-
session.modifiedFiles = null;
|
|
266
|
-
session.lastUserMessage = null;
|
|
267
|
-
session._lastPrompt = null;
|
|
268
|
-
session._contextScope = null;
|
|
269
|
-
session.originalProjectDir = null;
|
|
270
|
-
session._switchSessionList = null;
|
|
271
|
-
session._deleteSessionList = null;
|
|
272
|
-
session._pendingSwitchSession = null;
|
|
273
|
-
session._editTarget = null;
|
|
274
|
-
session._editList = null;
|
|
275
|
-
session._editSessionId = null;
|
|
276
|
-
session._historyList = null;
|
|
277
|
-
session._forkList = null;
|
|
278
|
-
session._forkSessionId = null;
|
|
279
|
-
session.expertMode = false;
|
|
280
|
-
session.systemPrompt = null;
|
|
281
|
-
session._analyzeMode = false;
|
|
282
|
-
session._analyzeTask = null;
|
|
283
|
-
session._showSessionState = null;
|
|
284
192
|
openCodeSessions?.delete(ctx.threadId);
|
|
285
193
|
globalThis.__latestOpenCodeSession = null;
|
|
286
194
|
await adapter.reply(ctx.threadId, '🔄 会话已重置,下次发送消息将创建新会话');
|
|
@@ -295,131 +203,6 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
295
203
|
}
|
|
296
204
|
return true;
|
|
297
205
|
}
|
|
298
|
-
case 'sessions': {
|
|
299
|
-
try {
|
|
300
|
-
const opencode = await initOpenCode();
|
|
301
|
-
if (!opencode) {
|
|
302
|
-
await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
|
|
303
|
-
return true;
|
|
304
|
-
}
|
|
305
|
-
const result = await opencode.client.session.list();
|
|
306
|
-
if (result.error || !result.data || result.data.length === 0) {
|
|
307
|
-
await adapter.reply(ctx.threadId, '📭 暂无会话');
|
|
308
|
-
return true;
|
|
309
|
-
}
|
|
310
|
-
const sorted = result.data.sort((a, b) => (b.time?.updated || 0) - (a.time?.updated || 0));
|
|
311
|
-
session._switchSessionList = sorted;
|
|
312
|
-
session._showSessionState = true;
|
|
313
|
-
let msg = '📂 选择会话(回复编号):\n\n';
|
|
314
|
-
sorted.slice(0, 10).forEach((s, i) => {
|
|
315
|
-
const n = i + 1;
|
|
316
|
-
const title = s.title || '无标题';
|
|
317
|
-
const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
|
|
318
|
-
msg += `${n}. ${title} (${time})\n`;
|
|
319
|
-
});
|
|
320
|
-
if (sorted.length > 10) {
|
|
321
|
-
msg += `\n... 共 ${sorted.length} 个会话`;
|
|
322
|
-
}
|
|
323
|
-
msg += '\n\n回复编号切换会话';
|
|
324
|
-
await adapter.reply(ctx.threadId, msg);
|
|
325
|
-
} catch (e) {
|
|
326
|
-
await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
|
|
327
|
-
}
|
|
328
|
-
return true;
|
|
329
|
-
}
|
|
330
|
-
case 'delsessions': {
|
|
331
|
-
try {
|
|
332
|
-
const opencode = await initOpenCode();
|
|
333
|
-
if (!opencode) {
|
|
334
|
-
await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
const result = await opencode.client.session.list();
|
|
338
|
-
if (result.error || !result.data || result.data.length === 0) {
|
|
339
|
-
await adapter.reply(ctx.threadId, '📭 暂无会话可删除');
|
|
340
|
-
return true;
|
|
341
|
-
}
|
|
342
|
-
const sorted = result.data.sort((a, b) => (b.time?.updated || 0) - (a.time?.updated || 0));
|
|
343
|
-
session._deleteSessionList = sorted;
|
|
344
|
-
let msg = '🗑️ 选择要删除的会话(回复编号):\n\n';
|
|
345
|
-
sorted.slice(0, 10).forEach((s, i) => {
|
|
346
|
-
const n = i + 1;
|
|
347
|
-
const title = s.title || '无标题';
|
|
348
|
-
const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
|
|
349
|
-
msg += `${n}. ${title} (${time})\n`;
|
|
350
|
-
});
|
|
351
|
-
if (sorted.length > 10) {
|
|
352
|
-
msg += `\n... 共 ${sorted.length} 个会话`;
|
|
353
|
-
}
|
|
354
|
-
msg += '\n\n回复编号删除';
|
|
355
|
-
await adapter.reply(ctx.threadId, msg);
|
|
356
|
-
} catch (e) {
|
|
357
|
-
await adapter.reply(ctx.threadId, `❌ 获取会话失败: ${e.message}`);
|
|
358
|
-
}
|
|
359
|
-
return true;
|
|
360
|
-
}
|
|
361
|
-
case 'loop': {
|
|
362
|
-
const argText = arg || '';
|
|
363
|
-
if (argText === 'off' || argText === 'stop') {
|
|
364
|
-
session.loopMode = false;
|
|
365
|
-
session.loopPrompt = null;
|
|
366
|
-
session.loopIterationCount = 0;
|
|
367
|
-
session.loopStartTime = null;
|
|
368
|
-
saveSessionMapping();
|
|
369
|
-
await adapter.reply(ctx.threadId, '⏹️ 循环任务已停止');
|
|
370
|
-
return true;
|
|
371
|
-
}
|
|
372
|
-
if (argText === 'status') {
|
|
373
|
-
if (session.loopMode) {
|
|
374
|
-
const elapsed = session.loopStartTime
|
|
375
|
-
? `已运行: ${Math.floor((Date.now() - session.loopStartTime) / 60000)}分钟`
|
|
376
|
-
: '';
|
|
377
|
-
const count = session.loopIterationCount || 0;
|
|
378
|
-
const limit = session.loopMaxIterations || 10;
|
|
379
|
-
await adapter.reply(ctx.threadId, `🔄 循环任务运行中\n指令: ${session.loopPrompt || '智能模式'}\n迭代: ${count}/${limit} ${elapsed}`);
|
|
380
|
-
} else {
|
|
381
|
-
await adapter.reply(ctx.threadId, '⏹️ 循环任务未运行\n发送 /loop 开始');
|
|
382
|
-
}
|
|
383
|
-
return true;
|
|
384
|
-
}
|
|
385
|
-
session.loopMode = true;
|
|
386
|
-
session.loopPrompt = argText || null;
|
|
387
|
-
session.lastLoopTime = Date.now();
|
|
388
|
-
session.loopStartTime = Date.now();
|
|
389
|
-
session.loopIterationCount = 0;
|
|
390
|
-
session.loopMaxIterations = 10;
|
|
391
|
-
session.loopMaxTimeMs = 30 * 60 * 1000;
|
|
392
|
-
saveSessionMapping();
|
|
393
|
-
const modeDesc = argText ? `指令: ${argText}` : '智能模式(根据上下文自动生成指令)';
|
|
394
|
-
await adapter.reply(ctx.threadId, `🔄 循环任务已启动\n${modeDesc}\n限制: 最多10次迭代或30分钟\n\n发送 /loop off 停止`);
|
|
395
|
-
return true;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
case 'refresh': {
|
|
399
|
-
const ocSession = openCodeSessions.get(ctx.threadId);
|
|
400
|
-
if (!ocSession) {
|
|
401
|
-
await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
await adapter.reply(ctx.threadId, '🔄 正在刷新会话...');
|
|
405
|
-
try {
|
|
406
|
-
await ocSession.client.session.compact({ path: { id: ocSession.sessionId } });
|
|
407
|
-
await ocSession.client.session.summarize({ path: { id: ocSession.sessionId } });
|
|
408
|
-
await adapter.reply(ctx.threadId, '✅ 会话已刷新');
|
|
409
|
-
} catch (e) {
|
|
410
|
-
await adapter.reply(ctx.threadId, '✅ 会话已刷新');
|
|
411
|
-
}
|
|
412
|
-
return true;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
case 'upload': {
|
|
416
|
-
await adapter.reply(ctx.threadId, 'ℹ️ 上传功能目前仅在微信客户端可用。\n请使用微信客户端上传文件。');
|
|
417
|
-
return true;
|
|
418
|
-
}
|
|
419
|
-
case 'delete': {
|
|
420
|
-
await adapter.reply(ctx.threadId, 'ℹ️ 删除功能目前仅在微信客户端可用。\n请使用微信客户端管理上传文件。');
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
206
|
case 'restart': {
|
|
424
207
|
console.log('[feishu-bot] restart command received');
|
|
425
208
|
await adapter.reply(ctx.threadId, '🔄 正在重启 bot...');
|
|
@@ -445,151 +228,33 @@ async function handleCommand(adapter, ctx, command, arg, openCodeSessions) {
|
|
|
445
228
|
return true;
|
|
446
229
|
}
|
|
447
230
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
case 'copy': {
|
|
452
|
-
const ocSession = openCodeSessions?.get(ctx.threadId);
|
|
453
|
-
if (!ocSession) {
|
|
454
|
-
await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
|
|
455
|
-
return true;
|
|
456
|
-
}
|
|
457
|
-
try {
|
|
458
|
-
const opencode = await initOpenCode();
|
|
459
|
-
if (!opencode) {
|
|
460
|
-
await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
|
|
461
|
-
return true;
|
|
462
|
-
}
|
|
463
|
-
const msgsResult = await opencode.client.session.messages({
|
|
464
|
-
path: { id: ocSession.sessionId },
|
|
465
|
-
query: { limit: 1 }
|
|
466
|
-
});
|
|
467
|
-
if (msgsResult.error || !msgsResult.data || msgsResult.data.length === 0) {
|
|
468
|
-
await adapter.reply(ctx.threadId, '❌ 无法获取最新消息');
|
|
469
|
-
return true;
|
|
470
|
-
}
|
|
471
|
-
let latestMsg = msgsResult.data[0];
|
|
472
|
-
if (latestMsg.info?.role !== 'assistant') {
|
|
473
|
-
const allMsgsResult = await opencode.client.session.messages({
|
|
474
|
-
path: { id: ocSession.sessionId },
|
|
475
|
-
query: { limit: 10 }
|
|
476
|
-
});
|
|
477
|
-
if (allMsgsResult.error || !allMsgsResult.data) {
|
|
478
|
-
await adapter.reply(ctx.threadId, '❌ 无法获取会话消息');
|
|
479
|
-
return true;
|
|
480
|
-
}
|
|
481
|
-
const aiMsg = allMsgsResult.data.find(m => m.info?.role === 'assistant');
|
|
482
|
-
if (!aiMsg) {
|
|
483
|
-
await adapter.reply(ctx.threadId, '❌ 未找到 AI 回复');
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
latestMsg = aiMsg;
|
|
487
|
-
}
|
|
488
|
-
let content = '';
|
|
489
|
-
if (latestMsg.parts) {
|
|
490
|
-
for (const part of latestMsg.parts) {
|
|
491
|
-
if (part.type === 'text') {
|
|
492
|
-
content += part.text + '\n';
|
|
493
|
-
}
|
|
494
|
-
if (part.type === 'code') {
|
|
495
|
-
content += `\`\`\`${part.language || ''}\n${part.code}\n\`\`\`\n`;
|
|
496
|
-
}
|
|
497
|
-
if (part.type === 'file' && part.content) {
|
|
498
|
-
content += `📁 ${part.filename}:\n${part.content}\n`;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
if (!content.trim()) {
|
|
503
|
-
await adapter.reply(ctx.threadId, '❌ AI 回复中没有可复制的文本内容');
|
|
504
|
-
return true;
|
|
505
|
-
}
|
|
506
|
-
await adapter.reply(ctx.threadId, `📋 已复制最新 AI 回复内容:\n\n${content.substring(0, 2000)}${content.length > 2000 ? '...' : ''}`);
|
|
507
|
-
} catch (e) {
|
|
508
|
-
await adapter.reply(ctx.threadId, `❌ 复制失败: ${e.message}`);
|
|
509
|
-
}
|
|
510
|
-
return true;
|
|
511
|
-
}
|
|
512
|
-
case 'revert': {
|
|
513
|
-
const ocS = openCodeSessions?.get(ctx.threadId);
|
|
514
|
-
if (!ocS) {
|
|
515
|
-
await adapter.reply(ctx.threadId, '❌ 没有活跃的会话');
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
518
|
-
try {
|
|
519
|
-
if (arg === 'undo') {
|
|
520
|
-
const ok = await unrevertSession(ocS.sessionId);
|
|
521
|
-
if (ok) {
|
|
522
|
-
await adapter.reply(ctx.threadId, '↩️ 已恢复撤销的内容');
|
|
523
|
-
} else {
|
|
524
|
-
await adapter.reply(ctx.threadId, '❌ 恢复失败');
|
|
525
|
-
}
|
|
526
|
-
return true;
|
|
527
|
-
}
|
|
528
|
-
const opencode = await initOpenCode();
|
|
529
|
-
if (!opencode) {
|
|
530
|
-
await adapter.reply(ctx.threadId, '❌ 无法连接 OpenCode');
|
|
531
|
-
return true;
|
|
532
|
-
}
|
|
533
|
-
const msgsResult = await opencode.client.session.messages({ path: { id: ocS.sessionId } });
|
|
534
|
-
if (msgsResult.error || !msgsResult.data) {
|
|
535
|
-
await adapter.reply(ctx.threadId, '❌ 无法获取消息');
|
|
536
|
-
return true;
|
|
537
|
-
}
|
|
538
|
-
const assistantMsgs = msgsResult.data.filter(m => m.info?.role === 'assistant' && m.time?.created);
|
|
539
|
-
if (assistantMsgs.length === 0) {
|
|
540
|
-
await adapter.reply(ctx.threadId, '📭 没有可撤销的消息');
|
|
541
|
-
return true;
|
|
542
|
-
}
|
|
543
|
-
const lastMsg = assistantMsgs[assistantMsgs.length - 1];
|
|
544
|
-
const ok = await revertSessionMessage(ocS.sessionId, lastMsg.id);
|
|
545
|
-
if (ok) {
|
|
546
|
-
const preview = lastMsg.info?.content?.slice(0, 100) || '(无内容)';
|
|
547
|
-
await adapter.reply(ctx.threadId, `↩️ 已撤销最近的消息\n\n${preview}\n\n发送 /revert undo 恢复`);
|
|
548
|
-
} else {
|
|
549
|
-
await adapter.reply(ctx.threadId, '❌ 撤销失败');
|
|
550
|
-
}
|
|
551
|
-
} catch (e) {
|
|
552
|
-
await adapter.reply(ctx.threadId, `❌ 撤销失败: ${e.message}`);
|
|
553
|
-
}
|
|
554
|
-
return true;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
578
231
|
case 'diagnose': {
|
|
579
232
|
const { checkConnection } = await import('../opencode/client.js');
|
|
580
233
|
const diag = ['🔍 诊断报告\n'];
|
|
581
234
|
diag.push(`OpenCode: ${await checkConnection().then(() => '✅').catch(() => '❌')}`);
|
|
582
235
|
diag.push(`七牛云: ${process.env.QINIU_ACCESS_KEY ? '✅' : '❌'}`);
|
|
583
|
-
diag.push(`项目目录: ${session.projectDir || globalThis.__autoProjectDir || '❌ 未设置'}`);
|
|
584
236
|
diag.push(`会话: ${openCodeSessions?.get(ctx.threadId) ? '✅' : '❌'}`);
|
|
585
237
|
const msgs = splitMessage(diag.join('\n'));
|
|
586
238
|
for (const m of msgs) await adapter.reply(ctx.threadId, m);
|
|
587
239
|
return true;
|
|
588
240
|
}
|
|
241
|
+
case 'raw': {
|
|
242
|
+
const val = arg?.trim().toLowerCase();
|
|
243
|
+
if (val === 'on' || val === '1' || val === 'true') {
|
|
244
|
+
setRawDebug(true);
|
|
245
|
+
await adapter.reply(ctx.threadId, '📄 RAW 输出已开启');
|
|
246
|
+
} else if (val === 'off' || val === '0' || val === 'false') {
|
|
247
|
+
setRawDebug(false);
|
|
248
|
+
await adapter.reply(ctx.threadId, '📄 RAW 输出已关闭');
|
|
249
|
+
} else {
|
|
250
|
+
await adapter.reply(ctx.threadId, `📄 RAW 输出当前: ${isRawDebug() ? '🟢 ON' : '🔴 OFF'}\n用法: /raw on 或 /raw off`);
|
|
251
|
+
}
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
589
254
|
default:
|
|
590
255
|
await adapter.reply(ctx.threadId, `${EMOJI.WARNING} 未知指令: ${command}\n\n请发送 /help 查看可用指令`);
|
|
591
256
|
return true;
|
|
592
257
|
}
|
|
593
258
|
}
|
|
594
259
|
|
|
595
|
-
export { handleCommand
|
|
260
|
+
export { handleCommand };
|