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
@@ -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
+ });
@@ -0,0 +1,446 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { ZodError } from "zod";
6
+ import { loadWeixinGatewayConfigFromEnv } from "./config.js";
7
+ import { WeixinGatewayMessageStore } from "./message-store.js";
8
+ import { createWeixinGatewayServer } from "./server.js";
9
+ import { forwardWeixinInboundToBridge, runWeixinLoginFlow as runWeixinLoginFlowImpl, WeixinClient } from "./weixin-client.js";
10
+ import { WeixinGatewayStateStore } from "./state.js";
11
+ export async function runCli(rawArgs, deps = {}) {
12
+ const args = rawArgs.filter((arg) => arg.length > 0 && arg !== "--");
13
+ const cwd = deps.cwd ?? process.cwd();
14
+ const env = deps.env ?? process.env;
15
+ const packageRoot = deps.packageRoot ?? findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
16
+ const fetchFn = deps.fetchFn ?? fetch;
17
+ const writeStdout = deps.writeStdout ?? ((line) => console.log(line));
18
+ const writeStderr = deps.writeStderr ?? ((line) => console.error(line));
19
+ const parsedArgs = parseCliArgs(args);
20
+ if (!parsedArgs) {
21
+ writeStderr(`[qq-codex-weixin-gateway] 未知命令:${args.join(" ")}`);
22
+ printHelp(writeStdout);
23
+ return 1;
24
+ }
25
+ if (parsedArgs.command === "init") {
26
+ return initEnvTemplate({ cwd, packageRoot, writeStdout, writeStderr });
27
+ }
28
+ if (parsedArgs.command === "help") {
29
+ printHelp(writeStdout);
30
+ return 0;
31
+ }
32
+ const envFilePath = path.join(cwd, ".env");
33
+ if (fs.existsSync(envFilePath)) {
34
+ const loadEnvFile = deps.loadEnvFile ?? process.loadEnvFile.bind(process);
35
+ loadEnvFile(envFilePath);
36
+ }
37
+ try {
38
+ const config = loadWeixinGatewayConfigFromEnv(env);
39
+ const stateStore = deps.createStateStore?.(config.stateFilePath)
40
+ ?? new WeixinGatewayStateStore(config.stateFilePath);
41
+ const runWeixinLoginFlow = deps.runWeixinLoginFlow ?? runWeixinLoginFlowImpl;
42
+ const selectedAccountId = parsedArgs.accountId ?? config.accountId;
43
+ if (parsedArgs.command === "login") {
44
+ const result = await runWeixinLoginFlow({
45
+ accountId: selectedAccountId,
46
+ force: parsedArgs.forceLogin,
47
+ onQrCode: (url) => {
48
+ writeStdout(`[qq-codex-weixin-gateway] 二维码地址:${url}`);
49
+ writeStdout("[qq-codex-weixin-gateway] 请在浏览器打开二维码地址并使用微信扫码确认。");
50
+ },
51
+ config,
52
+ stateStore: stateStore,
53
+ fetchFn
54
+ });
55
+ if (result.qrcodeUrl) {
56
+ writeStdout(`[qq-codex-weixin-gateway] 微信扫码登录成功,accountId=${result.accountId}`);
57
+ }
58
+ else {
59
+ writeStdout(`[qq-codex-weixin-gateway] 账号 ${result.accountId} 已存在可用登录态,baseUrl=${result.baseUrl}`);
60
+ }
61
+ return 0;
62
+ }
63
+ if (parsedArgs.command === "logout") {
64
+ stateStore.clearStoredAccount(selectedAccountId);
65
+ writeStdout(`[qq-codex-weixin-gateway] 已清理账号 ${selectedAccountId} 的登录态`);
66
+ return 0;
67
+ }
68
+ const service = await startWeixinGatewayService({
69
+ cwd,
70
+ env,
71
+ packageRoot,
72
+ fetchFn,
73
+ loadEnvFile: deps.loadEnvFile,
74
+ createMessageStore: deps.createMessageStore,
75
+ createStateStore: deps.createStateStore,
76
+ createServer: deps.createServer,
77
+ createWeixinClient: deps.createWeixinClient,
78
+ watchStateFile: deps.watchStateFile,
79
+ writeStdout,
80
+ writeStderr
81
+ });
82
+ process.once("SIGINT", () => {
83
+ void service.shutdown().finally(() => {
84
+ process.exit(0);
85
+ });
86
+ });
87
+ process.once("SIGTERM", () => {
88
+ void service.shutdown().finally(() => {
89
+ process.exit(0);
90
+ });
91
+ });
92
+ return 0;
93
+ }
94
+ catch (error) {
95
+ if (error instanceof ZodError) {
96
+ writeStderr(`[qq-codex-weixin-gateway] 配置无效:${error.issues.map((issue) => issue.message).join("; ")}`);
97
+ return 1;
98
+ }
99
+ writeStderr(`[qq-codex-weixin-gateway] fatal: ${error instanceof Error ? error.message : String(error)}`);
100
+ if (error instanceof Error && error.stack) {
101
+ writeStderr(` stack: ${error.stack}`);
102
+ }
103
+ return 1;
104
+ }
105
+ }
106
+ export async function startWeixinGatewayService(options = {}) {
107
+ const cwd = options.cwd ?? process.cwd();
108
+ const env = options.env ?? process.env;
109
+ const packageRoot = options.packageRoot ?? findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
110
+ const fetchFn = options.fetchFn ?? fetch;
111
+ const writeStdout = options.writeStdout ?? ((line) => console.log(line));
112
+ const writeStderr = options.writeStderr ?? ((line) => console.error(line));
113
+ const envFilePath = path.join(cwd, ".env");
114
+ if (fs.existsSync(envFilePath)) {
115
+ const loadEnvFile = options.loadEnvFile ?? process.loadEnvFile.bind(process);
116
+ loadEnvFile(envFilePath);
117
+ }
118
+ void packageRoot;
119
+ const config = loadWeixinGatewayConfigFromEnv(env);
120
+ const createMessageStore = options.createMessageStore
121
+ ?? ((filePath, limit) => new WeixinGatewayMessageStore(filePath, limit));
122
+ const messageStores = Object.fromEntries(config.accounts.map((account) => [
123
+ account.accountId,
124
+ createMessageStore(account.messageStorePath, config.recentMessageLimit)
125
+ ]));
126
+ const messageStore = {
127
+ append: (message) => {
128
+ const accountId = resolveOutboundAccountId({
129
+ accountId: message.accountId,
130
+ accountKey: message.accountKey
131
+ }, config.accountId);
132
+ const store = messageStores[accountId] ?? messageStores[config.accountId] ?? Object.values(messageStores)[0];
133
+ store?.append(message);
134
+ },
135
+ listRecent: () => Object.values(messageStores)
136
+ .flatMap((store) => store.listRecent())
137
+ .sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))
138
+ .slice(0, config.recentMessageLimit)
139
+ };
140
+ const stateStore = options.createStateStore?.(config.stateFilePath)
141
+ ?? new WeixinGatewayStateStore(config.stateFilePath);
142
+ const activeClients = new Map();
143
+ const activeClientKeys = new Map();
144
+ const closeActiveClient = async (accountId) => {
145
+ const activeClient = activeClients.get(accountId);
146
+ if (!activeClient) {
147
+ activeClientKeys.delete(accountId);
148
+ return;
149
+ }
150
+ activeClients.delete(accountId);
151
+ activeClientKeys.delete(accountId);
152
+ await activeClient.close();
153
+ };
154
+ const resolveRuntimeAccount = (account) => stateStore.resolveRuntimeAccount(account.accountId, {
155
+ token: account.token,
156
+ baseUrl: account.baseUrl
157
+ });
158
+ const createWeixinClient = options.createWeixinClient
159
+ ?? ((clientOptions) => new WeixinClient({
160
+ ...clientOptions,
161
+ stateStore: clientOptions.stateStore,
162
+ fetchFn: clientOptions.fetchFn
163
+ }));
164
+ const refreshWeixinClient = async (reason) => {
165
+ const expectedAccountIds = new Set(config.accounts.map((account) => account.accountId));
166
+ for (const account of config.accounts) {
167
+ const runtimeAccount = resolveRuntimeAccount(account);
168
+ const nextClientKey = runtimeAccount
169
+ ? `${runtimeAccount.accountId}|${runtimeAccount.baseUrl}|${runtimeAccount.token}`
170
+ : "";
171
+ if (!runtimeAccount) {
172
+ if (activeClients.has(account.accountId)) {
173
+ writeStdout(`[qq-codex-weixin-gateway] 未找到微信登录态,已停用 long-poll client { accountId: ${account.accountId} }`);
174
+ await closeActiveClient(account.accountId);
175
+ }
176
+ continue;
177
+ }
178
+ if (activeClients.has(account.accountId) && activeClientKeys.get(account.accountId) === nextClientKey) {
179
+ continue;
180
+ }
181
+ await closeActiveClient(account.accountId);
182
+ const nextClient = createWeixinClient({
183
+ accountId: runtimeAccount.accountId,
184
+ baseUrl: runtimeAccount.baseUrl,
185
+ token: runtimeAccount.token,
186
+ longPollTimeoutMs: config.longPollTimeoutMs,
187
+ apiTimeoutMs: config.apiTimeoutMs,
188
+ stateStore,
189
+ fetchFn,
190
+ onInboundMessage: async (message) => {
191
+ await forwardWeixinInboundToBridge(fetchFn, {
192
+ bridgeBaseUrl: config.bridgeBaseUrl,
193
+ bridgeWebhookPath: account.bridgeWebhookPath,
194
+ accountKey: `weixin:${account.accountId}`
195
+ }, message);
196
+ }
197
+ });
198
+ activeClients.set(account.accountId, nextClient);
199
+ activeClientKeys.set(account.accountId, nextClientKey);
200
+ void nextClient.connect();
201
+ writeStdout(`[qq-codex-weixin-gateway] 微信 client 已连接 { reason: ${reason}, accountId: ${runtimeAccount.accountId}, baseUrl: ${runtimeAccount.baseUrl} }`);
202
+ }
203
+ for (const accountId of [...activeClients.keys()]) {
204
+ if (!expectedAccountIds.has(accountId)) {
205
+ await closeActiveClient(accountId);
206
+ }
207
+ }
208
+ };
209
+ const outboundSender = {
210
+ sendTextMessage: async ({ accountKey, accountId, peerId, chatType, text, replyToMessageId }) => {
211
+ const targetAccountId = resolveOutboundAccountId({ accountKey, accountId }, config.accountId);
212
+ const activeClient = activeClients.get(targetAccountId);
213
+ if (!activeClient) {
214
+ throw new Error(`weixin gateway has no active logged-in client for account ${targetAccountId}`);
215
+ }
216
+ const contextToken = stateStore.getContextToken(activeClient.accountId, peerId);
217
+ await activeClient.sendTextMessage(peerId, text, contextToken || null);
218
+ console.log("[weixin-gateway] delivered outbound message", {
219
+ accountId: targetAccountId,
220
+ peerId,
221
+ chatType,
222
+ hasContextToken: Boolean(contextToken),
223
+ replyToMessageId
224
+ });
225
+ },
226
+ sendMessage: async ({ accountKey, accountId, peerId, chatType, content, mediaArtifacts, replyToMessageId }) => {
227
+ const targetAccountId = resolveOutboundAccountId({ accountKey, accountId }, config.accountId);
228
+ const activeClient = activeClients.get(targetAccountId);
229
+ if (!activeClient) {
230
+ throw new Error(`weixin gateway has no active logged-in client for account ${targetAccountId}`);
231
+ }
232
+ const contextToken = stateStore.getContextToken(activeClient.accountId, peerId);
233
+ await activeClient.sendMessage({
234
+ peerId,
235
+ chatType,
236
+ ...(content ? { content } : {}),
237
+ ...(mediaArtifacts?.length ? { mediaArtifacts } : {}),
238
+ contextToken: contextToken || null
239
+ });
240
+ console.log("[weixin-gateway] delivered outbound message", {
241
+ accountId: targetAccountId,
242
+ peerId,
243
+ chatType,
244
+ hasContextToken: Boolean(contextToken),
245
+ replyToMessageId,
246
+ mediaCount: mediaArtifacts?.length ?? 0
247
+ });
248
+ }
249
+ };
250
+ const server = options.createServer?.({
251
+ config,
252
+ messageStore,
253
+ fetchFn,
254
+ outboundSender
255
+ })
256
+ ?? createWeixinGatewayServer({
257
+ config,
258
+ messageStore,
259
+ fetchFn,
260
+ outboundSender
261
+ });
262
+ await new Promise((resolve, reject) => {
263
+ server.once("error", reject);
264
+ server.listen(config.listenPort, config.listenHost, () => {
265
+ server.off("error", reject);
266
+ resolve();
267
+ });
268
+ });
269
+ await refreshWeixinClient("startup");
270
+ const shouldWatchStateFile = options.watchStateFile !== false;
271
+ if (shouldWatchStateFile) {
272
+ fs.watchFile(config.stateFilePath, { interval: config.stateWatchIntervalMs }, async (current, previous) => {
273
+ if (current.mtimeMs === previous.mtimeMs) {
274
+ return;
275
+ }
276
+ try {
277
+ stateStore.reload();
278
+ await refreshWeixinClient("state-file-change");
279
+ }
280
+ catch (error) {
281
+ writeStderr(`[qq-codex-weixin-gateway] 刷新微信状态失败:${error instanceof Error ? error.message : String(error)}`);
282
+ }
283
+ });
284
+ }
285
+ writeStdout(`[qq-codex-weixin-gateway] ready { listenHost: ${config.listenHost}, listenPort: ${config.listenPort}, accounts: ${config.accounts.map((account) => account.accountId).join(",")}, loggedIn: ${activeClients.size} }`);
286
+ return {
287
+ shutdown: async () => {
288
+ fs.unwatchFile(config.stateFilePath);
289
+ await Promise.all([...activeClients.keys()].map((accountId) => closeActiveClient(accountId)));
290
+ await new Promise((resolve) => server.close(() => resolve()));
291
+ },
292
+ status: {
293
+ channel: "weixin",
294
+ enabled: config.enabled,
295
+ listenHost: config.listenHost,
296
+ listenPort: config.listenPort,
297
+ loggedIn: activeClients.size > 0,
298
+ accountId: config.accountId,
299
+ accountIds: config.accounts.map((account) => account.accountId),
300
+ loggedInAccountIds: [...activeClients.keys()]
301
+ }
302
+ };
303
+ }
304
+ export async function runCliFromProcess() {
305
+ process.exitCode = await runCli(process.argv.slice(2));
306
+ }
307
+ function initEnvTemplate(options) {
308
+ const targetPath = path.join(options.cwd, ".env.weixin-gateway");
309
+ if (fs.existsSync(targetPath)) {
310
+ options.writeStderr(`[qq-codex-weixin-gateway] 配置文件已存在:${targetPath}`);
311
+ return 1;
312
+ }
313
+ const template = [
314
+ "WEIXIN_ENABLED=true",
315
+ "WEIXIN_ACCOUNT_ID=default",
316
+ "WEIXIN_BASE_URL=https://ilinkai.weixin.qq.com",
317
+ "# WEIXIN_TOKEN=",
318
+ "WEIXIN_LONG_POLL_TIMEOUT_MS=35000",
319
+ "WEIXIN_API_TIMEOUT_MS=15000",
320
+ "WEIXIN_GATEWAY_STATE_FILE_PATH=runtime/weixin-gateway-state.json",
321
+ "WEIXIN_LOGIN_BASE_URL=https://ilinkai.weixin.qq.com",
322
+ "WEIXIN_BOT_TYPE=3",
323
+ "WEIXIN_QR_FETCH_TIMEOUT_MS=10000",
324
+ "WEIXIN_QR_POLL_TIMEOUT_MS=35000",
325
+ "WEIXIN_QR_TOTAL_TIMEOUT_MS=480000",
326
+ "WEIXIN_GATEWAY_STATE_WATCH_INTERVAL_MS=1000",
327
+ "WEIXIN_GATEWAY_LISTEN_HOST=127.0.0.1",
328
+ "WEIXIN_GATEWAY_LISTEN_PORT=3200",
329
+ "WEIXIN_GATEWAY_BRIDGE_BASE_URL=http://127.0.0.1:3100",
330
+ "WEIXIN_GATEWAY_BRIDGE_WEBHOOK_PATH=/webhooks/weixin",
331
+ "# WEIXIN_GATEWAY_EXPECTED_TOKEN=your-token",
332
+ "WEIXIN_GATEWAY_MESSAGE_STORE_PATH=runtime/weixin-gateway-messages.ndjson",
333
+ "WEIXIN_GATEWAY_RECENT_MESSAGE_LIMIT=100",
334
+ ""
335
+ ].join("\n");
336
+ fs.writeFileSync(targetPath, template, "utf8");
337
+ options.writeStdout(`[qq-codex-weixin-gateway] 已生成真实微信网关配置:${targetPath}`);
338
+ options.writeStdout("[qq-codex-weixin-gateway] 你也可以直接把这些变量写进项目根目录的 .env。");
339
+ return 0;
340
+ }
341
+ function resolveOutboundAccountId(target, fallbackAccountId) {
342
+ const explicitAccountId = String(target.accountId ?? "").trim();
343
+ if (explicitAccountId) {
344
+ return explicitAccountId;
345
+ }
346
+ const accountKey = String(target.accountKey ?? "").trim();
347
+ if (accountKey.startsWith("weixin:")) {
348
+ const accountId = accountKey.slice("weixin:".length).trim();
349
+ if (accountId) {
350
+ return accountId;
351
+ }
352
+ }
353
+ return fallbackAccountId;
354
+ }
355
+ function printHelp(writeStdout) {
356
+ writeStdout("qq-codex-weixin-gateway");
357
+ writeStdout("");
358
+ writeStdout("用法:");
359
+ writeStdout(" qq-codex-weixin-gateway 启动真实微信网关(long-poll + 本地转发)");
360
+ writeStdout(" qq-codex-weixin-gateway init 生成 .env.weixin-gateway 模板");
361
+ writeStdout(" qq-codex-weixin-gateway --weixin-login 发起微信扫码登录");
362
+ writeStdout(" qq-codex-weixin-gateway --weixin-login-force 强制重新扫码登录");
363
+ writeStdout(" qq-codex-weixin-gateway --weixin-logout 清理微信登录态");
364
+ writeStdout(" qq-codex-weixin-gateway help 查看帮助");
365
+ }
366
+ function findPackageRoot(startDir) {
367
+ let currentDir = startDir;
368
+ while (true) {
369
+ if (fs.existsSync(path.join(currentDir, "package.json"))) {
370
+ return currentDir;
371
+ }
372
+ const parentDir = path.dirname(currentDir);
373
+ if (parentDir === currentDir) {
374
+ throw new Error(`Unable to locate package root from ${startDir}`);
375
+ }
376
+ currentDir = parentDir;
377
+ }
378
+ }
379
+ function parseCliArgs(args) {
380
+ if (args.length === 0) {
381
+ return {
382
+ command: "serve",
383
+ forceLogin: false
384
+ };
385
+ }
386
+ if (args.length === 1 && (args[0] === "help" || args[0] === "-h" || args[0] === "--help")) {
387
+ return {
388
+ command: "help",
389
+ forceLogin: false
390
+ };
391
+ }
392
+ if (args.length === 1 && args[0] === "init") {
393
+ return {
394
+ command: "init",
395
+ forceLogin: false
396
+ };
397
+ }
398
+ let command = null;
399
+ let accountId;
400
+ let forceLogin = false;
401
+ for (let index = 0; index < args.length; index += 1) {
402
+ const arg = args[index];
403
+ if (arg === "--weixin-login") {
404
+ command = "login";
405
+ continue;
406
+ }
407
+ if (arg === "--weixin-login-force") {
408
+ command = "login";
409
+ forceLogin = true;
410
+ continue;
411
+ }
412
+ if (arg === "--weixin-logout") {
413
+ command = "logout";
414
+ continue;
415
+ }
416
+ if (arg === "--weixin-account") {
417
+ accountId = args[index + 1];
418
+ index += 1;
419
+ continue;
420
+ }
421
+ return null;
422
+ }
423
+ if (!command) {
424
+ return null;
425
+ }
426
+ return {
427
+ command,
428
+ accountId,
429
+ forceLogin
430
+ };
431
+ }
432
+ const isEntrypoint = (() => {
433
+ const entrypoint = process.argv[1];
434
+ if (!entrypoint) {
435
+ return false;
436
+ }
437
+ try {
438
+ return fileURLToPath(import.meta.url) === path.resolve(entrypoint);
439
+ }
440
+ catch {
441
+ return false;
442
+ }
443
+ })();
444
+ if (isEntrypoint) {
445
+ void runCliFromProcess();
446
+ }