@yvhitxcel/opencode-remote 0.15.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.
Files changed (102) hide show
  1. package/README.md +82 -0
  2. package/bin/opencode-remote.js +70 -0
  3. package/bin/opencode-weixin.js +10 -0
  4. package/dist/AGENTS.md +20 -0
  5. package/dist/MEMORY.md +21 -0
  6. package/dist/bot-runner.js +180 -0
  7. package/dist/cli.js +256 -0
  8. package/dist/core/approval.js +95 -0
  9. package/dist/core/auth.js +119 -0
  10. package/dist/core/config.js +61 -0
  11. package/dist/core/notifications.js +134 -0
  12. package/dist/core/qiniu.js +267 -0
  13. package/dist/core/registry.js +86 -0
  14. package/dist/core/router.js +344 -0
  15. package/dist/core/session.js +403 -0
  16. package/dist/core/setup.js +418 -0
  17. package/dist/core/types.js +16 -0
  18. package/dist/feishu/adapter.js +72 -0
  19. package/dist/feishu/bot.js +168 -0
  20. package/dist/feishu/commands.js +601 -0
  21. package/dist/feishu/handler.js +380 -0
  22. package/dist/index.js +60 -0
  23. package/dist/opencode/client.js +823 -0
  24. package/dist/package-lock.json +762 -0
  25. package/dist/patch_spawn.js +28 -0
  26. package/dist/plugins/agents/acp/acp-adapter.js +42 -0
  27. package/dist/plugins/agents/claude-code/index.js +69 -0
  28. package/dist/plugins/agents/codex/index.js +44 -0
  29. package/dist/plugins/agents/copilot/index.js +44 -0
  30. package/dist/plugins/agents/opencode/index.js +66 -0
  31. package/dist/telegram/bot.js +288 -0
  32. package/dist/utils/message-split.js +38 -0
  33. package/dist/web/code-viewer.js +266 -0
  34. package/dist/weixin/adapter.js +135 -0
  35. package/dist/weixin/api.js +179 -0
  36. package/dist/weixin/bot.js +183 -0
  37. package/dist/weixin/commands.js +758 -0
  38. package/dist/weixin/handler.js +577 -0
  39. package/dist/weixin/node_modules/encodeurl/LICENSE +22 -0
  40. package/dist/weixin/node_modules/encodeurl/README.md +109 -0
  41. package/dist/weixin/node_modules/encodeurl/index.js +60 -0
  42. package/dist/weixin/node_modules/encodeurl/package.json +40 -0
  43. package/dist/weixin/node_modules/qiniu/.claude/settings.local.json +7 -0
  44. package/dist/weixin/node_modules/qiniu/.github/workflows/ci-test.yml +36 -0
  45. package/dist/weixin/node_modules/qiniu/.github/workflows/npm-publish.yml +20 -0
  46. package/dist/weixin/node_modules/qiniu/.github/workflows/version-check.yml +19 -0
  47. package/dist/weixin/node_modules/qiniu/.idea/MarsCodeWorkspaceAppSettings.xml +7 -0
  48. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/Project.xml +44 -0
  49. package/dist/weixin/node_modules/qiniu/.idea/codeStyles/codeStyleConfig.xml +5 -0
  50. package/dist/weixin/node_modules/qiniu/.idea/git_toolbox_blame.xml +6 -0
  51. package/dist/weixin/node_modules/qiniu/.idea/inspectionProfiles/Project_Default.xml +6 -0
  52. package/dist/weixin/node_modules/qiniu/.idea/jsLibraryMappings.xml +6 -0
  53. package/dist/weixin/node_modules/qiniu/.idea/modules.xml +8 -0
  54. package/dist/weixin/node_modules/qiniu/.idea/nodejs-sdk.iml +12 -0
  55. package/dist/weixin/node_modules/qiniu/.idea/vcs.xml +6 -0
  56. package/dist/weixin/node_modules/qiniu/CHANGELOG.md +292 -0
  57. package/dist/weixin/node_modules/qiniu/README.md +56 -0
  58. package/dist/weixin/node_modules/qiniu/StorageResponseInterface.d.ts +239 -0
  59. package/dist/weixin/node_modules/qiniu/codecov.yml +28 -0
  60. package/dist/weixin/node_modules/qiniu/index.d.ts +1995 -0
  61. package/dist/weixin/node_modules/qiniu/index.js +32 -0
  62. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/HISTORY.md +14 -0
  63. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/LICENSE +22 -0
  64. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/README.md +128 -0
  65. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/index.js +60 -0
  66. package/dist/weixin/node_modules/qiniu/node_modules/encodeurl/package.json +40 -0
  67. package/dist/weixin/node_modules/qiniu/package.json +80 -0
  68. package/dist/weixin/node_modules/qiniu/qiniu/auth/digest.js +13 -0
  69. package/dist/weixin/node_modules/qiniu/qiniu/cdn.js +149 -0
  70. package/dist/weixin/node_modules/qiniu/qiniu/conf.js +254 -0
  71. package/dist/weixin/node_modules/qiniu/qiniu/fop.js +112 -0
  72. package/dist/weixin/node_modules/qiniu/qiniu/httpc/client.js +253 -0
  73. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpoint.js +66 -0
  74. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsProvider.js +27 -0
  75. package/dist/weixin/node_modules/qiniu/qiniu/httpc/endpointsRetryPolicy.js +76 -0
  76. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/base.js +31 -0
  77. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/index.js +9 -0
  78. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/qiniuAuth.js +53 -0
  79. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/retryDomains.js +101 -0
  80. package/dist/weixin/node_modules/qiniu/qiniu/httpc/middleware/ua.js +36 -0
  81. package/dist/weixin/node_modules/qiniu/qiniu/httpc/region.js +349 -0
  82. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsProvider.js +788 -0
  83. package/dist/weixin/node_modules/qiniu/qiniu/httpc/regionsRetryPolicy.js +242 -0
  84. package/dist/weixin/node_modules/qiniu/qiniu/httpc/responseWrapper.js +40 -0
  85. package/dist/weixin/node_modules/qiniu/qiniu/retry/index.js +4 -0
  86. package/dist/weixin/node_modules/qiniu/qiniu/retry/retrier.js +99 -0
  87. package/dist/weixin/node_modules/qiniu/qiniu/retry/retryPolicy.js +55 -0
  88. package/dist/weixin/node_modules/qiniu/qiniu/rpc.js +237 -0
  89. package/dist/weixin/node_modules/qiniu/qiniu/rtc/app.js +123 -0
  90. package/dist/weixin/node_modules/qiniu/qiniu/rtc/credentials.js +57 -0
  91. package/dist/weixin/node_modules/qiniu/qiniu/rtc/room.js +118 -0
  92. package/dist/weixin/node_modules/qiniu/qiniu/rtc/util.js +16 -0
  93. package/dist/weixin/node_modules/qiniu/qiniu/sms/message.js +58 -0
  94. package/dist/weixin/node_modules/qiniu/qiniu/storage/form.js +442 -0
  95. package/dist/weixin/node_modules/qiniu/qiniu/storage/internal.js +214 -0
  96. package/dist/weixin/node_modules/qiniu/qiniu/storage/resume.js +1272 -0
  97. package/dist/weixin/node_modules/qiniu/qiniu/storage/rs.js +1764 -0
  98. package/dist/weixin/node_modules/qiniu/qiniu/util.js +382 -0
  99. package/dist/weixin/node_modules/qiniu/qiniu/zone.js +230 -0
  100. package/dist/weixin/node_modules/qiniu/tsconfig.json +112 -0
  101. package/dist/weixin/types.js +25 -0
  102. package/package.json +56 -0
@@ -0,0 +1,577 @@
1
+ import { getOrCreateSession, updateSession, loadSessionMapping, saveSessionMapping, getThreadsBySessionIdFromMapping, saveSessionCommandHistory, sessionManager } from '../core/session.js';
2
+ import { splitMessage } from '../core/notifications.js';
3
+ import { initOpenCode, createSession, sendMessage as sendToOpenCode, checkConnection, resumeSession, shareSession, forkSession } from '../opencode/client.js';
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 } from '../core/router.js';
9
+ import { handleCommand, formatTimeAgo, _registerStartLoopCycle } from './commands.js';
10
+
11
+ const EXPERT_SYSTEM_PROMPT = `你是一个专家角色扮演系统,严格按照 AGENTS.md 中的"专家点评系统"流程执行。
12
+
13
+ 当用户输入包含触发词(z / 叫全部专家 / 叫所有专家 / 呼叫专家点评 / 专家点评 / 专家意见 / call all experts / expert review)时,启动专家评审。
14
+
15
+ ## 规则
16
+ - 严格遵循 AGENTS.md 中定义的 13 位角色和点评流程
17
+ - 言辞必须苛刻犀利,不讨好不委婉
18
+ - 不说客套话
19
+ - 直接指出问题`;
20
+
21
+ async function startLoopCycle(adapter, ctx, openCodeSessions, session) {
22
+ if (!session.loopMode) return;
23
+
24
+ const now = Date.now();
25
+ const iteration = (session.loopIterationCount || 0) + 1;
26
+ const maxIterations = session.loopMaxIterations || 10;
27
+ const maxTimeMs = session.loopMaxTimeMs || 30 * 60 * 1000;
28
+ const startTime = session.loopStartTime || now;
29
+
30
+ if (iteration > maxIterations) {
31
+ session.loopMode = false;
32
+ await adapter.reply(ctx.threadId, `⏹️ 循环任务已完成(达到最大迭代次数 ${maxIterations})`);
33
+ return;
34
+ }
35
+
36
+ if (now - startTime > maxTimeMs) {
37
+ session.loopMode = false;
38
+ await adapter.reply(ctx.threadId, `⏹️ 循环任务已停止(达到最大运行时长 ${Math.floor(maxTimeMs / 60000)}分钟)`);
39
+ return;
40
+ }
41
+
42
+ if (session.lastUserMessage && now - session.lastUserMessage < 120000) {
43
+ session.lastLoopTime = now;
44
+ setTimeout(() => startLoopCycle(adapter, ctx, openCodeSessions, session), 30000);
45
+ return;
46
+ }
47
+
48
+ session.loopIterationCount = iteration;
49
+ const prompt = session.loopPrompt || '根据当前项目状态,继续推进未完成的工作';
50
+ try {
51
+ await adapter.reply(ctx.threadId, `🔄 循环执行 [${iteration}/${maxIterations}]: ${prompt}`);
52
+ await forwardToOpenCode(adapter, ctx, prompt, openCodeSessions, session);
53
+ session.lastLoopTime = now;
54
+ setTimeout(() => startLoopCycle(adapter, ctx, openCodeSessions, session), 5 * 60 * 1000);
55
+ } catch (e) {
56
+ console.error('Loop cycle error:', e);
57
+ session.loopMode = false;
58
+ await adapter.reply(ctx.threadId, `❌ 循环任务因错误停止: ${e.message}`);
59
+ }
60
+ }
61
+
62
+ _registerStartLoopCycle(startLoopCycle);
63
+
64
+ async function forwardToOpenCode(adapter, ctx, text, openCodeSessions, session) {
65
+ adapter.sendTypingIndicator(ctx.threadId).catch(() => {});
66
+ let openCodeSession = openCodeSessions.get(ctx.threadId);
67
+ if (!openCodeSession) {
68
+ if (session.opencodeSessionId) {
69
+ openCodeSession = await resumeSession(session.opencodeSessionId);
70
+ }
71
+ if (!openCodeSession && globalThis.__latestOpenCodeSession?.id) {
72
+ console.log('Connecting to latest OpenCode session...');
73
+ openCodeSession = await resumeSession(globalThis.__latestOpenCodeSession.id);
74
+ }
75
+ if (!openCodeSession) {
76
+ const mapping = loadSessionMapping();
77
+ const saved = mapping[ctx.threadId];
78
+ if (saved?.opencodeSessionId) {
79
+ openCodeSession = await resumeSession(saved.opencodeSessionId);
80
+ }
81
+ }
82
+ if (!openCodeSession) {
83
+ console.log('Creating new WeChat session...');
84
+ openCodeSession = await createSession(ctx.threadId, `Weixin ${ctx.threadId}`);
85
+ if (!openCodeSession) {
86
+ await adapter.reply(ctx.threadId, '❌ 无法创建 OpenCode 会话');
87
+ return;
88
+ }
89
+ console.log(`✅ Created new WeChat session: ${openCodeSession.sessionId}`);
90
+ }
91
+ openCodeSessions.set(ctx.threadId, openCodeSession);
92
+ session.opencodeSessionId = openCodeSession.sessionId;
93
+ const key = `weixin:${ctx.threadId}:${ctx.threadId}`;
94
+ sessionManager.saveSession(key, session).catch(() => {});
95
+ saveSessionMapping();
96
+ }
97
+
98
+ if (session.modelOverride) {
99
+ openCodeSession.model = session.modelOverride;
100
+ }
101
+
102
+ session.taskStartTime = Date.now();
103
+ session.currentTool = null;
104
+
105
+ let lastToolNotified = '';
106
+ const stopHeartbeat = () => {
107
+ session.taskStartTime = null;
108
+ session.currentTool = null;
109
+ if (session.modifiedFiles instanceof Set) {
110
+ session.modifiedFiles = Array.from(session.modifiedFiles);
111
+ }
112
+ };
113
+
114
+ console.log(`📤 Message sent: → ${text}`);
115
+
116
+ const projectDir = session.projectDir || globalThis.__autoProjectDir;
117
+
118
+ let scopedText = text;
119
+ if (session._contextScope) {
120
+ scopedText = `[上下文范围: ${session._contextScope}]\n\n${text}`;
121
+ }
122
+
123
+ if (projectDir && !scopedText.includes('项目目录')) {
124
+ scopedText = `[当前项目目录: ${projectDir}]\n\n${scopedText}`;
125
+ }
126
+
127
+ if (session.expertMode && session.systemPrompt) {
128
+ scopedText = `${session.systemPrompt}\n\n${scopedText}`;
129
+ }
130
+
131
+ let response = '';
132
+ let hasToolActivity = false;
133
+ const IDLE_TIMEOUT = session.expertMode ? 120000 : 30000;
134
+
135
+ const result = await sendToOpenCode(openCodeSession, scopedText, {
136
+ onEvent: (event) => {
137
+ if (event.type === 'tool.call') {
138
+ const props = event.properties || {};
139
+ const toolName = props.name || props.tool_name || 'unknown';
140
+ const input = props.input || {};
141
+
142
+ hasToolActivity = true;
143
+
144
+ let toolDesc = `🔧 执行工具: ${toolName}`;
145
+ if (input.path) toolDesc += `\n📁 ${input.path}`;
146
+ if (input.command) toolDesc += `\n💻 ${input.command}`;
147
+
148
+ adapter.reply(ctx.threadId, toolDesc).catch(() => {});
149
+
150
+ if (input.path) {
151
+ if (!session.modifiedFiles) session.modifiedFiles = new Set();
152
+ session.modifiedFiles.add(input.path);
153
+ }
154
+ }
155
+ },
156
+ onStatusChange: (status) => {
157
+ if (status.hasToolActivity) hasToolActivity = true;
158
+ },
159
+ }).catch((e) => {
160
+ console.error('[forwardToOpenCode] Task error:', e.message);
161
+ return '';
162
+ });
163
+
164
+ stopHeartbeat();
165
+
166
+ const trimmedResponse = (result || response).trim();
167
+ if (!trimmedResponse) {
168
+ console.error(`[forwardToOpenCode] Empty response (race: ${whoWon})`);
169
+ const msg = whoWon === 'idle' ? '⏰ OpenCode 响应超时,请重试' : 'AI 返回空响应,请重试';
170
+ await adapter.reply(ctx.threadId, msg);
171
+ return;
172
+ }
173
+
174
+ if (trimmedResponse.startsWith('⏰') || trimmedResponse.startsWith('❌')) {
175
+ console.error('[forwardToOpenCode] Error response:', trimmedResponse);
176
+ await adapter.reply(ctx.threadId, trimmedResponse);
177
+ return;
178
+ }
179
+
180
+ const responseMsgs = splitMessage(trimmedResponse);
181
+ for (const m of responseMsgs) {
182
+ const trimmed = m.trim();
183
+ if (!trimmed) continue;
184
+ try {
185
+ await adapter.reply(ctx.threadId, m);
186
+ } catch (replyErr) {
187
+ console.error('[forwardToOpenCode] reply failed:', replyErr.message);
188
+ }
189
+ }
190
+
191
+ const allThreads = getThreadsBySessionIdFromMapping(openCodeSession.sessionId);
192
+ for (const otherThreadId of allThreads) {
193
+ if (otherThreadId === ctx.threadId) continue;
194
+ const otherContextToken = adapter.contextTokens.get(otherThreadId);
195
+ if (!otherContextToken) continue;
196
+ try {
197
+ await sendWeixinMessage({
198
+ baseUrl: adapter._baseUrl,
199
+ token: adapter._token,
200
+ 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${response}` } }] } }
201
+ });
202
+ } catch (e) {
203
+ console.error(`Failed to broadcast to ${otherThreadId}:`, e.message);
204
+ }
205
+ }
206
+
207
+ const shareUrl = await shareSession(openCodeSession);
208
+ const filesCount = (session.modifiedFiles?.length || session.modifiedFiles?.size || 0);
209
+ if (filesCount > 0 && shareUrl) {
210
+ try {
211
+ await adapter.reply(ctx.threadId, `🔗 ${shareUrl}`);
212
+ } catch (e) {
213
+ console.error('[forwardToOpenCode] share URL reply failed:', e.message);
214
+ }
215
+ }
216
+
217
+ saveSessionMapping();
218
+ }
219
+
220
+ async function handleMessage(adapter, ctx, text, openCodeSessions) {
221
+ const session = await getOrCreateSession(ctx.threadId, 'weixin');
222
+
223
+ const expertTriggers = ['z', '叫全部专家', '叫所有专家', '呼叫专家点评', '专家点评', '专家意见', 'call all experts', 'expert review'];
224
+ const trimmedLower = text.trim().toLowerCase();
225
+ if (text.startsWith('/z')) {
226
+ const arg = text.slice(2).trim();
227
+ if (arg === 'off' || arg === 'reset' || arg === '关闭') {
228
+ session.expertMode = false;
229
+ session.systemPrompt = null;
230
+ await adapter.reply(ctx.threadId, '⏹️ 专家模式已关闭');
231
+ return;
232
+ }
233
+ if (arg) {
234
+ session.expertMode = true;
235
+ session.systemPrompt = arg;
236
+ await adapter.reply(ctx.threadId, `✅ 自定义专家 prompt 已设置 (${arg.length}字)`);
237
+ return;
238
+ }
239
+ if (!session.expertMode) {
240
+ session.expertMode = true;
241
+ session.systemPrompt = EXPERT_SYSTEM_PROMPT;
242
+ }
243
+ await adapter.reply(ctx.threadId, '✅ 专家模式已启动,直接发送你的问题\n/z off — 关闭\n/z <内容> — 自定义 prompt');
244
+ return;
245
+ }
246
+
247
+ const detected = detectCommand(text);
248
+ if (detected) {
249
+ const handled = await handleCommand(adapter, ctx, detected.name, detected.arg, openCodeSessions);
250
+ if (handled) return;
251
+ }
252
+
253
+ if (!isAuthorized('weixin', ctx.userId)) {
254
+ if (!hasOwner('weixin')) {
255
+ await adapter.reply(ctx.threadId, '🔐 请先发送 /start 进行安全认证');
256
+ } else {
257
+ await adapter.reply(ctx.threadId, '🚫 你无权使用此 bot');
258
+ }
259
+ return;
260
+ }
261
+
262
+ if (session._deleteSessionList) {
263
+ const trimmed = text.trim();
264
+ if (/^\d+$/.test(trimmed)) {
265
+ const num = parseInt(trimmed, 10);
266
+ if (num >= 1 && num <= session._deleteSessionList.length) {
267
+ const target = session._deleteSessionList[num - 1];
268
+ try {
269
+ const opencode = await initOpenCode();
270
+ if (!opencode) {
271
+ await adapter.reply(ctx.threadId, '❌ 无法删除会话');
272
+ return;
273
+ }
274
+ await opencode.client.session.delete({ path: { id: target.id } });
275
+ if (session.opencodeSessionId === target.id) {
276
+ openCodeSessions.delete(ctx.threadId);
277
+ session.opencodeSessionId = undefined;
278
+ saveSessionMapping();
279
+ }
280
+ const result = await opencode.client.session.list();
281
+ if (!result.error && result.data) {
282
+ session._deleteSessionList = result.data;
283
+ if (result.data.length === 0) {
284
+ session._deleteSessionList = null;
285
+ await adapter.reply(ctx.threadId, `🗑️ 已删除: ${target.title || '无标题'}\n📭 没有更多会话了`);
286
+ } else {
287
+ let msg = `🗑️ 已删除: ${target.title || '无标题'}\n\n📂 选择要删除的会话(回复编号):\n\n`;
288
+ result.data.forEach((s, i) => {
289
+ const n = i + 1;
290
+ const title = s.title || '无标题';
291
+ let status = '';
292
+ if (typeof s.status === 'string') status = s.status;
293
+ else if (s.status?.type) status = s.status.type;
294
+ const time = s.updated_at ? formatTimeAgo(s.updated_at * 1000) : '';
295
+ msg += `${n}. ${title} (${status || '空闲'} ${time})\n`;
296
+ });
297
+ msg += '\n回复编号删除';
298
+ const msgs = splitMessage(msg);
299
+ for (const m of msgs) {
300
+ await adapter.reply(ctx.threadId, m);
301
+ }
302
+ }
303
+ } else {
304
+ session._deleteSessionList = null;
305
+ await adapter.reply(ctx.threadId, `🗑️ 已删除: ${target.title || '无标题'}`);
306
+ }
307
+ } catch (e) {
308
+ await adapter.reply(ctx.threadId, `❌ 删除失败: ${e.message}`);
309
+ }
310
+ } else {
311
+ await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._deleteSessionList.length}`);
312
+ }
313
+ return;
314
+ }
315
+ }
316
+
317
+ if (session._switchSessionList) {
318
+ const trimmed = text.trim();
319
+ if (/^\d+$/.test(trimmed)) {
320
+ const num = parseInt(trimmed, 10);
321
+ if (num >= 1 && num <= session._switchSessionList.length) {
322
+ const target = session._switchSessionList[num - 1];
323
+ if (session._showSessionState) {
324
+ session._showSessionState = null;
325
+ session._pendingSwitchSession = target;
326
+ const targetDir = target.directory || process.cwd();
327
+ let stateMsg = `📋 会话: ${target.title || '无标题'}\n`;
328
+ stateMsg += `ID: ${target.id.slice(0, 8)}...\n`;
329
+ if (target.directory) stateMsg += `📁 目录: ${target.directory}\n`;
330
+ stateMsg += '\n';
331
+ try {
332
+ const { readFileSync, existsSync } = await import('fs');
333
+ const { join } = await import('path');
334
+ const memoryPath = join(targetDir, 'MEMORY.md');
335
+ if (existsSync(memoryPath)) {
336
+ const content = readFileSync(memoryPath, 'utf-8');
337
+ const summaryMatch = content.match(/## 最近会话摘要\n([\s\S]*?)(?=##|$)/);
338
+ if (summaryMatch) {
339
+ const lines = summaryMatch[1].trim().split('\n').filter(l => l.trim());
340
+ const recent = lines.slice(-3);
341
+ if (recent.length > 0) {
342
+ stateMsg += `📝 最近会话:\n${recent.map(l => ` ${l.trim()}`).join('\n')}\n\n`;
343
+ }
344
+ }
345
+ const threadsMatch = content.match(/## 开放线程\n([\s\S]*?)(?=##|$)/);
346
+ if (threadsMatch && threadsMatch[1].trim()) {
347
+ stateMsg += `🔓 开放线程:\n${threadsMatch[1].trim().slice(0, 300)}\n\n`;
348
+ }
349
+ }
350
+ const today = new Date().toISOString().slice(0, 10);
351
+ const dailyLogPath = join(targetDir, 'daily-logs', `${today}.md`);
352
+ if (existsSync(dailyLogPath)) {
353
+ const logContent = readFileSync(dailyLogPath, 'utf-8');
354
+ const lastEntry = logContent.split('## ').pop();
355
+ if (lastEntry && lastEntry.trim()) {
356
+ stateMsg += `📅 今日日志:\n${lastEntry.trim().slice(0, 300)}`;
357
+ }
358
+ }
359
+ } catch { console.debug('[session-switch] Failed to read daily log'); }
360
+ stateMsg += `\n💡 回复 "确认" 切换到此会话,或回复其他内容取消`;
361
+ await adapter.reply(ctx.threadId, stateMsg);
362
+ return;
363
+ }
364
+
365
+ try {
366
+ const resumed = await resumeSession(target.id);
367
+ if (resumed) {
368
+ openCodeSessions.set(ctx.threadId, resumed);
369
+ session.opencodeSessionId = resumed.sessionId;
370
+ session.taskStartTime = null;
371
+ session.currentTool = null;
372
+ session.modifiedFiles = null;
373
+ const key = `weixin:${ctx.userId}:${ctx.threadId}`;
374
+ sessionManager.saveSession(key, session).catch(() => {});
375
+ saveSessionMapping();
376
+ if (target.directory) {
377
+ session.projectDir = target.directory;
378
+ globalThis.__autoProjectDir = target.directory;
379
+ }
380
+ await adapter.reply(ctx.threadId, `✅ 已切换到: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
381
+ } else {
382
+ await adapter.reply(ctx.threadId, `❌ 切换失败`);
383
+ }
384
+ } catch (e) {
385
+ await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
386
+ }
387
+ } else {
388
+ await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._switchSessionList.length}`);
389
+ }
390
+ session._switchSessionList = null;
391
+ session._showSessionState = null;
392
+ session._pendingSwitchSession = null;
393
+ return;
394
+ }
395
+ session._switchSessionList = null;
396
+ session._showSessionState = null;
397
+ session._pendingSwitchSession = null;
398
+ }
399
+
400
+ if (session._pendingSwitchSession) {
401
+ const trimmed = text.trim().toLowerCase();
402
+ const target = session._pendingSwitchSession;
403
+ session._pendingSwitchSession = null;
404
+ if (trimmed === '确认' || trimmed === 'confirm' || trimmed === 'y' || trimmed === '1') {
405
+ try {
406
+ const resumed = await resumeSession(target.id);
407
+ if (resumed) {
408
+ openCodeSessions.set(ctx.threadId, resumed);
409
+ session.opencodeSessionId = resumed.sessionId;
410
+ session.taskStartTime = null;
411
+ session.currentTool = null;
412
+ session.modifiedFiles = null;
413
+ saveSessionMapping();
414
+ if (target.directory) {
415
+ session.projectDir = target.directory;
416
+ globalThis.__autoProjectDir = target.directory;
417
+ }
418
+ await adapter.reply(ctx.threadId, `✅ 已切换到: ${target.title || '无标题'}\nID: ${resumed.sessionId.slice(0, 8)}...`);
419
+ } else {
420
+ await adapter.reply(ctx.threadId, `❌ 切换失败`);
421
+ }
422
+ } catch (e) {
423
+ await adapter.reply(ctx.threadId, `❌ 切换失败: ${e.message}`);
424
+ }
425
+ return;
426
+ }
427
+ await adapter.reply(ctx.threadId, '已取消切换');
428
+ return;
429
+ }
430
+
431
+ if (session._historyList) {
432
+ const trimmed = text.trim();
433
+ if (/^\d+$/.test(trimmed)) {
434
+ const num = parseInt(trimmed, 10);
435
+ if (num >= 1 && num <= session._historyList.length) {
436
+ const cmd = session._historyList[session._historyList.length - num];
437
+ session._historyList = null;
438
+ session._lastPrompt = cmd;
439
+ await forwardToOpenCode(adapter, ctx, cmd, openCodeSessions, session);
440
+ return;
441
+ } else {
442
+ await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._historyList.length}`);
443
+ return;
444
+ }
445
+ }
446
+ session._historyList = null;
447
+ }
448
+
449
+ if (session._forkList) {
450
+ const trimmed = text.trim();
451
+ if (/^\d+$/.test(trimmed)) {
452
+ const num = parseInt(trimmed, 10);
453
+ if (num >= 1 && num <= session._forkList.length) {
454
+ const targetMsg = session._forkList[num - 1];
455
+ const forked = await forkSession(session._forkSessionId, targetMsg.id, session.projectDir);
456
+ session._forkList = null;
457
+ session._forkSessionId = null;
458
+ if (forked) {
459
+ openCodeSessions.set(ctx.threadId, forked);
460
+ session.opencodeSessionId = forked.sessionId;
461
+ session.taskStartTime = null;
462
+ session.currentTool = null;
463
+ session.modifiedFiles = null;
464
+ saveSessionMapping();
465
+ await adapter.reply(ctx.threadId, `🔀 已从消息 #${num} 创建分支\n\n新会话: ${forked.sessionId.slice(0, 8)}...\n之前的上下文已保留`);
466
+ } else {
467
+ await adapter.reply(ctx.threadId, '❌ 分支失败');
468
+ }
469
+ return;
470
+ } else {
471
+ await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._forkList.length}`);
472
+ return;
473
+ }
474
+ }
475
+ session._forkList = null;
476
+ session._forkSessionId = null;
477
+ }
478
+
479
+ if (session._editList) {
480
+ const trimmed = text.trim();
481
+ if (/^\d+$/.test(trimmed)) {
482
+ const num = parseInt(trimmed, 10);
483
+ if (num >= 1 && num <= session._editList.length) {
484
+ const targetMsg = session._editList[num - 1];
485
+ const preview = targetMsg.info?.content?.slice(0, 80) || '(空)';
486
+ session._editTarget = { sessionId: session._editSessionId, messageID: targetMsg.id, num };
487
+ session._editList = null;
488
+ session._editSessionId = null;
489
+ await adapter.reply(ctx.threadId, `✏️ 选择修改消息 #${num}:\n\n${preview}\n\n请发送修正后的内容,将从该消息之前创建新分支`);
490
+ return;
491
+ } else {
492
+ await adapter.reply(ctx.threadId, `❌ 无效编号,请输入 1-${session._editList.length}`);
493
+ return;
494
+ }
495
+ }
496
+ session._editList = null;
497
+ session._editSessionId = null;
498
+ }
499
+
500
+ if (session._editTarget) {
501
+ const target = session._editTarget;
502
+ session._editTarget = null;
503
+ const forked = await forkSession(target.sessionId, target.messageID, session.projectDir);
504
+ if (forked) {
505
+ openCodeSessions.set(ctx.threadId, forked);
506
+ session.opencodeSessionId = forked.sessionId;
507
+ session.taskStartTime = null;
508
+ session.currentTool = null;
509
+ session.modifiedFiles = null;
510
+ saveSessionMapping();
511
+ await adapter.reply(ctx.threadId, `✅ 已创建新分支并发送修正内容\n\n从消息 #${target.num} 之前分支,发送了新提示`);
512
+ await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
513
+ } else {
514
+ await adapter.reply(ctx.threadId, '❌ 分支失败');
515
+ }
516
+ return;
517
+ }
518
+
519
+ if (session._analyzeMode) {
520
+ const trimmed = text.trim().toLowerCase();
521
+ if (trimmed === '执行' || trimmed === 'execute' || trimmed === '开始' || trimmed === 'go') {
522
+ session._analyzeMode = false;
523
+ const task = session._analyzeTask || text;
524
+ session._analyzeTask = null;
525
+ await adapter.reply(ctx.threadId, `🔧 开始执行: ${task}\n\nAI 将实施最小改动,完成后列出变更点和验证步骤。`);
526
+ const execPrompt = `现在开始施工队模式。任务:${task}\n\n请严格执行以下步骤:\n\n1. 实施最小改动:只修改必要的代码,不要重构其他地方\n2. 保持风格一致:遵循项目现有的代码风格\n3. 列出变更点:修改完成后,列出所有改动的文件和代码\n4. 验证步骤:说明需要运行哪些测试或检查来验证修改\n5. 列出未验证的部分:说明还有哪些边界情况需要人工检查\n\n注意:\n- 只做最小改动\n- 不要顺手重构\n- 不要修改不相关的文件`;
527
+ await forwardToOpenCode(adapter, ctx, execPrompt, openCodeSessions, session);
528
+ return;
529
+ }
530
+ session._analyzeMode = false;
531
+ session._analyzeTask = null;
532
+ }
533
+
534
+ const connected = await checkConnection();
535
+ if (!connected) {
536
+ await adapter.reply(ctx.threadId, '❌ OpenCode 离线,请检查服务是否运行');
537
+ return;
538
+ }
539
+
540
+ if (!session.commandHistory) {
541
+ session.commandHistory = [];
542
+ }
543
+ session.commandHistory.push(text);
544
+ if (session.commandHistory.length > 50) {
545
+ session.commandHistory = session.commandHistory.slice(-50);
546
+ }
547
+
548
+ await saveSessionCommandHistory(ctx.threadId, session.commandHistory);
549
+
550
+ session.lastUserMessage = Date.now();
551
+ session._lastPrompt = text;
552
+
553
+ if (session.currentAgent && session.currentAgent !== 'opencode') {
554
+ const agent = registry.findAgent(session.currentAgent);
555
+ if (agent) {
556
+ const available = await agent.isAvailable().catch(() => false);
557
+ if (available) {
558
+ await adapter.sendTyping?.(ctx.threadId, true);
559
+ try {
560
+ const response = await agent.sendPrompt(session.id, text, session.commandHistory || [], { projectDir: session.projectDir || globalThis.__autoProjectDir });
561
+ await adapter.sendTyping?.(ctx.threadId, false);
562
+ const chunks = splitMessage(response || '无响应');
563
+ for (const chunk of chunks) {
564
+ await adapter.reply(ctx.threadId, chunk);
565
+ }
566
+ } catch (error) {
567
+ await adapter.reply(ctx.threadId, `❌ ${session.currentAgent} 错误: ${error.message}`);
568
+ }
569
+ return;
570
+ }
571
+ }
572
+ }
573
+
574
+ await forwardToOpenCode(adapter, ctx, text, openCodeSessions, session);
575
+ }
576
+
577
+ export { handleMessage, forwardToOpenCode, startLoopCycle };
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2016 Douglas Christopher Wilson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.