@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/handler.js
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
|
-
import { getOrCreateSession } from '../core/session.js';
|
|
2
1
|
import { splitMessage } from '../core/notifications.js';
|
|
3
|
-
import {
|
|
2
|
+
import { createSession, sendMessage, checkConnection, listOpenCodeSessions, resumeSession } from '../opencode/client.js';
|
|
4
3
|
import { isAuthorized, hasOwner } from '../core/auth.js';
|
|
5
4
|
import { detectCommand, EXPERT_SYSTEM_PROMPT } from '../core/router.js';
|
|
6
|
-
import { handleCommand
|
|
7
|
-
import { existsSync, readFileSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
5
|
+
import { handleCommand } from './commands.js';
|
|
9
6
|
|
|
10
7
|
async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
11
|
-
const session = await getOrCreateSession(ctx.threadId, 'feishu');
|
|
12
|
-
|
|
13
8
|
const expertTriggers = ['z', 'Z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review', '专家会诊', '团队评审', '代码审查', '全员review', 'review all', '请专家', '叫专家', '找专家'];
|
|
14
9
|
let expertPrompt = null;
|
|
15
10
|
|
|
@@ -24,7 +19,7 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
|
24
19
|
} else {
|
|
25
20
|
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
26
21
|
}
|
|
27
|
-
await forwardToOpenCode(adapter, ctx, text, openCodeSessions,
|
|
22
|
+
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt);
|
|
28
23
|
return;
|
|
29
24
|
}
|
|
30
25
|
|
|
@@ -37,155 +32,6 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
|
37
32
|
await handleCommand(adapter, ctx, parsed.name, parsed.arg, openCodeSessions);
|
|
38
33
|
return;
|
|
39
34
|
}
|
|
40
|
-
const trimmed = text.trim();
|
|
41
|
-
if (session._deleteSessionList) {
|
|
42
|
-
if (/^\d+$/.test(trimmed)) {
|
|
43
|
-
const num = parseInt(trimmed, 10);
|
|
44
|
-
const list = session._deleteSessionList;
|
|
45
|
-
if (num >= 1 && num <= list.length) {
|
|
46
|
-
const target = list[num - 1];
|
|
47
|
-
try {
|
|
48
|
-
const opencode = await initOpenCode();
|
|
49
|
-
if (!opencode) { await adapter.reply(ctx.threadId, '❌ 无法删除会话'); return; }
|
|
50
|
-
await opencode.client.session.delete({ path: { id: target.id } });
|
|
51
|
-
if (session.opencodeSessionId === target.id) {
|
|
52
|
-
openCodeSessions.delete(ctx.threadId);
|
|
53
|
-
session.opencodeSessionId = undefined;
|
|
54
|
-
}
|
|
55
|
-
const result = await opencode.client.session.list();
|
|
56
|
-
if (!result.error && result.data && result.data.length > 0) {
|
|
57
|
-
const sorted = result.data.sort((a, b) => (b.time?.updated || 0) - (a.time?.updated || 0));
|
|
58
|
-
session._deleteSessionList = sorted;
|
|
59
|
-
let msg = `🗑️ 已删除: ${target.title || '无标题'}\n\n📂 选择要删除的会话(回复编号):\n\n`;
|
|
60
|
-
sorted.slice(0, 10).forEach((s, i) => {
|
|
61
|
-
const n = i + 1;
|
|
62
|
-
msg += `${n}. ${s.title || '无标题'} (${s.updated_at ? formatTimeAgo(s.updated_at * 1000) : ''})\n`;
|
|
63
|
-
});
|
|
64
|
-
if (sorted.length > 10) msg += `\n... 共 ${sorted.length} 个会话`;
|
|
65
|
-
msg += '\n\n回复编号删除';
|
|
66
|
-
for (const m of splitMessage(msg)) await adapter.reply(ctx.threadId, m);
|
|
67
|
-
} else {
|
|
68
|
-
session._deleteSessionList = null;
|
|
69
|
-
await adapter.reply(ctx.threadId, `🗑️ 已删除: ${target.title || '无标题'}\n📭 没有更多会话了`);
|
|
70
|
-
}
|
|
71
|
-
} catch (e) {
|
|
72
|
-
await adapter.reply(ctx.threadId, `❌ 删除失败: ${e.message}`);
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._deleteSessionList.length}`);
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
session._deleteSessionList = null;
|
|
80
|
-
}
|
|
81
|
-
if (session._switchSessionList) {
|
|
82
|
-
if (/^\d+$/.test(trimmed)) {
|
|
83
|
-
const num = parseInt(trimmed, 10);
|
|
84
|
-
const list = session._switchSessionList;
|
|
85
|
-
if (num >= 1 && num <= list.length) {
|
|
86
|
-
const target = list[num - 1];
|
|
87
|
-
if (session._showSessionState) {
|
|
88
|
-
session._showSessionState = null;
|
|
89
|
-
session._pendingSwitchSession = target;
|
|
90
|
-
const targetDir = target.directory || process.cwd();
|
|
91
|
-
let stateMsg = `📋 会话: ${target.title || '无标题'}\n`;
|
|
92
|
-
stateMsg += `ID: ${target.id.slice(0, 8)}...\n`;
|
|
93
|
-
if (target.directory) stateMsg += `📁 目录: ${target.directory}\n`;
|
|
94
|
-
stateMsg += '\n';
|
|
95
|
-
try {
|
|
96
|
-
const memoryPath = join(targetDir, 'MEMORY.md');
|
|
97
|
-
if (existsSync(memoryPath)) {
|
|
98
|
-
const content = readFileSync(memoryPath, 'utf-8');
|
|
99
|
-
const summaryMatch = content.match(/## 最近会话摘要\n([\s\S]*?)(?=##|$)/);
|
|
100
|
-
if (summaryMatch) {
|
|
101
|
-
const lines = summaryMatch[1].trim().split('\n').filter(l => l.trim());
|
|
102
|
-
const recent = lines.slice(-3);
|
|
103
|
-
if (recent.length > 0) {
|
|
104
|
-
stateMsg += `📝 最近会话:\n${recent.map(l => ` ${l.trim()}`).join('\n')}\n\n`;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
} catch { console.debug('[session-switch] 无法读取MEMORY.md'); }
|
|
109
|
-
stateMsg += `💡 回复 "确认" 切换到此会话,或回复其他内容取消`;
|
|
110
|
-
await adapter.reply(ctx.threadId, stateMsg);
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
try {
|
|
114
|
-
const resumed = await resumeSession(target.id);
|
|
115
|
-
if (resumed) {
|
|
116
|
-
openCodeSessions.set(ctx.threadId, resumed);
|
|
117
|
-
session.opencodeSessionId = resumed.sessionId;
|
|
118
|
-
session.taskStartTime = null;
|
|
119
|
-
session.currentTool = null;
|
|
120
|
-
session.modifiedFiles = null;
|
|
121
|
-
if (target.directory) {
|
|
122
|
-
session.projectDir = target.directory;
|
|
123
|
-
globalThis.__autoProjectDir = target.directory;
|
|
124
|
-
}
|
|
125
|
-
await adapter.reply(ctx.threadId, `✅ 已切换至: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
|
|
126
|
-
} else {
|
|
127
|
-
await adapter.reply(ctx.threadId, '❌ 切换失败');
|
|
128
|
-
}
|
|
129
|
-
} catch (e) {
|
|
130
|
-
await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
|
|
131
|
-
}
|
|
132
|
-
} else {
|
|
133
|
-
await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._switchSessionList.length}`);
|
|
134
|
-
}
|
|
135
|
-
session._switchSessionList = null;
|
|
136
|
-
session._showSessionState = null;
|
|
137
|
-
session._pendingSwitchSession = null;
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
session._switchSessionList = null;
|
|
141
|
-
session._showSessionState = null;
|
|
142
|
-
session._pendingSwitchSession = null;
|
|
143
|
-
}
|
|
144
|
-
if (session._pendingSwitchSession) {
|
|
145
|
-
const confirm = trimmed.toLowerCase();
|
|
146
|
-
const target = session._pendingSwitchSession;
|
|
147
|
-
session._pendingSwitchSession = null;
|
|
148
|
-
if (confirm === '确认' || confirm === 'confirm' || confirm === 'y' || confirm === '1') {
|
|
149
|
-
try {
|
|
150
|
-
const resumed = await resumeSession(target.id);
|
|
151
|
-
if (resumed) {
|
|
152
|
-
openCodeSessions.set(ctx.threadId, resumed);
|
|
153
|
-
session.opencodeSessionId = resumed.sessionId;
|
|
154
|
-
session.taskStartTime = null;
|
|
155
|
-
session.currentTool = null;
|
|
156
|
-
session.modifiedFiles = null;
|
|
157
|
-
if (target.directory) {
|
|
158
|
-
session.projectDir = target.directory;
|
|
159
|
-
globalThis.__autoProjectDir = target.directory;
|
|
160
|
-
}
|
|
161
|
-
await adapter.reply(ctx.threadId, `✅ 已切换至: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
|
|
162
|
-
} else {
|
|
163
|
-
await adapter.reply(ctx.threadId, '❌ 切换失败');
|
|
164
|
-
}
|
|
165
|
-
} catch (e) {
|
|
166
|
-
await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
|
|
167
|
-
}
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
await adapter.reply(ctx.threadId, '已取消切换');
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
173
|
-
if (session._analyzeMode) {
|
|
174
|
-
const confirm = trimmed.toLowerCase();
|
|
175
|
-
if (confirm === '执行' || confirm === 'execute' || confirm === '开始' || confirm === 'go') {
|
|
176
|
-
session._analyzeMode = false;
|
|
177
|
-
const task = session._analyzeTask || text;
|
|
178
|
-
session._analyzeTask = null;
|
|
179
|
-
await adapter.reply(ctx.threadId, `🔧 开始执行: ${task}\n\nAI 将实施最小改动,完成后列出变更点和验证步骤。`);
|
|
180
|
-
const execPrompt = `现在开始施工队模式。任务:${task}\n\n请严格执行以下步骤:\n\n1. 实施最小改动:只修改必要的代码,不要重构其他地方\n2. 保持风格一致:遵循项目现有的代码风格\n3. 列出变更点:修改完成后,列出所有改动的文件和代码\n4. 验证步骤:说明需要运行哪些测试或检查来验证修改\n5. 列出未验证的部分:说明还有哪些边界情况需要人工检查\n\n注意:\n- 只做最小改动\n- 不要顺手重构\n- 不要修改不相关的文件`;
|
|
181
|
-
await forwardToOpenCode(adapter, ctx, execPrompt, openCodeSessions, session);
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
session._analyzeMode = false;
|
|
185
|
-
session._analyzeTask = null;
|
|
186
|
-
await adapter.reply(ctx.threadId, '已取消分析模式');
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
35
|
if (!isAuthorized('feishu', ctx.userId)) {
|
|
190
36
|
if (!hasOwner('feishu')) {
|
|
191
37
|
await adapter.reply(ctx.threadId, `🔐 **需要认证**
|
|
@@ -209,18 +55,27 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
|
|
|
209
55
|
🔄 /retry — 重试连接`);
|
|
210
56
|
return;
|
|
211
57
|
}
|
|
212
|
-
await forwardToOpenCode(adapter, ctx, text, openCodeSessions
|
|
58
|
+
await forwardToOpenCode(adapter, ctx, text, openCodeSessions);
|
|
213
59
|
}
|
|
214
60
|
|
|
215
|
-
async function forwardToOpenCode(adapter, ctx, text, openCodeSessions,
|
|
61
|
+
async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt) {
|
|
216
62
|
await adapter.sendTypingIndicator(ctx.threadId);
|
|
217
63
|
let openCodeSession = openCodeSessions.get(ctx.threadId);
|
|
218
64
|
if (!openCodeSession) {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
65
|
+
console.log(`[feishu-forward] no in-memory session, trying to resume most recent...`);
|
|
66
|
+
try {
|
|
67
|
+
const sessions = await listOpenCodeSessions();
|
|
68
|
+
if (sessions.length > 0) {
|
|
69
|
+
const latest = sessions.sort((a, b) => (b.updated_at || 0) - (a.updated_at || 0))[0];
|
|
70
|
+
const resumed = await resumeSession(latest.id);
|
|
71
|
+
if (resumed) {
|
|
72
|
+
openCodeSession = resumed;
|
|
73
|
+
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
74
|
+
console.log(`[feishu-forward] resumed session ${latest.id.slice(0, 8)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
console.log(`[feishu-forward] failed to resume: ${e.message}`);
|
|
224
79
|
}
|
|
225
80
|
if (!openCodeSession) {
|
|
226
81
|
openCodeSession = await createSession(ctx.threadId, `Feishu ${ctx.threadId}`);
|
|
@@ -228,25 +83,11 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
228
83
|
await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话');
|
|
229
84
|
return;
|
|
230
85
|
}
|
|
86
|
+
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
231
87
|
}
|
|
232
|
-
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
233
|
-
session.opencodeSessionId = openCodeSession.sessionId;
|
|
234
88
|
}
|
|
235
|
-
|
|
236
|
-
openCodeSession.model = session.modelOverride;
|
|
237
|
-
}
|
|
238
|
-
session.taskStartTime = Date.now();
|
|
239
|
-
session.currentTool = null;
|
|
240
|
-
const projectDir = session.projectDir || globalThis.__autoProjectDir;
|
|
89
|
+
|
|
241
90
|
let scopedText = text;
|
|
242
|
-
if (session._contextScope) {
|
|
243
|
-
scopedText = `[上下文范围: ${session._contextScope}]\n\n${text}`;
|
|
244
|
-
}
|
|
245
|
-
if (projectDir) {
|
|
246
|
-
if (!scopedText.includes('项目目录')) {
|
|
247
|
-
scopedText = `[当前项目目录: ${projectDir}]\n\n${scopedText}`;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
91
|
|
|
251
92
|
if (expertPrompt) {
|
|
252
93
|
scopedText = `${expertPrompt}\n\n${scopedText}`;
|
|
@@ -254,9 +95,6 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
254
95
|
|
|
255
96
|
console.log(`📤 Forwarding to OpenCode: ${text.substring(0, 80)}...`);
|
|
256
97
|
try {
|
|
257
|
-
let lastToolNotified = '';
|
|
258
|
-
let hasToolActivity = false;
|
|
259
|
-
let toolCallCount = 0;
|
|
260
98
|
let response = await sendMessage(openCodeSession, scopedText, {
|
|
261
99
|
idleThreshold: expertPrompt ? 30 : 10,
|
|
262
100
|
onEvent: (event) => {
|
|
@@ -264,7 +102,6 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
264
102
|
const props = event.properties || {};
|
|
265
103
|
const toolName = props.name || props.tool_name || 'unknown';
|
|
266
104
|
const input = props.input || {};
|
|
267
|
-
toolCallCount++;
|
|
268
105
|
let toolDesc = `🔧 执行工具: ${toolName}`;
|
|
269
106
|
if (input.path) {
|
|
270
107
|
toolDesc += `\n📁 ${input.path}`;
|
|
@@ -273,10 +110,6 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
273
110
|
toolDesc += `\n💻 ${input.command}`;
|
|
274
111
|
}
|
|
275
112
|
adapter.reply(ctx.threadId, toolDesc).catch(() => {});
|
|
276
|
-
if (input.path) {
|
|
277
|
-
if (!session.modifiedFiles) session.modifiedFiles = new Set();
|
|
278
|
-
session.modifiedFiles.add(input.path);
|
|
279
|
-
}
|
|
280
113
|
console.log(`[feishu-tool] Executing: ${toolName}`);
|
|
281
114
|
}
|
|
282
115
|
if (event.type && !event.type.includes('delta')) {
|
|
@@ -284,20 +117,7 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
284
117
|
}
|
|
285
118
|
},
|
|
286
119
|
onTextDelta: () => {},
|
|
287
|
-
|
|
288
|
-
if (status.hasToolActivity) {
|
|
289
|
-
hasToolActivity = true;
|
|
290
|
-
}
|
|
291
|
-
if (status.type === 'retry') {
|
|
292
|
-
adapter.updateMessage(ctx.threadId, '', `⏳ 重试中 (${status.attempt})...`).catch(() => {});
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
session.taskStartTime = null;
|
|
297
|
-
session.currentTool = null;
|
|
298
|
-
if (session.modifiedFiles instanceof Set) {
|
|
299
|
-
session.modifiedFiles = Array.from(session.modifiedFiles);
|
|
300
|
-
}
|
|
120
|
+
}, ctx.threadId);
|
|
301
121
|
if (!response || typeof response !== 'string') {
|
|
302
122
|
await adapter.reply(ctx.threadId, '...');
|
|
303
123
|
return;
|
|
@@ -307,8 +127,10 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
307
127
|
await adapter.reply(ctx.threadId, '...');
|
|
308
128
|
return;
|
|
309
129
|
}
|
|
130
|
+
// 超时/错误 → 清除 session 让下次重建
|
|
310
131
|
if (trimmedResponse.startsWith('⏰') || trimmedResponse.startsWith('❌')) {
|
|
311
132
|
console.error('[feishu-forward] Error response:', trimmedResponse);
|
|
133
|
+
openCodeSessions.delete(ctx.threadId);
|
|
312
134
|
await adapter.reply(ctx.threadId, trimmedResponse);
|
|
313
135
|
return;
|
|
314
136
|
}
|
|
@@ -317,8 +139,8 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
317
139
|
await new Promise(r => setTimeout(r, 5000));
|
|
318
140
|
try {
|
|
319
141
|
const msgsResult = await openCodeSession.client.session.messages({
|
|
320
|
-
|
|
321
|
-
|
|
142
|
+
sessionID: openCodeSession.sessionId,
|
|
143
|
+
limit: 5,
|
|
322
144
|
});
|
|
323
145
|
if (!msgsResult.error && msgsResult.data && msgsResult.data.length > 0) {
|
|
324
146
|
for (let i = msgsResult.data.length - 1; i >= 0; i--) {
|
|
@@ -349,20 +171,7 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
|
|
|
349
171
|
console.error('[feishu-forward] 回复失败:', replyErr.message);
|
|
350
172
|
}
|
|
351
173
|
}
|
|
352
|
-
const filesCount = (session.modifiedFiles?.length || session.modifiedFiles?.size || 0);
|
|
353
|
-
if (filesCount > 0) {
|
|
354
|
-
try {
|
|
355
|
-
const shareUrl = await shareSession(openCodeSession);
|
|
356
|
-
if (shareUrl) {
|
|
357
|
-
await adapter.reply(ctx.threadId, `🔗 ${shareUrl}`);
|
|
358
|
-
}
|
|
359
|
-
} catch (e) {
|
|
360
|
-
console.error('[feishu-forward] 分享会话失败:', e.message);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
174
|
} catch (error) {
|
|
364
|
-
session.taskStartTime = null;
|
|
365
|
-
session.currentTool = null;
|
|
366
175
|
console.error('❌ Feishu 错误:', error);
|
|
367
176
|
await adapter.reply(ctx.threadId, `❌ 错误: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
368
177
|
}
|