qq-codex-bridge 0.1.2 → 0.1.4
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/.env.example +62 -0
- package/README.md +232 -287
- package/bin/chatgpt-desktop.js +2 -0
- package/bin/qq-codex-weixin-gateway.js +14 -0
- package/dist/apps/bridge-daemon/src/bootstrap.js +161 -31
- package/dist/apps/bridge-daemon/src/cli.js +5 -1
- package/dist/apps/bridge-daemon/src/config.js +168 -37
- package/dist/apps/bridge-daemon/src/http-server.js +23 -11
- package/dist/apps/bridge-daemon/src/main.js +163 -29
- package/dist/apps/bridge-daemon/src/thread-command-handler.js +320 -23
- package/dist/apps/chatgpt-desktop-cli/src/cli.js +191 -0
- package/dist/apps/weixin-gateway/src/cli.js +446 -0
- package/dist/apps/weixin-gateway/src/config.js +135 -0
- package/dist/apps/weixin-gateway/src/dev.js +2 -0
- package/dist/apps/weixin-gateway/src/message-store.js +50 -0
- package/dist/apps/weixin-gateway/src/server.js +216 -0
- package/dist/apps/weixin-gateway/src/state.js +163 -0
- package/dist/apps/weixin-gateway/src/weixin-client.js +520 -0
- package/dist/packages/adapters/chatgpt-desktop/src/ax-client.js +472 -0
- package/dist/packages/adapters/chatgpt-desktop/src/bridge-provider.js +82 -0
- package/dist/packages/adapters/chatgpt-desktop/src/driver.js +161 -0
- package/dist/packages/adapters/chatgpt-desktop/src/image-cache.js +155 -0
- package/dist/packages/adapters/chatgpt-desktop/src/session-registry.js +48 -0
- package/dist/packages/adapters/chatgpt-desktop/src/types.js +1 -0
- package/dist/packages/adapters/codex-desktop/src/codex-app-server-driver.js +810 -0
- package/dist/packages/adapters/codex-desktop/src/codex-app-ui-notification-forwarder.js +33 -0
- package/dist/packages/adapters/codex-desktop/src/codex-desktop-driver.js +727 -123
- package/dist/packages/adapters/codex-desktop/src/codex-local-rollout-reader.js +227 -0
- package/dist/packages/adapters/codex-desktop/src/codex-local-submission-reader.js +142 -0
- package/dist/packages/adapters/weixin/src/weixin-channel-adapter.js +15 -0
- package/dist/packages/adapters/weixin/src/weixin-http-client.js +42 -0
- package/dist/packages/adapters/weixin/src/weixin-sender.js +200 -0
- package/dist/packages/adapters/weixin/src/weixin-webhook.js +35 -0
- package/dist/packages/orchestrator/src/bridge-orchestrator.js +72 -25
- package/dist/packages/orchestrator/src/weixin-outbound-format.js +55 -0
- package/dist/packages/ports/src/chat.js +1 -0
- package/dist/packages/store/src/session-repo.js +16 -3
- package/dist/packages/store/src/sqlite.js +3 -0
- package/package.json +8 -2
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { BridgeSessionStatus } from "../../../packages/domain/src/session.js";
|
|
3
|
+
import { ensureAppVisible } from "../../../packages/adapters/chatgpt-desktop/src/ax-client.js";
|
|
3
4
|
import { DesktopDriverError } from "../../../packages/domain/src/driver.js";
|
|
4
5
|
export class ThreadCommandHandler {
|
|
5
6
|
deps;
|
|
7
|
+
chatgptThreadListRefreshSessionKeys = new Set();
|
|
6
8
|
constructor(deps) {
|
|
7
9
|
this.deps = deps;
|
|
8
10
|
}
|
|
@@ -11,9 +13,10 @@ export class ThreadCommandHandler {
|
|
|
11
13
|
return false;
|
|
12
14
|
}
|
|
13
15
|
const text = message.text.trim();
|
|
14
|
-
if (!
|
|
16
|
+
if (!text.startsWith("/")) {
|
|
15
17
|
return false;
|
|
16
18
|
}
|
|
19
|
+
const supportedCommand = this.isSupportedCommand(text);
|
|
17
20
|
const alreadySeen = await this.deps.transcriptStore.hasInbound(message.messageId);
|
|
18
21
|
if (alreadySeen) {
|
|
19
22
|
return true;
|
|
@@ -25,15 +28,30 @@ export class ThreadCommandHandler {
|
|
|
25
28
|
}
|
|
26
29
|
await this.ensureSessionExists(message);
|
|
27
30
|
await this.deps.transcriptStore.recordInbound(message);
|
|
31
|
+
if (!supportedCommand) {
|
|
32
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
33
|
+
await this.deliverControlReply(message, this.buildUnknownCommandText(text, this.currentProvider(session)));
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
28
36
|
if (text === "/threads" || text === "/t") {
|
|
37
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
38
|
+
if (this.currentProvider(session) === "chatgpt-desktop") {
|
|
39
|
+
await this.deliverChatgptThreads(message, session);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
29
42
|
const threads = await this.deps.desktopDriver.listRecentThreads(20);
|
|
30
|
-
await this.deliverControlReply(message, this.formatThreads(threads));
|
|
43
|
+
await this.deliverControlReply(message, this.formatThreads(threads, session?.codexThreadRef ?? null));
|
|
31
44
|
return;
|
|
32
45
|
}
|
|
33
46
|
if (text === "/thread current" || text === "/tc") {
|
|
34
47
|
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
48
|
+
if (this.currentProvider(session) === "chatgpt-desktop") {
|
|
49
|
+
await this.deliverChatgptCurrentThread(message, session);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
35
52
|
const threads = await this.deps.desktopDriver.listRecentThreads(20);
|
|
36
|
-
const current = threads.find((thread) =>
|
|
53
|
+
const current = threads.find((thread) => session?.codexThreadRef
|
|
54
|
+
&& areThreadRefsEquivalent(thread.threadRef, session.codexThreadRef))
|
|
37
55
|
?? threads.find((thread) => thread.isCurrent)
|
|
38
56
|
?? null;
|
|
39
57
|
const reply = current
|
|
@@ -42,12 +60,73 @@ export class ThreadCommandHandler {
|
|
|
42
60
|
await this.deliverControlReply(message, reply);
|
|
43
61
|
return;
|
|
44
62
|
}
|
|
63
|
+
const sourceMatch = text.match(/^\/source\s+(codex|chatgpt)$/);
|
|
64
|
+
if (sourceMatch) {
|
|
65
|
+
const target = sourceMatch[1] === "chatgpt" ? "chatgpt-desktop" : "codex-desktop";
|
|
66
|
+
await this.deps.sessionStore.updateConversationProvider(message.sessionKey, target);
|
|
67
|
+
if (target === "chatgpt-desktop") {
|
|
68
|
+
this.chatgptThreadListRefreshSessionKeys.add(message.sessionKey);
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
this.chatgptThreadListRefreshSessionKeys.delete(message.sessionKey);
|
|
72
|
+
}
|
|
73
|
+
const label = target === "chatgpt-desktop" ? "ChatGPT Desktop" : "Codex Desktop";
|
|
74
|
+
await this.deliverControlReply(message, `已切换对话源:${label}\n后续消息将通过 ${label} 回复。`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (text === "/source") {
|
|
78
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
79
|
+
const current = session?.conversationProvider ?? "codex-desktop(全局默认)";
|
|
80
|
+
await this.deliverControlReply(message, `当前对话源:${current}\n切换:/source codex 或 /source chatgpt`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (text === "/accounts") {
|
|
84
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
85
|
+
await this.deliverControlReply(message, this.buildAccountsText(message, session));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (text === "/cgpt" || text === "/cgpt threads") {
|
|
89
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
90
|
+
await this.deliverChatgptThreads(message, session);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const cgptUseMatch = text.match(/^\/cgpt\s+use\s+(\d+)$/);
|
|
94
|
+
if (cgptUseMatch) {
|
|
95
|
+
const cgDriver = this.deps.chatgptDriver;
|
|
96
|
+
if (!cgDriver) {
|
|
97
|
+
await this.deliverControlReply(message, "ChatGPT Desktop 未启用。");
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const index = Number(cgptUseMatch[1]);
|
|
101
|
+
const chats = cgDriver.listChats(20);
|
|
102
|
+
const target = chats[index - 1];
|
|
103
|
+
if (!target) {
|
|
104
|
+
await this.deliverControlReply(message, `没有第 ${index} 条对话,请先发 /cgpt 查看列表。`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const switched = cgDriver.switchToChat(target.title);
|
|
108
|
+
if (!switched) {
|
|
109
|
+
await this.deliverControlReply(message, `切换失败:在侧边栏未找到「${target.title}」,请重试或刷新列表。`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// 写入当前对话标题,下次 run() 检测到后跳过 clickNewChat
|
|
113
|
+
cgDriver.markSwitched(message.sessionKey, target.title);
|
|
114
|
+
await this.deliverControlReply(message, `已切换到 ChatGPT 对话:${target.title}\n下次消息将继续该对话。`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (text === "/cgpt new") {
|
|
118
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
119
|
+
await this.createChatgptThread(message, session);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
45
122
|
if (text === "/help") {
|
|
46
|
-
await this.
|
|
123
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
124
|
+
await this.deliverControlReply(message, this.buildHelpText(this.currentProvider(session)));
|
|
47
125
|
return;
|
|
48
126
|
}
|
|
49
127
|
if (text === "/h") {
|
|
50
|
-
await this.
|
|
128
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
129
|
+
await this.deliverControlReply(message, this.buildHelpText(this.currentProvider(session)));
|
|
51
130
|
return;
|
|
52
131
|
}
|
|
53
132
|
if (text === "/model" || text === "/m") {
|
|
@@ -58,8 +137,14 @@ export class ThreadCommandHandler {
|
|
|
58
137
|
const switchModelMatch = text.match(/^(?:\/model\s+use|\/mu)\s+(.+)$/);
|
|
59
138
|
if (switchModelMatch) {
|
|
60
139
|
const targetModel = switchModelMatch[1].trim();
|
|
61
|
-
|
|
62
|
-
|
|
140
|
+
try {
|
|
141
|
+
const state = await this.deps.desktopDriver.switchModel(targetModel);
|
|
142
|
+
await this.deliverControlReply(message, this.formatModelSwitchReply(targetModel, state));
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
146
|
+
await this.deliverControlReply(message, `切换模型失败:${reason}\n请检查模型名称是否正确,或当前 Codex Desktop 界面是否可操作。`);
|
|
147
|
+
}
|
|
63
148
|
return;
|
|
64
149
|
}
|
|
65
150
|
if (text === "/quota" || text === "/q") {
|
|
@@ -69,7 +154,12 @@ export class ThreadCommandHandler {
|
|
|
69
154
|
}
|
|
70
155
|
if (text === "/status" || text === "/st") {
|
|
71
156
|
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
72
|
-
const state = await this.deps.desktopDriver.getControlState(
|
|
157
|
+
const state = await this.deps.desktopDriver.getControlState(session
|
|
158
|
+
? {
|
|
159
|
+
sessionKey: session.sessionKey,
|
|
160
|
+
codexThreadRef: session.codexThreadRef
|
|
161
|
+
}
|
|
162
|
+
: null);
|
|
73
163
|
const quotaSummary = await this.deps.desktopDriver.getQuotaSummary();
|
|
74
164
|
await this.deliverControlReply(message, this.formatStatusReply(session, state, quotaSummary));
|
|
75
165
|
return;
|
|
@@ -77,6 +167,11 @@ export class ThreadCommandHandler {
|
|
|
77
167
|
const useMatch = text.match(/^(?:\/thread\s+use|\/tu)\s+(\d+)$/);
|
|
78
168
|
if (useMatch) {
|
|
79
169
|
const index = Number(useMatch[1]);
|
|
170
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
171
|
+
if (this.currentProvider(session) === "chatgpt-desktop") {
|
|
172
|
+
await this.useChatgptThread(message, index);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
80
175
|
const threads = await this.deps.desktopDriver.listRecentThreads(20);
|
|
81
176
|
const thread = threads[index - 1];
|
|
82
177
|
if (!thread) {
|
|
@@ -95,12 +190,22 @@ export class ThreadCommandHandler {
|
|
|
95
190
|
throw error;
|
|
96
191
|
}
|
|
97
192
|
await this.deps.sessionStore.updateBinding(message.sessionKey, binding.codexThreadRef);
|
|
98
|
-
await this.
|
|
193
|
+
await this.deps.sessionStore.updateSkillContextKey(message.sessionKey, null);
|
|
194
|
+
await this.deliverControlReply(message, [
|
|
195
|
+
`已切换到线程:${thread.title}`,
|
|
196
|
+
...(thread.projectName ? [`项目:${thread.projectName}`] : []),
|
|
197
|
+
`绑定标识:${binding.codexThreadRef ?? "未绑定"}`
|
|
198
|
+
].join("\n"));
|
|
99
199
|
return;
|
|
100
200
|
}
|
|
101
201
|
const newMatch = text.match(/^(?:\/thread\s+new|\/tn)\s+(.+)$/);
|
|
102
202
|
if (newMatch) {
|
|
103
203
|
const title = newMatch[1].trim();
|
|
204
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
205
|
+
if (this.currentProvider(session) === "chatgpt-desktop") {
|
|
206
|
+
await this.createChatgptThread(message, session);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
104
209
|
const binding = await this.deps.desktopDriver.createThread(message.sessionKey, this.buildNewThreadSeedPrompt(title));
|
|
105
210
|
await this.deps.sessionStore.updateBinding(message.sessionKey, binding.codexThreadRef);
|
|
106
211
|
await this.deliverControlReply(message, `已创建并切换到新线程:${title}`);
|
|
@@ -109,13 +214,18 @@ export class ThreadCommandHandler {
|
|
|
109
214
|
const forkMatch = text.match(/^(?:\/thread\s+fork|\/tf)\s+(.+)$/);
|
|
110
215
|
if (forkMatch) {
|
|
111
216
|
const title = forkMatch[1].trim();
|
|
217
|
+
const session = await this.deps.sessionStore.getSession(message.sessionKey);
|
|
218
|
+
if (this.currentProvider(session) === "chatgpt-desktop") {
|
|
219
|
+
await this.createChatgptThread(message, session);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
112
222
|
const recentConversation = await this.deps.transcriptStore.listRecentConversation(message.sessionKey, 8);
|
|
113
223
|
const binding = await this.deps.desktopDriver.createThread(message.sessionKey, this.buildForkThreadSeedPrompt(title, recentConversation));
|
|
114
224
|
await this.deps.sessionStore.updateBinding(message.sessionKey, binding.codexThreadRef);
|
|
115
225
|
await this.deliverControlReply(message, `已根据最近几轮对话 fork 新线程:${title}`);
|
|
116
226
|
return;
|
|
117
227
|
}
|
|
118
|
-
await this.deliverControlReply(message, this.buildHelpText());
|
|
228
|
+
await this.deliverControlReply(message, this.buildHelpText(this.currentProvider(await this.deps.sessionStore.getSession(message.sessionKey))));
|
|
119
229
|
});
|
|
120
230
|
return true;
|
|
121
231
|
}
|
|
@@ -131,7 +241,9 @@ export class ThreadCommandHandler {
|
|
|
131
241
|
chatType: message.chatType,
|
|
132
242
|
peerId: message.senderId,
|
|
133
243
|
codexThreadRef: null,
|
|
244
|
+
lastCodexTurnId: null,
|
|
134
245
|
skillContextKey: null,
|
|
246
|
+
conversationProvider: null,
|
|
135
247
|
status: BridgeSessionStatus.Active,
|
|
136
248
|
lastInboundAt: message.receivedAt,
|
|
137
249
|
lastOutboundAt: null,
|
|
@@ -146,6 +258,7 @@ export class ThreadCommandHandler {
|
|
|
146
258
|
text === "/tc" ||
|
|
147
259
|
text === "/help" ||
|
|
148
260
|
text === "/h" ||
|
|
261
|
+
text === "/accounts" ||
|
|
149
262
|
text === "/model" ||
|
|
150
263
|
text === "/m" ||
|
|
151
264
|
text === "/quota" ||
|
|
@@ -160,9 +273,15 @@ export class ThreadCommandHandler {
|
|
|
160
273
|
/^\/tn\s+.+$/.test(text) ||
|
|
161
274
|
/^\/thread\s+fork\s+.+$/.test(text) ||
|
|
162
275
|
/^\/tf\s+.+$/.test(text) ||
|
|
163
|
-
text === "/thread"
|
|
276
|
+
text === "/thread" ||
|
|
277
|
+
text === "/cgpt" ||
|
|
278
|
+
text === "/cgpt threads" ||
|
|
279
|
+
text === "/cgpt new" ||
|
|
280
|
+
/^\/cgpt\s+use\s+\d+$/.test(text) ||
|
|
281
|
+
text === "/source" ||
|
|
282
|
+
/^\/source\s+(codex|chatgpt)$/.test(text));
|
|
164
283
|
}
|
|
165
|
-
formatThreads(threads) {
|
|
284
|
+
formatThreads(threads, boundThreadRef = null) {
|
|
166
285
|
if (threads.length === 0) {
|
|
167
286
|
return "当前没有可用的 Codex 线程。";
|
|
168
287
|
}
|
|
@@ -173,7 +292,11 @@ export class ThreadCommandHandler {
|
|
|
173
292
|
"| 序号 | 项目 | 线程标题 | 最近活动 |",
|
|
174
293
|
"| --- | --- | --- | --- |",
|
|
175
294
|
...threads.map((thread) => {
|
|
176
|
-
const
|
|
295
|
+
const isBound = Boolean(boundThreadRef
|
|
296
|
+
&& thread.threadRef
|
|
297
|
+
&& areThreadRefsEquivalent(thread.threadRef, boundThreadRef));
|
|
298
|
+
const shouldMark = boundThreadRef ? isBound : thread.isCurrent;
|
|
299
|
+
const index = shouldMark ? `👉🏻 ${thread.index}` : `${thread.index}`;
|
|
177
300
|
const project = escapeCell(thread.projectName) || "-";
|
|
178
301
|
const title = escapeCell(thread.title) || "-";
|
|
179
302
|
const time = escapeCell(thread.relativeTime) || "-";
|
|
@@ -181,6 +304,100 @@ export class ThreadCommandHandler {
|
|
|
181
304
|
})
|
|
182
305
|
].join("\n");
|
|
183
306
|
}
|
|
307
|
+
currentProvider(session) {
|
|
308
|
+
return session?.conversationProvider ?? "codex-desktop";
|
|
309
|
+
}
|
|
310
|
+
async deliverChatgptThreads(message, session) {
|
|
311
|
+
const cgDriver = this.deps.chatgptDriver;
|
|
312
|
+
if (!cgDriver) {
|
|
313
|
+
await this.deliverControlReply(message, "ChatGPT Desktop 未启用,请先 /source chatgpt 切换。");
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const shouldRefresh = this.chatgptThreadListRefreshSessionKeys.delete(message.sessionKey);
|
|
317
|
+
try {
|
|
318
|
+
ensureAppVisible();
|
|
319
|
+
}
|
|
320
|
+
catch { /* non-fatal */ }
|
|
321
|
+
const currentRef = session ? cgDriver.getSessionThreadRef(session.sessionKey) : null;
|
|
322
|
+
const currentWindowTitle = cgDriver.getCurrentThreadTitle();
|
|
323
|
+
const chats = this.listChatgptChats(cgDriver, shouldRefresh);
|
|
324
|
+
if (chats.length === 0) {
|
|
325
|
+
await this.deliverControlReply(message, "ChatGPT 侧边栏未读取到对话列表。请确保 ChatGPT Desktop 已启动且有历史对话。");
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const escapeCell = (v) => v.replace(/\|/g, "\\|").replace(/\n/g, " ").trim();
|
|
329
|
+
const lines = [
|
|
330
|
+
"最近 20 条 ChatGPT 对话:",
|
|
331
|
+
"",
|
|
332
|
+
"| 序号 | 对话标题 |",
|
|
333
|
+
"| --- | --- |",
|
|
334
|
+
...chats.map((c) => {
|
|
335
|
+
const mark = this.isCurrentChatgptChat(c.title, currentRef, currentWindowTitle) ? "👉🏻 " : "";
|
|
336
|
+
return `| ${mark}${c.index} | ${escapeCell(c.title)} |`;
|
|
337
|
+
})
|
|
338
|
+
];
|
|
339
|
+
await this.deliverControlReply(message, lines.join("\n"));
|
|
340
|
+
}
|
|
341
|
+
listChatgptChats(cgDriver, shouldRefresh) {
|
|
342
|
+
const chats = cgDriver.listChats(20);
|
|
343
|
+
if (!shouldRefresh && chats.length > 0) {
|
|
344
|
+
return chats;
|
|
345
|
+
}
|
|
346
|
+
const refreshedChats = cgDriver.listChats(20);
|
|
347
|
+
return refreshedChats.length > 0 ? refreshedChats : chats;
|
|
348
|
+
}
|
|
349
|
+
async deliverChatgptCurrentThread(message, session) {
|
|
350
|
+
const cgDriver = this.deps.chatgptDriver;
|
|
351
|
+
if (!cgDriver) {
|
|
352
|
+
await this.deliverControlReply(message, "ChatGPT Desktop 未启用,请先 /source chatgpt 切换。");
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const currentRef = session ? cgDriver.getSessionThreadRef(session.sessionKey) : null;
|
|
356
|
+
await this.deliverControlReply(message, currentRef ? `当前绑定 ChatGPT 对话:${currentRef}` : "当前私聊还没有绑定 ChatGPT 对话。");
|
|
357
|
+
}
|
|
358
|
+
async useChatgptThread(message, index) {
|
|
359
|
+
const cgDriver = this.deps.chatgptDriver;
|
|
360
|
+
if (!cgDriver) {
|
|
361
|
+
await this.deliverControlReply(message, "ChatGPT Desktop 未启用。");
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
const chats = cgDriver.listChats(20);
|
|
365
|
+
const target = chats[index - 1];
|
|
366
|
+
if (!target) {
|
|
367
|
+
await this.deliverControlReply(message, `没有第 ${index} 条 ChatGPT 对话,请先发 /threads 查看列表。`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const switched = cgDriver.switchToChat(target.title);
|
|
371
|
+
if (!switched) {
|
|
372
|
+
await this.deliverControlReply(message, `切换失败:在 ChatGPT 侧边栏未找到「${target.title}」,请重试或刷新列表。`);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
cgDriver.markSwitched(message.sessionKey, target.title);
|
|
376
|
+
await this.deliverControlReply(message, `已切换到 ChatGPT 对话:${target.title}\n下次消息将继续该对话。`);
|
|
377
|
+
}
|
|
378
|
+
isCurrentChatgptChat(title, currentRef, currentWindowTitle) {
|
|
379
|
+
const normalizedTitle = normalizeChatgptTitle(title);
|
|
380
|
+
const normalizedRef = currentRef && currentRef !== "__switched__"
|
|
381
|
+
? normalizeChatgptTitle(currentRef)
|
|
382
|
+
: "";
|
|
383
|
+
if (normalizedRef && normalizedTitle === normalizedRef) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
const normalizedWindowTitle = currentWindowTitle ? normalizeChatgptTitle(currentWindowTitle) : "";
|
|
387
|
+
return Boolean(normalizedWindowTitle
|
|
388
|
+
&& normalizedTitle
|
|
389
|
+
&& (normalizedWindowTitle === normalizedTitle
|
|
390
|
+
|| normalizedWindowTitle.includes(normalizedTitle)));
|
|
391
|
+
}
|
|
392
|
+
async createChatgptThread(message, session) {
|
|
393
|
+
const cgDriver = this.deps.chatgptDriver;
|
|
394
|
+
if (!cgDriver) {
|
|
395
|
+
await this.deliverControlReply(message, "ChatGPT Desktop 未启用。");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
cgDriver.newChat(session?.sessionKey ?? message.sessionKey);
|
|
399
|
+
await this.deliverControlReply(message, "已为本会话新建 ChatGPT 对话,下条消息将从新对话开始。");
|
|
400
|
+
}
|
|
184
401
|
async deliverControlReply(message, text) {
|
|
185
402
|
const draft = {
|
|
186
403
|
draftId: randomUUID(),
|
|
@@ -192,25 +409,72 @@ export class ThreadCommandHandler {
|
|
|
192
409
|
await this.deps.transcriptStore.recordOutbound(draft);
|
|
193
410
|
await this.deps.qqEgress.deliver(draft);
|
|
194
411
|
}
|
|
195
|
-
buildHelpText() {
|
|
412
|
+
buildHelpText(provider = "codex-desktop") {
|
|
413
|
+
if (provider === "chatgpt-desktop") {
|
|
414
|
+
return [
|
|
415
|
+
"快捷命令(当前源:ChatGPT Desktop):",
|
|
416
|
+
"",
|
|
417
|
+
"| 用途 | 完整命令 | 简写 |",
|
|
418
|
+
"| --- | --- | --- |",
|
|
419
|
+
"| 查看 ChatGPT 最近对话 | `/threads` | `/t` |",
|
|
420
|
+
"| 查看当前绑定 ChatGPT 对话 | `/thread current` | `/tc` |",
|
|
421
|
+
"| 切换 ChatGPT 对话 | `/thread use <序号>` | `/tu <序号>` |",
|
|
422
|
+
"| 新建 ChatGPT 对话 | `/thread new <标题>` | `/tn <标题>` |",
|
|
423
|
+
"| 查看当前对话源 | `/source` | - |",
|
|
424
|
+
"| 查看账号状态 | `/accounts` | - |",
|
|
425
|
+
"| 切换到 Codex Desktop | `/source codex` | - |",
|
|
426
|
+
"| 查看帮助 | `/help` | `/h` |",
|
|
427
|
+
"",
|
|
428
|
+
"建议先发 `/t` 刷新并查看 ChatGPT 对话列表,再用 `/tu 2` 切换。",
|
|
429
|
+
"模型、额度、状态和真正的 fork 命令目前只适用于 Codex Desktop。"
|
|
430
|
+
].join("\n");
|
|
431
|
+
}
|
|
196
432
|
return [
|
|
197
|
-
"
|
|
433
|
+
"快捷命令(当前源:Codex Desktop):",
|
|
198
434
|
"",
|
|
199
435
|
"| 用途 | 完整命令 | 简写 |",
|
|
200
436
|
"| --- | --- | --- |",
|
|
201
|
-
"|
|
|
202
|
-
"|
|
|
203
|
-
"|
|
|
204
|
-
"|
|
|
205
|
-
"| 基于最近对话 fork 线程 | `/thread fork <标题>` | `/tf <标题>` |",
|
|
437
|
+
"| 查看 Codex 最近线程 | `/threads` | `/t` |",
|
|
438
|
+
"| 查看当前绑定 Codex 线程 | `/thread current` | `/tc` |",
|
|
439
|
+
"| 切换 Codex 线程 | `/thread use <序号>` | `/tu <序号>` |",
|
|
440
|
+
"| 新建 Codex 线程 | `/thread new <标题>` | `/tn <标题>` |",
|
|
441
|
+
"| 基于最近对话 fork Codex 线程 | `/thread fork <标题>` | `/tf <标题>` |",
|
|
206
442
|
"| 查看当前模型 | `/model` | `/m` |",
|
|
207
443
|
"| 切换模型 | `/model use <名称>` | `/mu <名称>` |",
|
|
208
444
|
"| 查看额度信息 | `/quota` | `/q` |",
|
|
209
445
|
"| 查看当前运行状态 | `/status` | `/st` |",
|
|
210
446
|
"| 查看帮助 | `/help` | `/h` |",
|
|
447
|
+
"| 查看当前对话源 | `/source` | - |",
|
|
448
|
+
"| 查看账号状态 | `/accounts` | - |",
|
|
449
|
+
"| 切换到 ChatGPT Desktop | `/source chatgpt` | - |",
|
|
450
|
+
"| 切换到 Codex Desktop | `/source codex` | - |",
|
|
451
|
+
"",
|
|
452
|
+
"所有 `/` 开头的桥接快捷指令都会先由桥接层处理,不会直接发给 Codex。",
|
|
453
|
+
"建议先用 `/source` 确认当前对话源,再发 `/t` 看列表,用 `/tu 2` 切换。",
|
|
454
|
+
"切到 ChatGPT 后,这套 `/t`、`/tu`、`/tn` 会自动操作 ChatGPT 对话。"
|
|
455
|
+
].join("\n");
|
|
456
|
+
}
|
|
457
|
+
buildUnknownCommandText(text, provider) {
|
|
458
|
+
return [
|
|
459
|
+
`未识别的桥接快捷指令:\`${text}\``,
|
|
460
|
+
"这条 `/` 指令不会转发给当前对话源。",
|
|
211
461
|
"",
|
|
212
|
-
|
|
213
|
-
|
|
462
|
+
this.buildHelpText(provider)
|
|
463
|
+
].join("\n");
|
|
464
|
+
}
|
|
465
|
+
buildAccountsText(message, session) {
|
|
466
|
+
const currentProvider = this.currentProvider(session);
|
|
467
|
+
const accountKeys = [...new Set(this.deps.accountKeys ?? [message.accountKey])].sort();
|
|
468
|
+
return [
|
|
469
|
+
"账号状态:",
|
|
470
|
+
"",
|
|
471
|
+
"| 项目 | 值 |",
|
|
472
|
+
"| --- | --- |",
|
|
473
|
+
`| 当前账号 | ${escapeMarkdownCell(message.accountKey)} |`,
|
|
474
|
+
`| 当前来源 | ${message.accountKey.startsWith("weixin:") ? "微信" : "QQ"} |`,
|
|
475
|
+
`| 当前会话 | ${escapeMarkdownCell(message.sessionKey)} |`,
|
|
476
|
+
`| 当前对话源 | ${currentProvider} |`,
|
|
477
|
+
`| 已接入账号 | ${accountKeys.map(escapeMarkdownCell).join(", ")} |`
|
|
214
478
|
].join("\n");
|
|
215
479
|
}
|
|
216
480
|
formatModelReply(state) {
|
|
@@ -232,9 +496,13 @@ export class ThreadCommandHandler {
|
|
|
232
496
|
return `额度信息:${quotaSummary ?? "当前界面未显示明确额度,暂未识别到剩余配额。"}`;
|
|
233
497
|
}
|
|
234
498
|
formatStatusReply(session, state, quotaSummary) {
|
|
499
|
+
const boundThreadRef = state.threadRef ?? session?.codexThreadRef ?? null;
|
|
235
500
|
return [
|
|
236
501
|
"当前运行状态:",
|
|
237
|
-
`线程绑定:${
|
|
502
|
+
`线程绑定:${boundThreadRef ?? "未绑定"}`,
|
|
503
|
+
...(state.threadTitle ? [`线程标题:${state.threadTitle}`] : []),
|
|
504
|
+
...(state.threadProjectName ? [`线程项目:${state.threadProjectName}`] : []),
|
|
505
|
+
...(state.threadRelativeTime ? [`线程最近活动:${state.threadRelativeTime}`] : []),
|
|
238
506
|
`模型:${state.model ?? "未识别"}`,
|
|
239
507
|
`推理强度:${state.reasoningEffort ?? "未识别"}`,
|
|
240
508
|
`工作区:${state.workspace ?? "未识别"}`,
|
|
@@ -268,3 +536,32 @@ export class ThreadCommandHandler {
|
|
|
268
536
|
].join("\n");
|
|
269
537
|
}
|
|
270
538
|
}
|
|
539
|
+
function areThreadRefsEquivalent(left, right) {
|
|
540
|
+
if (left === right) {
|
|
541
|
+
return true;
|
|
542
|
+
}
|
|
543
|
+
const leftAppThreadId = extractAppServerThreadId(left);
|
|
544
|
+
const rightAppThreadId = extractAppServerThreadId(right);
|
|
545
|
+
return Boolean(leftAppThreadId && rightAppThreadId && leftAppThreadId === rightAppThreadId);
|
|
546
|
+
}
|
|
547
|
+
function extractAppServerThreadId(threadRef) {
|
|
548
|
+
const prefix = "codex-app-thread:";
|
|
549
|
+
if (!threadRef.startsWith(prefix)) {
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
const payload = threadRef.slice(prefix.length);
|
|
553
|
+
const separatorIndex = payload.indexOf(":");
|
|
554
|
+
const threadId = separatorIndex >= 0 ? payload.slice(0, separatorIndex) : payload;
|
|
555
|
+
return threadId.trim() ? threadId : null;
|
|
556
|
+
}
|
|
557
|
+
function normalizeChatgptTitle(value) {
|
|
558
|
+
return value
|
|
559
|
+
.replace(/^chatgpt\s*[-–—:|]?\s*/i, "")
|
|
560
|
+
.replace(/\s*[-–—:|]?\s*chatgpt$/i, "")
|
|
561
|
+
.replace(/\s+/g, " ")
|
|
562
|
+
.trim()
|
|
563
|
+
.toLowerCase();
|
|
564
|
+
}
|
|
565
|
+
function escapeMarkdownCell(value) {
|
|
566
|
+
return value.replace(/\|/g, "\\|").replace(/\n/g, " ").trim();
|
|
567
|
+
}
|