@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/weixin/handler.js
CHANGED
|
@@ -1,550 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { isAuthorized, hasOwner } from '../core/auth.js';
|
|
5
|
-
import { registry } from '../core/registry.js';
|
|
6
|
-
import { sendMessage as sendWeixinMessage } from './api.js';
|
|
7
|
-
import { randomBytes } from 'crypto';
|
|
8
|
-
import { detectCommand, EXPERT_SYSTEM_PROMPT, startTypingPing } from '../core/router.js';
|
|
9
|
-
import { handleCommand, formatTimeAgo, _registerStartLoopCycle } from './commands.js';
|
|
1
|
+
import { createHandler } from '../core/handler.js';
|
|
2
|
+
import { handleCommand, sharedRoom, isSharedMember } from './commands.js';
|
|
3
|
+
import { userAdapterMap } from './user-adapter-map.js';
|
|
10
4
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const now = Date.now();
|
|
15
|
-
const iteration = (session.loopIterationCount || 0) + 1;
|
|
16
|
-
const maxIterations = session.loopMaxIterations || 10;
|
|
17
|
-
const maxTimeMs = session.loopMaxTimeMs || 30 * 60 * 1000;
|
|
18
|
-
const startTime = session.loopStartTime || now;
|
|
19
|
-
|
|
20
|
-
if (iteration > maxIterations) {
|
|
21
|
-
session.loopMode = false;
|
|
22
|
-
await adapter.reply(ctx.threadId, `⏹️ 循环任务已完成(达到最大迭代次数 ${maxIterations})`);
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (now - startTime > maxTimeMs) {
|
|
27
|
-
session.loopMode = false;
|
|
28
|
-
await adapter.reply(ctx.threadId, `⏹️ 循环任务已停止(达到最大运行时长 ${Math.floor(maxTimeMs / 60000)}分钟)`);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (session.lastUserMessage && now - session.lastUserMessage < 120000) {
|
|
33
|
-
session.lastLoopTime = now;
|
|
34
|
-
setTimeout(() => startLoopCycle(adapter, ctx, openCodeSessions, session), 30000);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
session.loopIterationCount = iteration;
|
|
39
|
-
const prompt = session.loopPrompt || '根据当前项目状态,继续推进未完成的工作';
|
|
40
|
-
try {
|
|
41
|
-
await adapter.reply(ctx.threadId, `🔄 循环执行 [${iteration}/${maxIterations}]: ${prompt}`);
|
|
42
|
-
await forwardToOpenCode(adapter, ctx, prompt, openCodeSessions, session);
|
|
43
|
-
session.lastLoopTime = now;
|
|
44
|
-
setTimeout(() => startLoopCycle(adapter, ctx, openCodeSessions, session), 5 * 60 * 1000);
|
|
45
|
-
} catch (e) {
|
|
46
|
-
console.error('Loop cycle error:', e);
|
|
47
|
-
session.loopMode = false;
|
|
48
|
-
await adapter.reply(ctx.threadId, `❌ 循环任务因错误停止: ${e.message}`);
|
|
49
|
-
}
|
|
5
|
+
function replyTo(userId, text, fallbackAdapter) {
|
|
6
|
+
const a = userAdapterMap.get(userId) || fallbackAdapter;
|
|
7
|
+
return a.reply(userId, text);
|
|
50
8
|
}
|
|
51
9
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
await adapter.reply(ctx.threadId, '❌ 无法创建评审会话');
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
console.log(`✅ 新建评审会话: ${openCodeSession.sessionId.slice(0, 8)}`);
|
|
66
|
-
} else {
|
|
67
|
-
openCodeSession = openCodeSessions.get(ctx.threadId);
|
|
68
|
-
if (!openCodeSession) {
|
|
69
|
-
if (session.opencodeSessionId) openCodeSession = await resumeSession(session.opencodeSessionId);
|
|
70
|
-
if (!openCodeSession && globalThis.__latestOpenCodeSession?.id) openCodeSession = await resumeSession(globalThis.__latestOpenCodeSession.id);
|
|
71
|
-
if (!openCodeSession) {
|
|
72
|
-
const mapping = loadSessionMapping();
|
|
73
|
-
if (mapping[ctx.threadId]?.opencodeSessionId) openCodeSession = await resumeSession(mapping[ctx.threadId].opencodeSessionId);
|
|
74
|
-
}
|
|
75
|
-
if (!openCodeSession) {
|
|
76
|
-
openCodeSession = await createSession(ctx.threadId, `Weixin ${ctx.threadId}`);
|
|
77
|
-
if (!openCodeSession) { await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话'); return; }
|
|
78
|
-
}
|
|
79
|
-
openCodeSessions.set(ctx.threadId, openCodeSession);
|
|
80
|
-
session.opencodeSessionId = openCodeSession.sessionId;
|
|
81
|
-
saveSessionMapping();
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (session.modelOverride) openCodeSession.model = session.modelOverride;
|
|
86
|
-
|
|
87
|
-
session.taskStartTime = Date.now();
|
|
88
|
-
session.currentTool = null;
|
|
89
|
-
const stopHeartbeat = () => {
|
|
90
|
-
session.taskStartTime = null; session.currentTool = null;
|
|
91
|
-
if (session.modifiedFiles instanceof Set) session.modifiedFiles = Array.from(session.modifiedFiles);
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
console.log(`📤 Message sent: → ${text}`);
|
|
95
|
-
const projectDir = session.projectDir || globalThis.__autoProjectDir;
|
|
96
|
-
|
|
97
|
-
let scopedText = text;
|
|
98
|
-
if (session._contextScope) scopedText = `[上下文范围: ${session._contextScope}]\n\n${text}`;
|
|
99
|
-
if (projectDir && !scopedText.includes('项目目录')) scopedText = `[当前项目目录: ${projectDir}]\n\n${scopedText}`;
|
|
100
|
-
if (expertPrompt) scopedText = `${expertPrompt}\n\n${scopedText}`;
|
|
101
|
-
|
|
102
|
-
let hasToolActivity = false;
|
|
103
|
-
let toolCount = 0;
|
|
104
|
-
|
|
105
|
-
const typingPing = startTypingPing(adapter, ctx.threadId);
|
|
106
|
-
|
|
107
|
-
const replyWithTyping = async (text) => {
|
|
108
|
-
const snippet = text.length > 80 ? text.slice(0, 80) + '...' : text;
|
|
109
|
-
console.log(`[→发送] ${snippet}`);
|
|
110
|
-
try { await adapter.reply(ctx.threadId, text); } catch (e) { console.error('[→发送] 失败:', e.message); }
|
|
111
|
-
adapter.sendTypingIndicator(ctx.threadId).catch(() => {});
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
let contentSent = false;
|
|
115
|
-
const result = await sendToOpenCode(openCodeSession, scopedText, {
|
|
116
|
-
idleThreshold: expertPrompt ? 30 : 10,
|
|
117
|
-
onNewContent: (delta) => {
|
|
118
|
-
const trimmed = delta.trim();
|
|
119
|
-
if (trimmed) { replyWithTyping(trimmed); typingPing.poke(); contentSent = true; }
|
|
120
|
-
},
|
|
121
|
-
onEvent: (event) => {
|
|
122
|
-
if (event.type === 'tool.call') {
|
|
123
|
-
const props = event.properties || {};
|
|
124
|
-
const toolName = props.name || props.tool_name || 'unknown';
|
|
125
|
-
const input = props.input || {};
|
|
126
|
-
hasToolActivity = true; toolCount++;
|
|
127
|
-
let toolDesc = `🔧 ${toolName}`;
|
|
128
|
-
if (input.path) toolDesc += ` 📁${input.path}`;
|
|
129
|
-
if (input.command) toolDesc += ` 💻${input.command}`;
|
|
130
|
-
replyWithTyping(toolDesc);
|
|
131
|
-
typingPing.poke();
|
|
132
|
-
if (input.path) {
|
|
133
|
-
if (!session.modifiedFiles) session.modifiedFiles = new Set();
|
|
134
|
-
session.modifiedFiles.add(input.path);
|
|
10
|
+
function wrapAdapterForShared(adapter, senderId) {
|
|
11
|
+
return new Proxy(adapter, {
|
|
12
|
+
get(target, prop) {
|
|
13
|
+
if (prop !== 'reply') return target[prop];
|
|
14
|
+
return async (threadId, msg) => {
|
|
15
|
+
const result = await target.reply(threadId, msg).catch(() => {});
|
|
16
|
+
if (threadId === senderId) {
|
|
17
|
+
for (const tid of sharedRoom.members) {
|
|
18
|
+
if (tid !== senderId) replyTo(tid, msg, adapter).catch(() => {});
|
|
19
|
+
}
|
|
135
20
|
}
|
|
136
|
-
|
|
21
|
+
return result;
|
|
22
|
+
};
|
|
137
23
|
},
|
|
138
|
-
onStatusChange: (status) => {
|
|
139
|
-
if (status.hasToolActivity) hasToolActivity = true;
|
|
140
|
-
},
|
|
141
|
-
}, ctx.threadId).catch((e) => {
|
|
142
|
-
console.error('[forwardToOpenCode] Task error:', e.message);
|
|
143
|
-
return '';
|
|
144
24
|
});
|
|
145
|
-
|
|
146
|
-
stopHeartbeat();
|
|
147
|
-
typingPing.done();
|
|
148
|
-
|
|
149
|
-
// onNewContent 已发过内容就不兜底了,避免重复
|
|
150
|
-
if (!contentSent) {
|
|
151
|
-
const finalText = (result || '').trim();
|
|
152
|
-
if (finalText && !finalText.startsWith('⏰') && !finalText.startsWith('❌')) {
|
|
153
|
-
const msgs = splitMessage(finalText);
|
|
154
|
-
for (const m of msgs) {
|
|
155
|
-
if (m.trim()) adapter.reply(ctx.threadId, m).catch(e => console.error('[reply] 兜底失败:', e.message));
|
|
156
|
-
}
|
|
157
|
-
} else if (finalText) {
|
|
158
|
-
adapter.reply(ctx.threadId, finalText).catch(e => console.error('[reply] 兜底失败:', e.message));
|
|
159
|
-
} else {
|
|
160
|
-
adapter.reply(ctx.threadId, '⚠️ AI 返回为空(可能是超时),请重试或 /diagnose').catch(e => console.error('[reply] 失败:', e.message));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const allThreads = getThreadsBySessionIdFromMapping(openCodeSession.sessionId);
|
|
165
|
-
for (const otherThreadId of allThreads) {
|
|
166
|
-
if (otherThreadId === ctx.threadId) continue;
|
|
167
|
-
const otherContextToken = adapter.contextTokens.get(otherThreadId);
|
|
168
|
-
if (!otherContextToken) continue;
|
|
169
|
-
try {
|
|
170
|
-
await sendWeixinMessage({
|
|
171
|
-
baseUrl: adapter._baseUrl,
|
|
172
|
-
token: adapter._token,
|
|
173
|
-
body: { msg: { from_user_id: adapter._botId, to_user_id: otherThreadId, client_id: `${Date.now()}-${randomBytes(8).toString('hex')}`, message_type: 2, message_state: 2, context_token: otherContextToken, item_list: [{ type: 1, text_item: { text: `[来自 ${ctx.threadId} 的会话]\n\n${result || ''}` } }] } }
|
|
174
|
-
});
|
|
175
|
-
} catch (e) {
|
|
176
|
-
console.error(`Failed to broadcast to ${otherThreadId}:`, e.message);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const shareUrl = await shareSession(openCodeSession);
|
|
181
|
-
const filesCount = (session.modifiedFiles?.length || session.modifiedFiles?.size || 0);
|
|
182
|
-
if (filesCount > 0 && shareUrl) {
|
|
183
|
-
try {
|
|
184
|
-
await adapter.reply(ctx.threadId, `🔗 ${shareUrl}`);
|
|
185
|
-
} catch (e) {
|
|
186
|
-
console.error('[forwardToOpenCode] share URL reply failed:', e.message);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
saveSessionMapping();
|
|
191
25
|
}
|
|
192
26
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const arg = text.slice(2).trim();
|
|
201
|
-
if (arg === 'off' || arg === 'reset' || arg === '关闭') {
|
|
202
|
-
await adapter.reply(ctx.threadId, '⏹️ 自定义 prompt 已清除');
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
if (arg) {
|
|
206
|
-
expertPrompt = arg;
|
|
207
|
-
await adapter.reply(ctx.threadId, `✅ 自定义专家 prompt (${arg.length}字),本消息生效`);
|
|
208
|
-
} else {
|
|
209
|
-
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
210
|
-
await adapter.reply(ctx.threadId, '✅ 专家评审已启动');
|
|
211
|
-
}
|
|
212
|
-
// /z 直接走专家模式,不继续向下走
|
|
213
|
-
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (expertTriggers.some(t => trimmedLower.includes(t))) {
|
|
217
|
-
expertPrompt = EXPERT_SYSTEM_PROMPT;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const detected = detectCommand(text);
|
|
221
|
-
if (detected) {
|
|
222
|
-
const handled = await handleCommand(adapter, ctx, detected.name, detected.arg, openCodeSessions);
|
|
223
|
-
if (handled) return;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (!isAuthorized('weixin', ctx.userId)) {
|
|
227
|
-
if (!hasOwner('weixin')) {
|
|
228
|
-
await adapter.reply(ctx.threadId, '🔐 请先发送 /start 进行安全认证');
|
|
229
|
-
} else {
|
|
230
|
-
await adapter.reply(ctx.threadId, '🚫 你无权使用此 bot');
|
|
231
|
-
}
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (session._deleteSessionList) {
|
|
236
|
-
const trimmed = text.trim();
|
|
237
|
-
if (/^\d+$/.test(trimmed)) {
|
|
238
|
-
const num = parseInt(trimmed, 10);
|
|
239
|
-
if (num >= 1 && num <= session._deleteSessionList.length) {
|
|
240
|
-
const target = session._deleteSessionList[num - 1];
|
|
241
|
-
try {
|
|
242
|
-
const opencode = await initOpenCode();
|
|
243
|
-
if (!opencode) {
|
|
244
|
-
await adapter.reply(ctx.threadId, '❌ 无法删除会话');
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
await opencode.client.session.delete({ path: { id: target.id } });
|
|
248
|
-
if (session.opencodeSessionId === target.id) {
|
|
249
|
-
openCodeSessions.delete(ctx.threadId);
|
|
250
|
-
session.opencodeSessionId = undefined;
|
|
251
|
-
saveSessionMapping();
|
|
252
|
-
}
|
|
253
|
-
const result = await opencode.client.session.list();
|
|
254
|
-
if (!result.error && result.data) {
|
|
255
|
-
session._deleteSessionList = result.data;
|
|
256
|
-
if (result.data.length === 0) {
|
|
257
|
-
session._deleteSessionList = null;
|
|
258
|
-
await adapter.reply(ctx.threadId, `🗑️ 已删除: ${target.title || '无标题'}\n📭 没有更多会话了`);
|
|
259
|
-
} else {
|
|
260
|
-
let msg = `🗑️ 已删除: ${target.title || '无标题'}\n\n📂 选择要删除的会话(回复编号):\n\n`;
|
|
261
|
-
result.data.forEach((s, i) => {
|
|
262
|
-
const n = i + 1;
|
|
263
|
-
const title = s.title || '无标题';
|
|
264
|
-
let status = '';
|
|
265
|
-
if (typeof s.status === 'string') status = s.status;
|
|
266
|
-
else if (s.status?.type) status = s.status.type;
|
|
267
|
-
const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
|
|
268
|
-
msg += `${n}. ${title} (${status || '空闲'} ${time})\n`;
|
|
269
|
-
});
|
|
270
|
-
msg += '\n回复编号删除';
|
|
271
|
-
const msgs = splitMessage(msg);
|
|
272
|
-
for (const m of msgs) {
|
|
273
|
-
await adapter.reply(ctx.threadId, m);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
} else {
|
|
277
|
-
session._deleteSessionList = null;
|
|
278
|
-
await adapter.reply(ctx.threadId, `🗑️ 已删除: ${target.title || '无标题'}`);
|
|
279
|
-
}
|
|
280
|
-
} catch (e) {
|
|
281
|
-
await adapter.reply(ctx.threadId, `❌ 删除失败: ${e.message}`);
|
|
282
|
-
}
|
|
283
|
-
} else {
|
|
284
|
-
await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._deleteSessionList.length}`);
|
|
285
|
-
}
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
if (session._switchSessionList) {
|
|
291
|
-
const trimmed = text.trim();
|
|
292
|
-
if (/^\d+$/.test(trimmed)) {
|
|
293
|
-
const num = parseInt(trimmed, 10);
|
|
294
|
-
if (num >= 1 && num <= session._switchSessionList.length) {
|
|
295
|
-
const target = session._switchSessionList[num - 1];
|
|
296
|
-
if (session._showSessionState) {
|
|
297
|
-
session._showSessionState = null;
|
|
298
|
-
session._pendingSwitchSession = target;
|
|
299
|
-
const targetDir = target.directory || process.cwd();
|
|
300
|
-
let stateMsg = `📋 会话: ${target.title || '无标题'}\n`;
|
|
301
|
-
stateMsg += `ID: ${target.id.slice(0, 8)}...\n`;
|
|
302
|
-
if (target.directory) stateMsg += `📁 目录: ${target.directory}\n`;
|
|
303
|
-
stateMsg += '\n';
|
|
304
|
-
try {
|
|
305
|
-
const { readFileSync, existsSync } = await import('fs');
|
|
306
|
-
const { join } = await import('path');
|
|
307
|
-
const memoryPath = join(targetDir, 'MEMORY.md');
|
|
308
|
-
if (existsSync(memoryPath)) {
|
|
309
|
-
const content = readFileSync(memoryPath, 'utf-8');
|
|
310
|
-
const summaryMatch = content.match(/## 最近会话摘要\n([\s\S]*?)(?=##|$)/);
|
|
311
|
-
if (summaryMatch) {
|
|
312
|
-
const lines = summaryMatch[1].trim().split('\n').filter(l => l.trim());
|
|
313
|
-
const recent = lines.slice(-3);
|
|
314
|
-
if (recent.length > 0) {
|
|
315
|
-
stateMsg += `📝 最近会话:\n${recent.map(l => ` ${l.trim()}`).join('\n')}\n\n`;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const threadsMatch = content.match(/## 开放线程\n([\s\S]*?)(?=##|$)/);
|
|
319
|
-
if (threadsMatch && threadsMatch[1].trim()) {
|
|
320
|
-
stateMsg += `🔓 开放线程:\n${threadsMatch[1].trim().slice(0, 300)}\n\n`;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
324
|
-
const dailyLogPath = join(targetDir, 'daily-logs', `${today}.md`);
|
|
325
|
-
if (existsSync(dailyLogPath)) {
|
|
326
|
-
const logContent = readFileSync(dailyLogPath, 'utf-8');
|
|
327
|
-
const lastEntry = logContent.split('## ').pop();
|
|
328
|
-
if (lastEntry && lastEntry.trim()) {
|
|
329
|
-
stateMsg += `📅 今日日志:\n${lastEntry.trim().slice(0, 300)}`;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
} catch { console.debug('[session-switch] Failed to read daily log'); }
|
|
333
|
-
stateMsg += `\n💡 回复 "确认" 切换到此会话,或回复其他内容取消`;
|
|
334
|
-
await adapter.reply(ctx.threadId, stateMsg);
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
try {
|
|
339
|
-
const resumed = await resumeSession(target.id);
|
|
340
|
-
if (resumed) {
|
|
341
|
-
openCodeSessions.set(ctx.threadId, resumed);
|
|
342
|
-
session.opencodeSessionId = resumed.sessionId;
|
|
343
|
-
session.taskStartTime = null;
|
|
344
|
-
session.currentTool = null;
|
|
345
|
-
session.modifiedFiles = null;
|
|
346
|
-
const key = `weixin:${ctx.userId}:${ctx.threadId}`;
|
|
347
|
-
sessionManager.saveSession(key, session).catch(e => console.error('[session] save failed:', e.message));
|
|
348
|
-
saveSessionMapping();
|
|
349
|
-
if (target.directory) {
|
|
350
|
-
session.projectDir = target.directory;
|
|
351
|
-
globalThis.__autoProjectDir = target.directory;
|
|
352
|
-
}
|
|
353
|
-
await adapter.reply(ctx.threadId, `✅ 已切换到: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
|
|
354
|
-
} else {
|
|
355
|
-
await adapter.reply(ctx.threadId, `❌ 切换失败`);
|
|
356
|
-
}
|
|
357
|
-
} catch (e) {
|
|
358
|
-
await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
|
|
359
|
-
}
|
|
360
|
-
} else {
|
|
361
|
-
await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._switchSessionList.length}`);
|
|
362
|
-
}
|
|
363
|
-
session._switchSessionList = null;
|
|
364
|
-
session._showSessionState = null;
|
|
365
|
-
session._pendingSwitchSession = null;
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
session._switchSessionList = null;
|
|
369
|
-
session._showSessionState = null;
|
|
370
|
-
session._pendingSwitchSession = null;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (session._pendingSwitchSession) {
|
|
374
|
-
const trimmed = text.trim().toLowerCase();
|
|
375
|
-
const target = session._pendingSwitchSession;
|
|
376
|
-
session._pendingSwitchSession = null;
|
|
377
|
-
if (trimmed === '确认' || trimmed === 'confirm' || trimmed === 'y' || trimmed === '1') {
|
|
378
|
-
try {
|
|
379
|
-
const resumed = await resumeSession(target.id);
|
|
380
|
-
if (resumed) {
|
|
381
|
-
openCodeSessions.set(ctx.threadId, resumed);
|
|
382
|
-
session.opencodeSessionId = resumed.sessionId;
|
|
383
|
-
session.taskStartTime = null;
|
|
384
|
-
session.currentTool = null;
|
|
385
|
-
session.modifiedFiles = null;
|
|
386
|
-
saveSessionMapping();
|
|
387
|
-
if (target.directory) {
|
|
388
|
-
session.projectDir = target.directory;
|
|
389
|
-
globalThis.__autoProjectDir = target.directory;
|
|
390
|
-
}
|
|
391
|
-
await adapter.reply(ctx.threadId, `✅ 已切换到: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
|
|
392
|
-
} else {
|
|
393
|
-
await adapter.reply(ctx.threadId, `❌ 切换失败`);
|
|
394
|
-
}
|
|
395
|
-
} catch (e) {
|
|
396
|
-
await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
|
|
397
|
-
}
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
|
-
await adapter.reply(ctx.threadId, '已取消切换');
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
if (session._historyList) {
|
|
405
|
-
const trimmed = text.trim();
|
|
406
|
-
if (/^\d+$/.test(trimmed)) {
|
|
407
|
-
const num = parseInt(trimmed, 10);
|
|
408
|
-
if (num >= 1 && num <= session._historyList.length) {
|
|
409
|
-
const cmd = session._historyList[session._historyList.length - num];
|
|
410
|
-
session._historyList = null;
|
|
411
|
-
session._lastPrompt = cmd;
|
|
412
|
-
await forwardToOpenCode(adapter, ctx, cmd, openCodeSessions, session);
|
|
413
|
-
return;
|
|
414
|
-
} else {
|
|
415
|
-
await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._historyList.length}`);
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
session._historyList = null;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
if (session._forkList) {
|
|
423
|
-
const trimmed = text.trim();
|
|
424
|
-
if (/^\d+$/.test(trimmed)) {
|
|
425
|
-
const num = parseInt(trimmed, 10);
|
|
426
|
-
if (num >= 1 && num <= session._forkList.length) {
|
|
427
|
-
const targetMsg = session._forkList[num - 1];
|
|
428
|
-
const forked = await forkSession(session._forkSessionId, targetMsg.id, session.projectDir);
|
|
429
|
-
session._forkList = null;
|
|
430
|
-
session._forkSessionId = null;
|
|
431
|
-
if (forked) {
|
|
432
|
-
openCodeSessions.set(ctx.threadId, forked);
|
|
433
|
-
session.opencodeSessionId = forked.sessionId;
|
|
434
|
-
session.taskStartTime = null;
|
|
435
|
-
session.currentTool = null;
|
|
436
|
-
session.modifiedFiles = null;
|
|
437
|
-
saveSessionMapping();
|
|
438
|
-
await adapter.reply(ctx.threadId, `🔀 已从消息 #${num} 创建分支\n\n新会话: ${forked.sessionId.slice(0, 8)}...\n之前的上下文已保留`);
|
|
439
|
-
} else {
|
|
440
|
-
await adapter.reply(ctx.threadId, '❌ 分支失败');
|
|
441
|
-
}
|
|
442
|
-
return;
|
|
443
|
-
} else {
|
|
444
|
-
await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._forkList.length}`);
|
|
445
|
-
return;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
session._forkList = null;
|
|
449
|
-
session._forkSessionId = null;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
if (session._editList) {
|
|
453
|
-
const trimmed = text.trim();
|
|
454
|
-
if (/^\d+$/.test(trimmed)) {
|
|
455
|
-
const num = parseInt(trimmed, 10);
|
|
456
|
-
if (num >= 1 && num <= session._editList.length) {
|
|
457
|
-
const targetMsg = session._editList[num - 1];
|
|
458
|
-
const preview = targetMsg.info?.content?.slice(0, 80) || '(空)';
|
|
459
|
-
session._editTarget = { sessionId: session._editSessionId, messageID: targetMsg.id, num };
|
|
460
|
-
session._editList = null;
|
|
461
|
-
session._editSessionId = null;
|
|
462
|
-
await adapter.reply(ctx.threadId, `✏️ 选择修改消息 #${num}:\n\n${preview}\n\n请发送修正后的内容,将从该消息之前创建新分支`);
|
|
463
|
-
return;
|
|
464
|
-
} else {
|
|
465
|
-
await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._editList.length}`);
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
session._editList = null;
|
|
470
|
-
session._editSessionId = null;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (session._editTarget) {
|
|
474
|
-
const target = session._editTarget;
|
|
475
|
-
session._editTarget = null;
|
|
476
|
-
const forked = await forkSession(target.sessionId, target.messageID, session.projectDir);
|
|
477
|
-
if (forked) {
|
|
478
|
-
openCodeSessions.set(ctx.threadId, forked);
|
|
479
|
-
session.opencodeSessionId = forked.sessionId;
|
|
480
|
-
session.taskStartTime = null;
|
|
481
|
-
session.currentTool = null;
|
|
482
|
-
session.modifiedFiles = null;
|
|
483
|
-
saveSessionMapping();
|
|
484
|
-
await adapter.reply(ctx.threadId, `✅ 已创建新分支并发送修正内容\n\n从消息 #${target.num} 之前分支,发送了新提示`);
|
|
485
|
-
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
|
|
486
|
-
} else {
|
|
487
|
-
await adapter.reply(ctx.threadId, '❌ 分支失败');
|
|
488
|
-
}
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (session._analyzeMode) {
|
|
493
|
-
const trimmed = text.trim().toLowerCase();
|
|
494
|
-
if (trimmed === '执行' || trimmed === 'execute' || trimmed === '开始' || trimmed === 'go') {
|
|
495
|
-
session._analyzeMode = false;
|
|
496
|
-
const task = session._analyzeTask || text;
|
|
497
|
-
session._analyzeTask = null;
|
|
498
|
-
await adapter.reply(ctx.threadId, `🔧 开始执行: ${task}\n\nAI 将实施最小改动,完成后列出变更点和验证步骤。`);
|
|
499
|
-
const execPrompt = `现在开始施工队模式。任务:${task}\n\n请严格执行以下步骤:\n\n1. 实施最小改动:只修改必要的代码,不要重构其他地方\n2. 保持风格一致:遵循项目现有的代码风格\n3. 列出变更点:修改完成后,列出所有改动的文件和代码\n4. 验证步骤:说明需要运行哪些测试或检查来验证修改\n5. 列出未验证的部分:说明还有哪些边界情况需要人工检查\n\n注意:\n- 只做最小改动\n- 不要顺手重构\n- 不要修改不相关的文件`;
|
|
500
|
-
await forwardToOpenCode(adapter, ctx, execPrompt, openCodeSessions, session);
|
|
501
|
-
return;
|
|
502
|
-
}
|
|
503
|
-
session._analyzeMode = false;
|
|
504
|
-
session._analyzeTask = null;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const connected = await checkConnection();
|
|
508
|
-
if (!connected) {
|
|
509
|
-
await adapter.reply(ctx.threadId, '❌ OpenCode 离线,请检查服务是否运行');
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (!session.commandHistory) {
|
|
514
|
-
session.commandHistory = [];
|
|
515
|
-
}
|
|
516
|
-
session.commandHistory.push(text);
|
|
517
|
-
if (session.commandHistory.length > 50) {
|
|
518
|
-
session.commandHistory = session.commandHistory.slice(-50);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
await saveSessionCommandHistory(ctx.threadId, session.commandHistory);
|
|
522
|
-
|
|
523
|
-
session.lastUserMessage = Date.now();
|
|
524
|
-
session._lastPrompt = text;
|
|
525
|
-
|
|
526
|
-
if (session.currentAgent && session.currentAgent !== 'opencode') {
|
|
527
|
-
const agent = registry.findAgent(session.currentAgent);
|
|
528
|
-
if (agent) {
|
|
529
|
-
const available = await agent.isAvailable().catch(() => false);
|
|
530
|
-
if (available) {
|
|
531
|
-
await adapter.sendTyping?.(ctx.threadId, true);
|
|
532
|
-
try {
|
|
533
|
-
const response = await agent.sendPrompt(session.id, text, session.commandHistory || [], { projectDir: session.projectDir || globalThis.__autoProjectDir });
|
|
534
|
-
await adapter.sendTyping?.(ctx.threadId, false);
|
|
535
|
-
const chunks = splitMessage(response || '无响应');
|
|
536
|
-
for (const chunk of chunks) {
|
|
537
|
-
await adapter.reply(ctx.threadId, chunk);
|
|
538
|
-
}
|
|
539
|
-
} catch (error) {
|
|
540
|
-
await adapter.reply(ctx.threadId, `❌ ${session.currentAgent} 错误: ${error.message}`);
|
|
541
|
-
}
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
|
|
548
|
-
}
|
|
27
|
+
const handler = createHandler({
|
|
28
|
+
handleCommand,
|
|
29
|
+
replyTo,
|
|
30
|
+
wrapAdapterForShared,
|
|
31
|
+
isSharedMember,
|
|
32
|
+
sharedRoom,
|
|
33
|
+
});
|
|
549
34
|
|
|
550
|
-
export
|
|
35
|
+
export const handleMessage = handler.handleMessage;
|
|
36
|
+
export const forwardToOpenCode = handler.forwardToOpenCode;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// userId → adapter 映射,用于跨用户消息路由
|
|
2
|
+
// 定期清理防内存泄漏
|
|
3
|
+
const MAX_SIZE = 5000;
|
|
4
|
+
export const userAdapterMap = new Map();
|
|
5
|
+
|
|
6
|
+
setInterval(() => {
|
|
7
|
+
if (userAdapterMap.size > MAX_SIZE) {
|
|
8
|
+
const toDelete = userAdapterMap.size - MAX_SIZE;
|
|
9
|
+
const keys = [...userAdapterMap.keys()];
|
|
10
|
+
for (let i = 0; i < toDelete; i++) userAdapterMap.delete(keys[i]);
|
|
11
|
+
}
|
|
12
|
+
}, 30 * 60 * 1000).unref();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yvhitxcel/opencode-remote",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "🤖 AI 专家团队随时待命!只需输入 /z,自动分析项目、诊断问题、给出改进方案。支持微信/飞书/Telegram 远程控制 OpenCode、Claude Code、Codex、Copilot。手机也能搞开发。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
"start": "node dist/index.js",
|
|
17
17
|
"lint": "node scripts/check-syntax.mjs",
|
|
18
18
|
"test": "npm run lint && node --test test/*.test.mjs",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"precommit": "npm run lint && npm run typecheck",
|
|
21
|
+
"prepublishOnly": "npm run lint",
|
|
22
|
+
"check-refs": "node scripts/check-ref-errors.mjs"
|
|
21
23
|
},
|
|
22
24
|
"files": [
|
|
23
25
|
"dist"
|