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.
Files changed (39) hide show
  1. package/.env.example +62 -0
  2. package/README.md +232 -287
  3. package/bin/chatgpt-desktop.js +2 -0
  4. package/bin/qq-codex-weixin-gateway.js +14 -0
  5. package/dist/apps/bridge-daemon/src/bootstrap.js +161 -31
  6. package/dist/apps/bridge-daemon/src/cli.js +5 -1
  7. package/dist/apps/bridge-daemon/src/config.js +168 -37
  8. package/dist/apps/bridge-daemon/src/http-server.js +23 -11
  9. package/dist/apps/bridge-daemon/src/main.js +163 -29
  10. package/dist/apps/bridge-daemon/src/thread-command-handler.js +320 -23
  11. package/dist/apps/chatgpt-desktop-cli/src/cli.js +191 -0
  12. package/dist/apps/weixin-gateway/src/cli.js +446 -0
  13. package/dist/apps/weixin-gateway/src/config.js +135 -0
  14. package/dist/apps/weixin-gateway/src/dev.js +2 -0
  15. package/dist/apps/weixin-gateway/src/message-store.js +50 -0
  16. package/dist/apps/weixin-gateway/src/server.js +216 -0
  17. package/dist/apps/weixin-gateway/src/state.js +163 -0
  18. package/dist/apps/weixin-gateway/src/weixin-client.js +520 -0
  19. package/dist/packages/adapters/chatgpt-desktop/src/ax-client.js +472 -0
  20. package/dist/packages/adapters/chatgpt-desktop/src/bridge-provider.js +82 -0
  21. package/dist/packages/adapters/chatgpt-desktop/src/driver.js +161 -0
  22. package/dist/packages/adapters/chatgpt-desktop/src/image-cache.js +155 -0
  23. package/dist/packages/adapters/chatgpt-desktop/src/session-registry.js +48 -0
  24. package/dist/packages/adapters/chatgpt-desktop/src/types.js +1 -0
  25. package/dist/packages/adapters/codex-desktop/src/codex-app-server-driver.js +810 -0
  26. package/dist/packages/adapters/codex-desktop/src/codex-app-ui-notification-forwarder.js +33 -0
  27. package/dist/packages/adapters/codex-desktop/src/codex-desktop-driver.js +727 -123
  28. package/dist/packages/adapters/codex-desktop/src/codex-local-rollout-reader.js +227 -0
  29. package/dist/packages/adapters/codex-desktop/src/codex-local-submission-reader.js +142 -0
  30. package/dist/packages/adapters/weixin/src/weixin-channel-adapter.js +15 -0
  31. package/dist/packages/adapters/weixin/src/weixin-http-client.js +42 -0
  32. package/dist/packages/adapters/weixin/src/weixin-sender.js +200 -0
  33. package/dist/packages/adapters/weixin/src/weixin-webhook.js +35 -0
  34. package/dist/packages/orchestrator/src/bridge-orchestrator.js +72 -25
  35. package/dist/packages/orchestrator/src/weixin-outbound-format.js +55 -0
  36. package/dist/packages/ports/src/chat.js +1 -0
  37. package/dist/packages/store/src/session-repo.js +16 -3
  38. package/dist/packages/store/src/sqlite.js +3 -0
  39. 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 (!this.isSupportedCommand(text)) {
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) => thread.threadRef === session?.codexThreadRef)
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.deliverControlReply(message, this.buildHelpText());
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.deliverControlReply(message, this.buildHelpText());
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
- const state = await this.deps.desktopDriver.switchModel(targetModel);
62
- await this.deliverControlReply(message, this.formatModelSwitchReply(targetModel, state));
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.deliverControlReply(message, `已切换到线程:${thread.title}${thread.projectName ? `\n项目:${thread.projectName}` : ""}`);
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 index = thread.isCurrent ? `👉🏻 ${thread.index}` : `${thread.index}`;
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
- "| 查看最近活跃线程 | `/threads` | `/t` |",
202
- "| 查看当前绑定线程 | `/thread current` | `/tc` |",
203
- "| 切换到指定线程 | `/thread use <序号>` | `/tu <序号>` |",
204
- "| 新建线程 | `/thread new <标题>` | `/tn <标题>` |",
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
- "建议先发 `/t` 看列表,再用 `/tu 2` 这种方式切换。",
213
- "模型和额度信息来自当前 Codex Desktop 界面,可见性取决于 UI 是否暴露对应信息。"
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
- `线程绑定:${session?.codexThreadRef ?? "未绑定"}`,
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
+ }