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.
- 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 +309 -26
- 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
|
}
|
|
@@ -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.
|
|
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) =>
|
|
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.
|
|
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.
|
|
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
|
-
|
|
67
|
-
|
|
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.
|
|
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
|
|
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
|
-
"|
|
|
207
|
-
"|
|
|
208
|
-
"|
|
|
209
|
-
"|
|
|
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
|
-
"
|
|
219
|
-
"
|
|
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
|
-
"这条 `/`
|
|
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
|
-
`线程绑定:${
|
|
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
|
+
});
|