qq-codex-bridge 0.1.3 → 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 +309 -26
  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
  }
@@ -27,18 +29,29 @@ export class ThreadCommandHandler {
27
29
  await this.ensureSessionExists(message);
28
30
  await this.deps.transcriptStore.recordInbound(message);
29
31
  if (!supportedCommand) {
30
- await this.deliverControlReply(message, this.buildUnknownCommandText(text));
32
+ const session = await this.deps.sessionStore.getSession(message.sessionKey);
33
+ await this.deliverControlReply(message, this.buildUnknownCommandText(text, this.currentProvider(session)));
31
34
  return;
32
35
  }
33
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
+ }
34
42
  const threads = await this.deps.desktopDriver.listRecentThreads(20);
35
- await this.deliverControlReply(message, this.formatThreads(threads));
43
+ await this.deliverControlReply(message, this.formatThreads(threads, session?.codexThreadRef ?? null));
36
44
  return;
37
45
  }
38
46
  if (text === "/thread current" || text === "/tc") {
39
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
+ }
40
52
  const threads = await this.deps.desktopDriver.listRecentThreads(20);
41
- const current = threads.find((thread) => thread.threadRef === session?.codexThreadRef)
53
+ const current = threads.find((thread) => session?.codexThreadRef
54
+ && areThreadRefsEquivalent(thread.threadRef, session.codexThreadRef))
42
55
  ?? threads.find((thread) => thread.isCurrent)
43
56
  ?? null;
44
57
  const reply = current
@@ -47,12 +60,73 @@ export class ThreadCommandHandler {
47
60
  await this.deliverControlReply(message, reply);
48
61
  return;
49
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
+ }
50
122
  if (text === "/help") {
51
- 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)));
52
125
  return;
53
126
  }
54
127
  if (text === "/h") {
55
- 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)));
56
130
  return;
57
131
  }
58
132
  if (text === "/model" || text === "/m") {
@@ -63,8 +137,14 @@ export class ThreadCommandHandler {
63
137
  const switchModelMatch = text.match(/^(?:\/model\s+use|\/mu)\s+(.+)$/);
64
138
  if (switchModelMatch) {
65
139
  const targetModel = switchModelMatch[1].trim();
66
- const state = await this.deps.desktopDriver.switchModel(targetModel);
67
- 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
+ }
68
148
  return;
69
149
  }
70
150
  if (text === "/quota" || text === "/q") {
@@ -74,7 +154,12 @@ export class ThreadCommandHandler {
74
154
  }
75
155
  if (text === "/status" || text === "/st") {
76
156
  const session = await this.deps.sessionStore.getSession(message.sessionKey);
77
- 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);
78
163
  const quotaSummary = await this.deps.desktopDriver.getQuotaSummary();
79
164
  await this.deliverControlReply(message, this.formatStatusReply(session, state, quotaSummary));
80
165
  return;
@@ -82,6 +167,11 @@ export class ThreadCommandHandler {
82
167
  const useMatch = text.match(/^(?:\/thread\s+use|\/tu)\s+(\d+)$/);
83
168
  if (useMatch) {
84
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
+ }
85
175
  const threads = await this.deps.desktopDriver.listRecentThreads(20);
86
176
  const thread = threads[index - 1];
87
177
  if (!thread) {
@@ -100,12 +190,22 @@ export class ThreadCommandHandler {
100
190
  throw error;
101
191
  }
102
192
  await this.deps.sessionStore.updateBinding(message.sessionKey, binding.codexThreadRef);
103
- 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"));
104
199
  return;
105
200
  }
106
201
  const newMatch = text.match(/^(?:\/thread\s+new|\/tn)\s+(.+)$/);
107
202
  if (newMatch) {
108
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
+ }
109
209
  const binding = await this.deps.desktopDriver.createThread(message.sessionKey, this.buildNewThreadSeedPrompt(title));
110
210
  await this.deps.sessionStore.updateBinding(message.sessionKey, binding.codexThreadRef);
111
211
  await this.deliverControlReply(message, `已创建并切换到新线程:${title}`);
@@ -114,13 +214,18 @@ export class ThreadCommandHandler {
114
214
  const forkMatch = text.match(/^(?:\/thread\s+fork|\/tf)\s+(.+)$/);
115
215
  if (forkMatch) {
116
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
+ }
117
222
  const recentConversation = await this.deps.transcriptStore.listRecentConversation(message.sessionKey, 8);
118
223
  const binding = await this.deps.desktopDriver.createThread(message.sessionKey, this.buildForkThreadSeedPrompt(title, recentConversation));
119
224
  await this.deps.sessionStore.updateBinding(message.sessionKey, binding.codexThreadRef);
120
225
  await this.deliverControlReply(message, `已根据最近几轮对话 fork 新线程:${title}`);
121
226
  return;
122
227
  }
123
- await this.deliverControlReply(message, this.buildHelpText());
228
+ await this.deliverControlReply(message, this.buildHelpText(this.currentProvider(await this.deps.sessionStore.getSession(message.sessionKey))));
124
229
  });
125
230
  return true;
126
231
  }
@@ -136,7 +241,9 @@ export class ThreadCommandHandler {
136
241
  chatType: message.chatType,
137
242
  peerId: message.senderId,
138
243
  codexThreadRef: null,
244
+ lastCodexTurnId: null,
139
245
  skillContextKey: null,
246
+ conversationProvider: null,
140
247
  status: BridgeSessionStatus.Active,
141
248
  lastInboundAt: message.receivedAt,
142
249
  lastOutboundAt: null,
@@ -151,6 +258,7 @@ export class ThreadCommandHandler {
151
258
  text === "/tc" ||
152
259
  text === "/help" ||
153
260
  text === "/h" ||
261
+ text === "/accounts" ||
154
262
  text === "/model" ||
155
263
  text === "/m" ||
156
264
  text === "/quota" ||
@@ -165,9 +273,15 @@ export class ThreadCommandHandler {
165
273
  /^\/tn\s+.+$/.test(text) ||
166
274
  /^\/thread\s+fork\s+.+$/.test(text) ||
167
275
  /^\/tf\s+.+$/.test(text) ||
168
- 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));
169
283
  }
170
- formatThreads(threads) {
284
+ formatThreads(threads, boundThreadRef = null) {
171
285
  if (threads.length === 0) {
172
286
  return "当前没有可用的 Codex 线程。";
173
287
  }
@@ -178,7 +292,11 @@ export class ThreadCommandHandler {
178
292
  "| 序号 | 项目 | 线程标题 | 最近活动 |",
179
293
  "| --- | --- | --- | --- |",
180
294
  ...threads.map((thread) => {
181
- 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}`;
182
300
  const project = escapeCell(thread.projectName) || "-";
183
301
  const title = escapeCell(thread.title) || "-";
184
302
  const time = escapeCell(thread.relativeTime) || "-";
@@ -186,6 +304,100 @@ export class ThreadCommandHandler {
186
304
  })
187
305
  ].join("\n");
188
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
+ }
189
401
  async deliverControlReply(message, text) {
190
402
  const draft = {
191
403
  draftId: randomUUID(),
@@ -197,34 +409,72 @@ export class ThreadCommandHandler {
197
409
  await this.deps.transcriptStore.recordOutbound(draft);
198
410
  await this.deps.qqEgress.deliver(draft);
199
411
  }
200
- 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
+ }
201
432
  return [
202
- "快捷命令:",
433
+ "快捷命令(当前源:Codex Desktop):",
203
434
  "",
204
435
  "| 用途 | 完整命令 | 简写 |",
205
436
  "| --- | --- | --- |",
206
- "| 查看最近活跃线程 | `/threads` | `/t` |",
207
- "| 查看当前绑定线程 | `/thread current` | `/tc` |",
208
- "| 切换到指定线程 | `/thread use <序号>` | `/tu <序号>` |",
209
- "| 新建线程 | `/thread new <标题>` | `/tn <标题>` |",
210
- "| 基于最近对话 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 <标题>` |",
211
442
  "| 查看当前模型 | `/model` | `/m` |",
212
443
  "| 切换模型 | `/model use <名称>` | `/mu <名称>` |",
213
444
  "| 查看额度信息 | `/quota` | `/q` |",
214
445
  "| 查看当前运行状态 | `/status` | `/st` |",
215
446
  "| 查看帮助 | `/help` | `/h` |",
447
+ "| 查看当前对话源 | `/source` | - |",
448
+ "| 查看账号状态 | `/accounts` | - |",
449
+ "| 切换到 ChatGPT Desktop | `/source chatgpt` | - |",
450
+ "| 切换到 Codex Desktop | `/source codex` | - |",
216
451
  "",
217
452
  "所有 `/` 开头的桥接快捷指令都会先由桥接层处理,不会直接发给 Codex。",
218
- "建议先发 `/t` 看列表,再用 `/tu 2` 这种方式切换。",
219
- "模型和额度信息来自当前 Codex Desktop 界面,可见性取决于 UI 是否暴露对应信息。"
453
+ "建议先用 `/source` 确认当前对话源,再发 `/t` 看列表,用 `/tu 2` 切换。",
454
+ "切到 ChatGPT 后,这套 `/t`、`/tu`、`/tn` 会自动操作 ChatGPT 对话。"
220
455
  ].join("\n");
221
456
  }
222
- buildUnknownCommandText(text) {
457
+ buildUnknownCommandText(text, provider) {
223
458
  return [
224
459
  `未识别的桥接快捷指令:\`${text}\``,
225
- "这条 `/` 指令不会转发给 Codex。",
460
+ "这条 `/` 指令不会转发给当前对话源。",
226
461
  "",
227
- this.buildHelpText()
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(", ")} |`
228
478
  ].join("\n");
229
479
  }
230
480
  formatModelReply(state) {
@@ -246,9 +496,13 @@ export class ThreadCommandHandler {
246
496
  return `额度信息:${quotaSummary ?? "当前界面未显示明确额度,暂未识别到剩余配额。"}`;
247
497
  }
248
498
  formatStatusReply(session, state, quotaSummary) {
499
+ const boundThreadRef = state.threadRef ?? session?.codexThreadRef ?? null;
249
500
  return [
250
501
  "当前运行状态:",
251
- `线程绑定:${session?.codexThreadRef ?? "未绑定"}`,
502
+ `线程绑定:${boundThreadRef ?? "未绑定"}`,
503
+ ...(state.threadTitle ? [`线程标题:${state.threadTitle}`] : []),
504
+ ...(state.threadProjectName ? [`线程项目:${state.threadProjectName}`] : []),
505
+ ...(state.threadRelativeTime ? [`线程最近活动:${state.threadRelativeTime}`] : []),
252
506
  `模型:${state.model ?? "未识别"}`,
253
507
  `推理强度:${state.reasoningEffort ?? "未识别"}`,
254
508
  `工作区:${state.workspace ?? "未识别"}`,
@@ -282,3 +536,32 @@ export class ThreadCommandHandler {
282
536
  ].join("\n");
283
537
  }
284
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
+ }
@@ -0,0 +1,191 @@
1
+ import { ChatgptDesktopDriver } from "../../../packages/adapters/chatgpt-desktop/src/driver.js";
2
+ function printJson(data) {
3
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
4
+ }
5
+ function printError(msg) {
6
+ process.stderr.write(msg + "\n");
7
+ }
8
+ const BOOL_FLAGS = new Set(["json", "help"]);
9
+ function parseArgs(argv) {
10
+ const [, , command = "help", ...rest] = argv;
11
+ const args = [];
12
+ const flags = {};
13
+ for (let i = 0; i < rest.length; i++) {
14
+ const tok = rest[i];
15
+ if (tok.startsWith("--")) {
16
+ const key = tok.slice(2);
17
+ if (BOOL_FLAGS.has(key)) {
18
+ flags[key] = true;
19
+ }
20
+ else {
21
+ const next = rest[i + 1];
22
+ if (next !== undefined && !next.startsWith("--")) {
23
+ flags[key] = next;
24
+ i++;
25
+ }
26
+ else {
27
+ flags[key] = true;
28
+ }
29
+ }
30
+ }
31
+ else {
32
+ args.push(tok);
33
+ }
34
+ }
35
+ return { command, args, flags };
36
+ }
37
+ async function cmdHealth(driver, flags) {
38
+ const result = await driver.health();
39
+ if (flags["json"]) {
40
+ printJson(result);
41
+ }
42
+ else {
43
+ console.log(`App running: ${result.appRunning}`);
44
+ console.log(`Accessibility: ${result.accessibility}`);
45
+ console.log(`Cache dir: ${result.cacheDirFound}`);
46
+ console.log(`Frontmost: ${result.frontmost}`);
47
+ console.log(`Overall OK: ${result.ok}`);
48
+ }
49
+ process.exit(result.ok ? 0 : 1);
50
+ }
51
+ async function cmdAsk(driver, args, flags) {
52
+ const prompt = args[0];
53
+ if (!prompt) {
54
+ printError("Usage: chatgpt-desktop ask [--json] [--session <key>] <prompt>");
55
+ process.exit(1);
56
+ }
57
+ const input = {
58
+ mode: "text",
59
+ prompt,
60
+ sessionKey: typeof flags["session"] === "string" ? flags["session"] : undefined,
61
+ timeoutMs: typeof flags["timeout"] === "string" ? Number(flags["timeout"]) : undefined
62
+ };
63
+ const result = await driver.run(input);
64
+ if (flags["json"]) {
65
+ printJson(result);
66
+ }
67
+ else if (result.ok) {
68
+ console.log(result.text);
69
+ }
70
+ else {
71
+ printError(`Error [${result.errorCode}]: ${result.message}`);
72
+ }
73
+ process.exit(result.ok ? 0 : 1);
74
+ }
75
+ async function cmdImage(driver, args, flags) {
76
+ const prompt = args[0];
77
+ if (!prompt) {
78
+ printError("Usage: chatgpt-desktop image [--json] [--out-dir <dir>] [--session <key>] <prompt>");
79
+ process.exit(1);
80
+ }
81
+ const outDir = typeof flags["out-dir"] === "string" ? flags["out-dir"] : undefined;
82
+ const input = {
83
+ mode: "image",
84
+ prompt,
85
+ sessionKey: typeof flags["session"] === "string" ? flags["session"] : undefined,
86
+ timeoutMs: typeof flags["timeout"] === "string" ? Number(flags["timeout"]) : undefined
87
+ };
88
+ const result = await driver.run(input);
89
+ if (flags["json"]) {
90
+ printJson(result);
91
+ }
92
+ else if (result.ok) {
93
+ for (const m of result.media) {
94
+ console.log(m.localPath);
95
+ }
96
+ if (result.text)
97
+ console.log(result.text);
98
+ }
99
+ else {
100
+ printError(`Error [${result.errorCode}]: ${result.message}`);
101
+ }
102
+ void outDir; // outDir is passed via driver opts at construction time; noted for future use
103
+ process.exit(result.ok ? 0 : 1);
104
+ }
105
+ async function cmdImages(driver, args, flags) {
106
+ const prompt = args[0];
107
+ if (!prompt) {
108
+ printError("Usage: chatgpt-desktop images [--json] [--count <n>] [--out-dir <dir>] <prompt>");
109
+ process.exit(1);
110
+ }
111
+ const count = typeof flags["count"] === "string" ? Math.max(1, parseInt(flags["count"], 10)) : 1;
112
+ const results = [];
113
+ let anyFailed = false;
114
+ for (let i = 0; i < count; i++) {
115
+ const input = {
116
+ mode: "image",
117
+ prompt: `${prompt} (第 ${i + 1} 张,共 ${count} 张)`,
118
+ sessionKey: typeof flags["session"] === "string" ? flags["session"] : undefined,
119
+ timeoutMs: typeof flags["timeout"] === "string" ? Number(flags["timeout"]) : undefined
120
+ };
121
+ const result = await driver.run(input);
122
+ results.push(result);
123
+ if (!result.ok)
124
+ anyFailed = true;
125
+ }
126
+ if (flags["json"]) {
127
+ printJson(results);
128
+ }
129
+ else {
130
+ for (const r of results) {
131
+ if (r.ok) {
132
+ for (const m of r.media)
133
+ console.log(m.localPath);
134
+ }
135
+ else {
136
+ printError(`Error [${r.errorCode}]: ${r.message}`);
137
+ }
138
+ }
139
+ }
140
+ process.exit(anyFailed ? 1 : 0);
141
+ }
142
+ function printHelp() {
143
+ console.log(`chatgpt-desktop <command> [options]
144
+
145
+ Commands:
146
+ health Check App, accessibility and cache dir
147
+ ask <prompt> Send a text message and print reply
148
+ image <prompt> Generate an image and print local path
149
+ images --count N <prompt> Generate N images serially
150
+
151
+ Options:
152
+ --json Output JSON
153
+ --session <key> Session key for conversation continuity
154
+ --out-dir <dir> Output directory for images
155
+ --timeout <ms> Override reply timeout in milliseconds
156
+ --count <n> Number of images (images command only)
157
+ `);
158
+ }
159
+ async function main() {
160
+ const { command, args, flags } = parseArgs(process.argv);
161
+ const outDir = typeof flags["out-dir"] === "string" ? flags["out-dir"] : undefined;
162
+ const driver = new ChatgptDesktopDriver({ destDir: outDir });
163
+ switch (command) {
164
+ case "health":
165
+ await cmdHealth(driver, flags);
166
+ break;
167
+ case "ask":
168
+ case "chat":
169
+ await cmdAsk(driver, args, flags);
170
+ break;
171
+ case "image":
172
+ await cmdImage(driver, args, flags);
173
+ break;
174
+ case "images":
175
+ await cmdImages(driver, args, flags);
176
+ break;
177
+ case "help":
178
+ case "--help":
179
+ case "-h":
180
+ printHelp();
181
+ break;
182
+ default:
183
+ printError(`Unknown command: ${command}`);
184
+ printHelp();
185
+ process.exit(1);
186
+ }
187
+ }
188
+ main().catch((err) => {
189
+ process.stderr.write(`Fatal: ${err instanceof Error ? err.message : String(err)}\n`);
190
+ process.exit(1);
191
+ });