@yvhitxcel/opencode-remote 0.16.3 → 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.
@@ -1,59 +1,75 @@
1
- import { getOrCreateSession, updateSession, loadSessionMapping, saveSessionMapping, getThreadsBySessionIdFromMapping, saveSessionCommandHistory, sessionManager } from '../core/session.js';
2
1
  import { splitMessage } from '../core/notifications.js';
3
- import { initOpenCode, createSession, sendMessage as sendToOpenCode, checkConnection, resumeSession, shareSession, forkSession } from '../opencode/client.js';
2
+ import { createSession, sendMessage as sendToOpenCode, checkConnection, shareSession, listOpenCodeSessions, resumeSession } from '../opencode/client.js';
4
3
  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';
10
-
11
- async function startLoopCycle(adapter, ctx, openCodeSessions, session) {
12
- if (!session.loopMode) return;
13
4
 
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
5
 
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
- }
6
+ import { detectCommand, EXPERT_SYSTEM_PROMPT, startTypingPing } from '../core/router.js';
7
+ import { handleCommand, sharedRoom, isSharedMember, threadAgent } from './commands.js';
8
+ import { userAdapterMap } from './user-adapter-map.js';
9
+ import { hasPendingDecision, resolveDecision } from '../autonomous/decisions.js';
10
+ import { registry } from '../core/registry.js';
31
11
 
32
- if (session.lastUserMessage && now - session.lastUserMessage < 120000) {
33
- session.lastLoopTime = now;
34
- setTimeout(() => startLoopCycle(adapter, ctx, openCodeSessions, session), 30000);
35
- return;
36
- }
12
+ const IDLE_MODEL_HINT_MS = 5 * 60 * 1000;
13
+ const threadLastActive = new Map();
14
+ const threadHistory = new Map();
37
15
 
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
- }
16
+ function replyTo(userId, text, fallbackAdapter) {
17
+ const a = userAdapterMap.get(userId) || fallbackAdapter;
18
+ return a.reply(userId, text);
50
19
  }
51
20
 
52
- _registerStartLoopCycle(startLoopCycle);
21
+ function wrapAdapterForShared(adapter, senderId) {
22
+ return new Proxy(adapter, {
23
+ get(target, prop) {
24
+ if (prop !== 'reply') return target[prop];
25
+ return async (threadId, msg) => {
26
+ const result = await target.reply(threadId, msg).catch(() => {});
27
+ if (threadId === senderId) {
28
+ for (const tid of sharedRoom.members) {
29
+ if (tid !== senderId) {
30
+ replyTo(tid, msg, adapter).catch(() => {});
31
+ }
32
+ }
33
+ }
34
+ return result;
35
+ };
36
+ },
37
+ });
38
+ }
53
39
 
54
- async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt) {
40
+ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt) {
55
41
  adapter.sendTypingIndicator(ctx.threadId).catch(() => {});
56
42
  let openCodeSession = null;
43
+ let pendingModelHint = null;
44
+
45
+ // 共享模式:使用共享会话
46
+ const isShared = isSharedMember(ctx.threadId);
47
+ if (isShared) {
48
+ if (sharedRoom.busy) {
49
+ await adapter.reply(ctx.threadId, '⏳ 当前有人在用,请稍等...');
50
+ return;
51
+ }
52
+ if (sharedRoom.session) {
53
+ openCodeSession = sharedRoom.session;
54
+ } else {
55
+ // 首次使用共享,创建会话
56
+ openCodeSession = await createSession(`shared-${Date.now()}`, '共享会话');
57
+ if (!openCodeSession) {
58
+ await adapter.reply(ctx.threadId, '❌ 无法创建共享会话');
59
+ return;
60
+ }
61
+ sharedRoom.session = openCodeSession;
62
+ console.log(`✅ 共享会话已创建: ${openCodeSession.sessionId.slice(0, 8)}`);
63
+ }
64
+ sharedRoom.busy = true;
65
+ } else {
66
+ // 原有个体逻辑
67
+ // 检查是否长时间未活跃,提示模型信息
68
+ const lastActive = threadLastActive.get(ctx.threadId) || 0;
69
+ const isIdle = !expertPrompt && lastActive > 0 && (Date.now() - lastActive) > IDLE_MODEL_HINT_MS;
70
+ if (isIdle) {
71
+ pendingModelHint = true;
72
+ }
57
73
 
58
74
  // 专家评审每次开独立会话,避免旧历史干扰
59
75
  if (expertPrompt) {
@@ -66,120 +82,103 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
66
82
  } else {
67
83
  openCodeSession = openCodeSessions.get(ctx.threadId);
68
84
  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);
85
+ console.log(`[forwardToOpenCode] no in-memory session, trying to resume most recent...`);
86
+ try {
87
+ const sessions = await listOpenCodeSessions();
88
+ if (sessions.length > 0) {
89
+ const latest = sessions.sort((a, b) => (b.lastActivity || 0) - (a.lastActivity || 0))[0];
90
+ const resumed = await resumeSession(latest.id);
91
+ if (resumed) {
92
+ openCodeSession = resumed;
93
+ openCodeSessions.set(ctx.threadId, openCodeSession);
94
+ console.log(`[forwardToOpenCode] resumed session ${latest.id.slice(0, 8)}`);
95
+ }
96
+ }
97
+ } catch (e) {
98
+ console.log(`[forwardToOpenCode] failed to resume: ${e.message}`);
74
99
  }
75
100
  if (!openCodeSession) {
101
+ console.log(`[forwardToOpenCode] creating new session for thread=${ctx.threadId}`);
76
102
  openCodeSession = await createSession(ctx.threadId, `Weixin ${ctx.threadId}`);
77
103
  if (!openCodeSession) { await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话'); return; }
104
+ openCodeSessions.set(ctx.threadId, openCodeSession);
78
105
  }
79
- openCodeSessions.set(ctx.threadId, openCodeSession);
80
- session.opencodeSessionId = openCodeSession.sessionId;
81
- saveSessionMapping();
82
106
  }
83
107
  }
108
+ }
84
109
 
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
110
  console.log(`📤 Message sent: → ${text}`);
95
- const projectDir = session.projectDir || globalThis.__autoProjectDir;
96
-
97
111
  let scopedText = text;
98
- if (session._contextScope) scopedText = `[上下文范围: ${session._contextScope}]\n\n${text}`;
99
- if (projectDir && !scopedText.includes('项目目录')) scopedText = `[当前项目目录: ${projectDir}]\n\n${scopedText}`;
100
112
  if (expertPrompt) scopedText = `${expertPrompt}\n\n${scopedText}`;
101
113
 
102
- let hasToolActivity = false;
103
- let toolCount = 0;
104
-
105
114
  const typingPing = startTypingPing(adapter, ctx.threadId);
106
115
 
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
116
  const result = await sendToOpenCode(openCodeSession, scopedText, {
116
117
  idleThreshold: expertPrompt ? 30 : 10,
117
118
  onNewContent: (delta) => {
118
- const trimmed = delta.trim();
119
- if (trimmed) { replyWithTyping(trimmed); typingPing.poke(); contentSent = true; }
119
+ typingPing.poke();
120
+ },
121
+ onResponseMeta: (meta) => {
122
+ if (pendingModelHint && meta.modelID) {
123
+ pendingModelHint = `🧠 ${meta.providerID}/${meta.modelID}`;
124
+ }
120
125
  },
121
126
  onEvent: (event) => {
122
127
  if (event.type === 'tool.call') {
123
128
  const props = event.properties || {};
124
129
  const toolName = props.name || props.tool_name || 'unknown';
125
130
  const input = props.input || {};
126
- hasToolActivity = true; toolCount++;
127
131
  let toolDesc = `🔧 ${toolName}`;
128
132
  if (input.path) toolDesc += ` 📁${input.path}`;
129
133
  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);
134
+ console.log(`[→发送] ${toolDesc}`);
135
+ const replyTargets = isShared ? [...sharedRoom.members] : [ctx.threadId];
136
+ for (const tid of replyTargets) {
137
+ replyTo(tid, toolDesc, adapter).catch(e => console.error('[→发送] 失败:', e.message));
135
138
  }
139
+ typingPing.poke();
136
140
  }
137
141
  },
138
- onStatusChange: (status) => {
139
- if (status.hasToolActivity) hasToolActivity = true;
140
- },
141
142
  }, ctx.threadId).catch((e) => {
142
143
  console.error('[forwardToOpenCode] Task error:', e.message);
143
144
  return '';
144
145
  });
145
146
 
146
- stopHeartbeat();
147
147
  typingPing.done();
148
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));
149
+ // 共享模式:广播到所有成员
150
+ const replyTargets = isShared ? [...sharedRoom.members] : [ctx.threadId];
151
+ if (isShared) sharedRoom.busy = false;
152
+
153
+ // 错误/超时 清除 session 让下次重建
154
+ const finalText = (result || '').trim();
155
+ console.log(`📥 AI response (${finalText.length} chars): ${finalText.slice(0, 200)}${finalText.length > 200 ? '...' : ''}`);
156
+ const displayText = pendingModelHint && typeof pendingModelHint === 'string'
157
+ ? `${pendingModelHint}\n${finalText}`
158
+ : finalText;
159
+ if (!finalText || finalText.startsWith('⏰') || finalText.startsWith('❌')) {
160
+ if (!isShared) openCodeSessions.delete(ctx.threadId);
161
+ for (const tid of replyTargets) {
162
+ replyTo(tid, finalText || '⚠️ AI 返回为空(可能是超时),请重试或 /diagnose', adapter)
163
+ .catch(e => console.error('[reply] 失败:', e.message));
164
+ }
165
+ } else {
166
+ const msgs = splitMessage(displayText);
167
+ for (const m of msgs) {
168
+ if (!m.trim()) continue;
169
+ for (const tid of replyTargets) {
170
+ replyTo(tid, m, adapter).catch(e => console.error('[reply] 失败:', e.message));
156
171
  }
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
172
  }
162
173
  }
163
174
 
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
- }
175
+ // 更新活跃时间
176
+ threadLastActive.set(ctx.threadId, Date.now());
177
+
178
+
179
179
 
180
180
  const shareUrl = await shareSession(openCodeSession);
181
- const filesCount = (session.modifiedFiles?.length || session.modifiedFiles?.size || 0);
182
- if (filesCount > 0 && shareUrl) {
181
+ if (shareUrl) {
183
182
  try {
184
183
  await adapter.reply(ctx.threadId, `🔗 ${shareUrl}`);
185
184
  } catch (e) {
@@ -187,11 +186,10 @@ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session,
187
186
  }
188
187
  }
189
188
 
190
- saveSessionMapping();
191
189
  }
192
190
 
193
191
  async function handleMessage(adapter, ctx, text, openCodeSessions) {
194
- const session = await getOrCreateSession(ctx.threadId, 'weixin');
192
+ const session = {};
195
193
 
196
194
  const expertTriggers = ['z', 'Z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review', '专家会诊', '团队评审', '代码审查', '全员review', 'review all', '请专家', '叫专家', '找专家'];
197
195
  const trimmedLower = text.trim().toLowerCase();
@@ -209,342 +207,92 @@ async function handleMessage(adapter, ctx, text, openCodeSessions) {
209
207
  expertPrompt = EXPERT_SYSTEM_PROMPT;
210
208
  await adapter.reply(ctx.threadId, '✅ 专家评审已启动');
211
209
  }
212
- // /z 直接走专家模式,不继续向下走
213
- await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session, expertPrompt);
210
+ await forwardToOpenCode(adapter, ctx, text, openCodeSessions, expertPrompt);
214
211
  return;
215
212
  }
216
213
  if (expertTriggers.some(t => trimmedLower.includes(t))) {
217
214
  expertPrompt = EXPERT_SYSTEM_PROMPT;
218
215
  }
219
216
 
217
+ // 自主开发决策回复拦截
218
+ if (hasPendingDecision(ctx.threadId)) {
219
+ if (/^\d+$/.test(text.trim())) {
220
+ if (resolveDecision(ctx.threadId, text.trim())) return;
221
+ }
222
+ if (/^\/(\d+)$/.test(text.trim())) {
223
+ if (resolveDecision(ctx.threadId, text.trim().slice(1))) return;
224
+ }
225
+ }
226
+
220
227
  const detected = detectCommand(text);
221
228
  if (detected) {
222
- const handled = await handleCommand(adapter, ctx, detected.name, detected.arg, openCodeSessions);
229
+ const cmdAdapter = isSharedMember(ctx.threadId) ? wrapAdapterForShared(adapter, ctx.threadId) : adapter;
230
+ const handled = await handleCommand(cmdAdapter, ctx, detected.name, detected.arg, openCodeSessions);
223
231
  if (handled) return;
224
232
  }
225
233
 
226
234
  if (!isAuthorized('weixin', ctx.userId)) {
227
235
  if (!hasOwner('weixin')) {
228
236
  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
237
  return;
367
238
  }
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);
239
+ // 共享成员跳过认证
240
+ if (!isSharedMember(ctx.threadId)) {
241
+ await adapter.reply(ctx.threadId, '🚫 你无权使用此 bot');
501
242
  return;
502
243
  }
503
- session._analyzeMode = false;
504
- session._analyzeTask = null;
505
244
  }
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
245
 
526
- if (session.currentAgent && session.currentAgent !== 'opencode') {
527
- const agent = registry.findAgent(session.currentAgent);
246
+ // 活跃 agent 路由
247
+ const activeAgentName = threadAgent.get(ctx.threadId);
248
+ if (activeAgentName && activeAgentName !== 'opencode') {
249
+ const agent = registry.findAgent(activeAgentName);
528
250
  if (agent) {
529
251
  const available = await agent.isAvailable().catch(() => false);
530
252
  if (available) {
531
- await adapter.sendTyping?.(ctx.threadId, true);
253
+ const t0 = Date.now();
254
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
255
+ console.log(`[→${activeAgentName}] ${text}`);
256
+ console.log(`──────────────────────────────────────────────────────`);
257
+
258
+ // 构建历史记录
259
+ const history = threadHistory.get(ctx.threadId) || [];
260
+ const maxHistory = 20;
261
+ const recentHistory = history.slice(-maxHistory);
262
+
532
263
  try {
533
- const response = await agent.sendPrompt(session.id, text, session.commandHistory || [], { projectDir: session.projectDir || globalThis.__autoProjectDir });
534
- await adapter.sendTyping?.(ctx.threadId, false);
264
+ const response = await agent.sendPrompt(activeAgentName, text, recentHistory, { projectDir: globalThis.__autoProjectDir });
265
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
266
+ console.log(`[←${activeAgentName}] ⏱ ${elapsed}s | chars: ${(response || '').length}`);
267
+ console.log(`[RESPONSE] ${(response || '').slice(0, 200)}${(response || '').length > 200 ? '...' : ''}`);
268
+
269
+ // 更新历史
270
+ history.push({ role: 'user', content: text }, { role: 'assistant', content: response || '' });
271
+ threadHistory.set(ctx.threadId, history);
272
+
535
273
  const chunks = splitMessage(response || '无响应');
536
274
  for (const chunk of chunks) {
537
275
  await adapter.reply(ctx.threadId, chunk);
538
276
  }
539
277
  } catch (error) {
540
- await adapter.reply(ctx.threadId, `❌ ${session.currentAgent} 错误: ${error.message}`);
278
+ console.error(`[${activeAgentName}] ${error.message}`);
279
+ await adapter.reply(ctx.threadId, `❌ ${activeAgentName} 错误: ${error.message}`);
541
280
  }
542
281
  return;
543
282
  }
283
+ console.warn(`[agent] ${activeAgentName} 不可用,回退到 OpenCode`);
544
284
  }
285
+ // agent 不可用,清除状态回退到 OpenCode
286
+ threadAgent.delete(ctx.threadId);
287
+ }
288
+
289
+ const connected = await checkConnection();
290
+ if (!connected) {
291
+ await adapter.reply(ctx.threadId, '❌ OpenCode 离线,请检查服务是否运行');
292
+ return;
545
293
  }
546
294
 
547
- await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
295
+ await forwardToOpenCode(adapter, ctx, text, openCodeSessions);
548
296
  }
549
297
 
550
- export { handleMessage, forwardToOpenCode, startLoopCycle };
298
+ export { handleMessage, forwardToOpenCode };