@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.
- package/dist/autonomous/decisions.js +73 -0
- package/dist/autonomous/index.js +141 -0
- package/dist/cli.js +1 -10
- package/dist/core/git-push.js +120 -0
- package/dist/core/notifications.js +2 -2
- package/dist/core/router.js +47 -293
- package/dist/feishu/bot.js +0 -2
- package/dist/feishu/commands.js +26 -395
- package/dist/feishu/handler.js +25 -216
- package/dist/opencode/client.js +126 -144
- package/dist/plugins/agents/claude-code/index.js +6 -2
- package/dist/plugins/agents/opencode/index.js +9 -5
- 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 +357 -604
- package/dist/weixin/handler.js +169 -421
- package/dist/weixin/user-adapter-map.js +1 -0
- package/package.json +1 -1
- package/dist/core/session.js +0 -403
package/dist/weixin/handler.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
//
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
527
|
-
|
|
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
|
-
|
|
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(
|
|
534
|
-
|
|
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
|
-
|
|
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
|
|
295
|
+
await forwardToOpenCode(adapter, ctx, text, openCodeSessions);
|
|
548
296
|
}
|
|
549
297
|
|
|
550
|
-
export { handleMessage, forwardToOpenCode
|
|
298
|
+
export { handleMessage, forwardToOpenCode };
|