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
|
@@ -2,13 +2,14 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { BridgeSessionStatus } from "../../domain/src/session.js";
|
|
3
3
|
import { TurnEventType } from "../../domain/src/message.js";
|
|
4
4
|
import { DesktopDriverError } from "../../domain/src/driver.js";
|
|
5
|
-
import { formatQqOutboundDraft } from "./qq-outbound-format.js";
|
|
6
5
|
export class BridgeOrchestrator {
|
|
7
6
|
deps;
|
|
8
7
|
recentInboundFingerprints = new Map();
|
|
9
8
|
turnStates = new Map();
|
|
9
|
+
draftFormatter;
|
|
10
10
|
constructor(deps) {
|
|
11
11
|
this.deps = deps;
|
|
12
|
+
this.draftFormatter = deps.draftFormatter ?? ((draft) => draft);
|
|
12
13
|
}
|
|
13
14
|
async handleInbound(message) {
|
|
14
15
|
const alreadySeen = await this.deps.transcriptStore.hasInbound(message.messageId);
|
|
@@ -36,7 +37,9 @@ export class BridgeOrchestrator {
|
|
|
36
37
|
chatType: message.chatType,
|
|
37
38
|
peerId: message.senderId,
|
|
38
39
|
codexThreadRef: null,
|
|
40
|
+
lastCodexTurnId: null,
|
|
39
41
|
skillContextKey: null,
|
|
42
|
+
conversationProvider: null,
|
|
40
43
|
status: BridgeSessionStatus.Active,
|
|
41
44
|
lastInboundAt: message.receivedAt,
|
|
42
45
|
lastOutboundAt: null,
|
|
@@ -54,18 +57,25 @@ export class BridgeOrchestrator {
|
|
|
54
57
|
return;
|
|
55
58
|
}
|
|
56
59
|
deliveredDraftIds.add(draft.draftId);
|
|
57
|
-
|
|
60
|
+
if (draft.turnId) {
|
|
61
|
+
await this.deps.sessionStore.updateLastCodexTurnId(message.sessionKey, draft.turnId);
|
|
62
|
+
}
|
|
63
|
+
const formattedDraft = this.draftFormatter(draft);
|
|
64
|
+
if (isEmptyDraft(formattedDraft)) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
await this.deps.transcriptStore.recordOutbound(formattedDraft);
|
|
58
68
|
try {
|
|
59
|
-
await this.deps.qqEgress.deliver(
|
|
60
|
-
this.recordDeliveredDraft(
|
|
69
|
+
await this.deps.qqEgress.deliver(formattedDraft);
|
|
70
|
+
this.recordDeliveredDraft(formattedDraft);
|
|
61
71
|
}
|
|
62
72
|
catch (error) {
|
|
63
73
|
const reason = error instanceof Error ? error.message : String(error);
|
|
64
|
-
deliveryErrors.push(`${
|
|
74
|
+
deliveryErrors.push(`${formattedDraft.draftId}: ${reason}`);
|
|
65
75
|
console.warn("[qq-codex-bridge] draft delivery failed", {
|
|
66
76
|
sessionKey: message.sessionKey,
|
|
67
77
|
messageId: message.messageId,
|
|
68
|
-
draftId:
|
|
78
|
+
draftId: formattedDraft.draftId,
|
|
69
79
|
error: reason
|
|
70
80
|
});
|
|
71
81
|
}
|
|
@@ -100,6 +110,7 @@ export class BridgeOrchestrator {
|
|
|
100
110
|
if (event.sequence <= state.lastSequence) {
|
|
101
111
|
return;
|
|
102
112
|
}
|
|
113
|
+
await this.deps.sessionStore.updateLastCodexTurnId(event.sessionKey, event.turnId);
|
|
103
114
|
state.lastSequence = event.sequence;
|
|
104
115
|
state.lastEventAt = event.createdAt;
|
|
105
116
|
if (typeof event.payload.fullText === "string") {
|
|
@@ -117,7 +128,7 @@ export class BridgeOrchestrator {
|
|
|
117
128
|
state.finalFlushed = true;
|
|
118
129
|
return;
|
|
119
130
|
}
|
|
120
|
-
const draft =
|
|
131
|
+
const draft = this.draftFormatter({
|
|
121
132
|
draftId: randomUUID(),
|
|
122
133
|
turnId: event.turnId,
|
|
123
134
|
sessionKey: event.sessionKey,
|
|
@@ -127,48 +138,65 @@ export class BridgeOrchestrator {
|
|
|
127
138
|
? { replyToMessageId: event.payload.replyToMessageId }
|
|
128
139
|
: {})
|
|
129
140
|
});
|
|
130
|
-
|
|
141
|
+
const pendingArtifacts = filterPendingArtifacts(draft.mediaArtifacts ?? [], state.sentArtifactKeys);
|
|
142
|
+
const normalizedDraft = pendingArtifacts.length === (draft.mediaArtifacts?.length ?? 0)
|
|
143
|
+
? draft
|
|
144
|
+
: {
|
|
145
|
+
...draft,
|
|
146
|
+
mediaArtifacts: pendingArtifacts
|
|
147
|
+
};
|
|
148
|
+
if (isEmptyDraft(normalizedDraft)) {
|
|
131
149
|
state.sentText = state.assembledText;
|
|
132
150
|
state.completed = true;
|
|
133
151
|
state.finalFlushed = true;
|
|
134
152
|
return;
|
|
135
153
|
}
|
|
136
|
-
await this.deps.transcriptStore.recordOutbound(
|
|
137
|
-
await this.deps.qqEgress.deliver(
|
|
154
|
+
await this.deps.transcriptStore.recordOutbound(normalizedDraft);
|
|
155
|
+
await this.deps.qqEgress.deliver(normalizedDraft);
|
|
156
|
+
this.recordDeliveredDraft(normalizedDraft);
|
|
138
157
|
state.sentText = state.assembledText;
|
|
139
158
|
state.completed = true;
|
|
140
159
|
state.finalFlushed = true;
|
|
141
160
|
});
|
|
142
161
|
}
|
|
143
162
|
isLikelyDuplicateInbound(message) {
|
|
144
|
-
const record = this.recentInboundFingerprints.get(message.sessionKey);
|
|
145
|
-
if (!record) {
|
|
146
|
-
return false;
|
|
147
|
-
}
|
|
148
163
|
const receivedAtMs = Date.parse(message.receivedAt);
|
|
149
164
|
if (!Number.isFinite(receivedAtMs)) {
|
|
150
165
|
return false;
|
|
151
166
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
167
|
+
const records = this.getRecentInboundRecords(message.sessionKey, receivedAtMs);
|
|
168
|
+
if (!records.length) {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
const fingerprint = buildInboundFingerprint(message);
|
|
172
|
+
return records.some((record) => record.fingerprint === fingerprint
|
|
173
|
+
&& receivedAtMs - record.receivedAtMs >= 0
|
|
174
|
+
&& receivedAtMs - record.receivedAtMs <= 90_000);
|
|
155
175
|
}
|
|
156
176
|
rememberInbound(message) {
|
|
157
177
|
const receivedAtMs = Date.parse(message.receivedAt);
|
|
158
178
|
if (!Number.isFinite(receivedAtMs)) {
|
|
159
179
|
return;
|
|
160
180
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
if (now - record.receivedAtMs > 120_000) {
|
|
164
|
-
this.recentInboundFingerprints.delete(sessionKey);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
this.recentInboundFingerprints.set(message.sessionKey, {
|
|
181
|
+
const records = this.getRecentInboundRecords(message.sessionKey, receivedAtMs);
|
|
182
|
+
records.push({
|
|
168
183
|
fingerprint: buildInboundFingerprint(message),
|
|
169
184
|
receivedAtMs,
|
|
170
185
|
messageId: message.messageId
|
|
171
186
|
});
|
|
187
|
+
this.recentInboundFingerprints.set(message.sessionKey, records);
|
|
188
|
+
}
|
|
189
|
+
getRecentInboundRecords(sessionKey, referenceTimeMs) {
|
|
190
|
+
for (const [key, records] of this.recentInboundFingerprints.entries()) {
|
|
191
|
+
const retained = records.filter((record) => referenceTimeMs - record.receivedAtMs <= 120_000);
|
|
192
|
+
if (retained.length > 0) {
|
|
193
|
+
this.recentInboundFingerprints.set(key, retained);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
this.recentInboundFingerprints.delete(key);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return [...(this.recentInboundFingerprints.get(sessionKey) ?? [])];
|
|
172
200
|
}
|
|
173
201
|
getOrCreateTurnState(event) {
|
|
174
202
|
const key = buildTurnStateKey(event.sessionKey, event.turnId);
|
|
@@ -180,6 +208,7 @@ export class BridgeOrchestrator {
|
|
|
180
208
|
lastSequence: 0,
|
|
181
209
|
assembledText: "",
|
|
182
210
|
sentText: "",
|
|
211
|
+
sentArtifactKeys: new Set(),
|
|
183
212
|
lastEventAt: null,
|
|
184
213
|
completed: false,
|
|
185
214
|
finalFlushed: false
|
|
@@ -188,7 +217,7 @@ export class BridgeOrchestrator {
|
|
|
188
217
|
return created;
|
|
189
218
|
}
|
|
190
219
|
recordDeliveredDraft(draft) {
|
|
191
|
-
if (!draft.turnId
|
|
220
|
+
if (!draft.turnId) {
|
|
192
221
|
return;
|
|
193
222
|
}
|
|
194
223
|
const key = buildTurnStateKey(draft.sessionKey, draft.turnId);
|
|
@@ -196,11 +225,15 @@ export class BridgeOrchestrator {
|
|
|
196
225
|
lastSequence: 0,
|
|
197
226
|
assembledText: "",
|
|
198
227
|
sentText: "",
|
|
228
|
+
sentArtifactKeys: new Set(),
|
|
199
229
|
lastEventAt: null,
|
|
200
230
|
completed: false,
|
|
201
231
|
finalFlushed: false
|
|
202
232
|
};
|
|
203
233
|
state.sentText += draft.text;
|
|
234
|
+
for (const artifact of draft.mediaArtifacts ?? []) {
|
|
235
|
+
state.sentArtifactKeys.add(buildArtifactKey(artifact));
|
|
236
|
+
}
|
|
204
237
|
this.turnStates.set(key, state);
|
|
205
238
|
}
|
|
206
239
|
}
|
|
@@ -259,3 +292,17 @@ function findSuffixPrefixOverlap(previous, next) {
|
|
|
259
292
|
function stripWhitespace(value) {
|
|
260
293
|
return value.replace(/\s+/g, "");
|
|
261
294
|
}
|
|
295
|
+
function filterPendingArtifacts(artifacts, sentArtifactKeys) {
|
|
296
|
+
return artifacts.filter((artifact) => !sentArtifactKeys.has(buildArtifactKey(artifact)));
|
|
297
|
+
}
|
|
298
|
+
function buildArtifactKey(artifact) {
|
|
299
|
+
return [
|
|
300
|
+
artifact.kind,
|
|
301
|
+
artifact.localPath || "",
|
|
302
|
+
artifact.sourceUrl || "",
|
|
303
|
+
artifact.originalName || ""
|
|
304
|
+
].join("::");
|
|
305
|
+
}
|
|
306
|
+
function isEmptyDraft(draft) {
|
|
307
|
+
return !draft.text.trim() && !(draft.mediaArtifacts?.length);
|
|
308
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function formatWeixinOutboundDraft(draft) {
|
|
2
|
+
const formattedText = formatWeixinOutboundText(draft.text, draft.mediaArtifacts ?? []);
|
|
3
|
+
if (formattedText === draft.text) {
|
|
4
|
+
return draft;
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
...draft,
|
|
8
|
+
text: formattedText
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function formatWeixinOutboundText(text, mediaArtifacts) {
|
|
12
|
+
if (!text) {
|
|
13
|
+
return text;
|
|
14
|
+
}
|
|
15
|
+
const strippedMediaBlocks = text.replace(/<qqmedia>[\s\S]*?<\/qqmedia>/g, "");
|
|
16
|
+
const hasMediaArtifacts = mediaArtifacts.length > 0;
|
|
17
|
+
const pathSet = new Set(mediaArtifacts.flatMap((artifact) => [artifact.localPath, artifact.sourceUrl].filter((value) => typeof value === "string" && value.trim().length > 0)));
|
|
18
|
+
const normalizedLines = strippedMediaBlocks
|
|
19
|
+
.split("\n")
|
|
20
|
+
.filter((line) => {
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
if (trimmed.includes("<qqmedia>") || trimmed.includes("</qqmedia>")) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (pathSet.has(stripWrapping(trimmed))) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (containsOnlyArtifactPath(trimmed, pathSet)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
if (hasMediaArtifacts && shouldDropInternalBridgeLine(trimmed)) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
});
|
|
39
|
+
return normalizedLines.join("\n").trim();
|
|
40
|
+
}
|
|
41
|
+
function containsOnlyArtifactPath(line, paths) {
|
|
42
|
+
const normalized = stripWrapping(line);
|
|
43
|
+
return Array.from(paths).some((path) => normalized === stripWrapping(path));
|
|
44
|
+
}
|
|
45
|
+
function stripWrapping(value) {
|
|
46
|
+
return value.replace(/^[-*]\s*/, "").replace(/^`+/, "").replace(/`+$/, "").trim();
|
|
47
|
+
}
|
|
48
|
+
function shouldDropInternalBridgeLine(line) {
|
|
49
|
+
return (line.includes("QQBot 桥接程序收到你上传附件后") ||
|
|
50
|
+
line.includes("临时落盘的运行目录路径") ||
|
|
51
|
+
line.includes("这里看到的是相对路径") ||
|
|
52
|
+
line.includes("runtime/media/") ||
|
|
53
|
+
line.includes("/Volumes/") ||
|
|
54
|
+
/^[A-Za-z]:[\\/]/.test(line));
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -13,7 +13,9 @@ export class SqliteSessionStore {
|
|
|
13
13
|
chat_type AS chatType,
|
|
14
14
|
peer_id AS peerId,
|
|
15
15
|
codex_thread_ref AS codexThreadRef,
|
|
16
|
+
last_codex_turn_id AS lastCodexTurnId,
|
|
16
17
|
skill_context_key AS skillContextKey,
|
|
18
|
+
conversation_provider AS conversationProvider,
|
|
17
19
|
status,
|
|
18
20
|
last_inbound_at AS lastInboundAt,
|
|
19
21
|
last_outbound_at AS lastOutboundAt,
|
|
@@ -27,9 +29,10 @@ export class SqliteSessionStore {
|
|
|
27
29
|
this.db
|
|
28
30
|
.prepare(`INSERT INTO bridge_sessions (
|
|
29
31
|
session_key, account_key, peer_key, chat_type, peer_id,
|
|
30
|
-
codex_thread_ref, skill_context_key, status,
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
codex_thread_ref, last_codex_turn_id, skill_context_key, status,
|
|
33
|
+
last_inbound_at, last_outbound_at, last_error
|
|
34
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
35
|
+
.run(session.sessionKey, session.accountKey, session.peerKey, session.chatType, session.peerId, session.codexThreadRef, session.lastCodexTurnId, session.skillContextKey, session.status, session.lastInboundAt, session.lastOutboundAt, session.lastError);
|
|
33
36
|
}
|
|
34
37
|
async updateSessionStatus(sessionKey, status, lastError = null) {
|
|
35
38
|
this.db
|
|
@@ -41,11 +44,21 @@ export class SqliteSessionStore {
|
|
|
41
44
|
.prepare(`UPDATE bridge_sessions SET codex_thread_ref = ? WHERE session_key = ?`)
|
|
42
45
|
.run(codexThreadRef, sessionKey);
|
|
43
46
|
}
|
|
47
|
+
async updateLastCodexTurnId(sessionKey, lastCodexTurnId) {
|
|
48
|
+
this.db
|
|
49
|
+
.prepare(`UPDATE bridge_sessions SET last_codex_turn_id = ? WHERE session_key = ?`)
|
|
50
|
+
.run(lastCodexTurnId, sessionKey);
|
|
51
|
+
}
|
|
44
52
|
async updateSkillContextKey(sessionKey, skillContextKey) {
|
|
45
53
|
this.db
|
|
46
54
|
.prepare(`UPDATE bridge_sessions SET skill_context_key = ? WHERE session_key = ?`)
|
|
47
55
|
.run(skillContextKey, sessionKey);
|
|
48
56
|
}
|
|
57
|
+
async updateConversationProvider(sessionKey, provider) {
|
|
58
|
+
this.db
|
|
59
|
+
.prepare(`UPDATE bridge_sessions SET conversation_provider = ? WHERE session_key = ?`)
|
|
60
|
+
.run(provider, sessionKey);
|
|
61
|
+
}
|
|
49
62
|
async withSessionLock(sessionKey, work) {
|
|
50
63
|
// Queue same-session work in-process so the SQLite lock row reflects a real exclusive section.
|
|
51
64
|
const previousTail = this.sessionLockTails.get(sessionKey) ?? Promise.resolve();
|
|
@@ -16,6 +16,7 @@ export function createSqliteDatabase(filePath) {
|
|
|
16
16
|
chat_type TEXT NOT NULL,
|
|
17
17
|
peer_id TEXT NOT NULL,
|
|
18
18
|
codex_thread_ref TEXT,
|
|
19
|
+
last_codex_turn_id TEXT,
|
|
19
20
|
skill_context_key TEXT,
|
|
20
21
|
status TEXT NOT NULL,
|
|
21
22
|
last_inbound_at TEXT,
|
|
@@ -53,6 +54,8 @@ export function createSqliteDatabase(filePath) {
|
|
|
53
54
|
);
|
|
54
55
|
`);
|
|
55
56
|
ensureColumn(db, "bridge_sessions", "skill_context_key", "TEXT");
|
|
57
|
+
ensureColumn(db, "bridge_sessions", "last_codex_turn_id", "TEXT");
|
|
58
|
+
ensureColumn(db, "bridge_sessions", "conversation_provider", "TEXT");
|
|
56
59
|
return db;
|
|
57
60
|
}
|
|
58
61
|
function ensureColumn(db, tableName, columnName, columnDefinition) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qq-codex-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "A bridge between QQ Official Bot and Codex Desktop, with media relay, STT, thread management, and incremental reply delivery.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/983033995/qq-codex-bridge",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"typescript"
|
|
22
22
|
],
|
|
23
23
|
"bin": {
|
|
24
|
-
"qq-codex-bridge": "bin/qq-codex-bridge.js"
|
|
24
|
+
"qq-codex-bridge": "bin/qq-codex-bridge.js",
|
|
25
|
+
"qq-codex-weixin-gateway": "bin/qq-codex-weixin-gateway.js"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"bin",
|
|
@@ -37,7 +38,12 @@
|
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsc -p tsconfig.json",
|
|
39
40
|
"dev": "tsx --env-file=.env apps/bridge-daemon/src/dev.ts",
|
|
41
|
+
"gitnexus:detect-changes": "node scripts/gitnexus-detect-changes.mjs",
|
|
42
|
+
"weixin:login": "tsx --env-file=.env apps/weixin-gateway/src/dev.ts -- --weixin-login",
|
|
43
|
+
"weixin:login:force": "tsx --env-file=.env apps/weixin-gateway/src/dev.ts -- --weixin-login-force",
|
|
44
|
+
"weixin:logout": "tsx --env-file=.env apps/weixin-gateway/src/dev.ts -- --weixin-logout",
|
|
40
45
|
"start": "node dist/apps/bridge-daemon/src/cli.js",
|
|
46
|
+
"start:weixin-gateway": "node dist/apps/weixin-gateway/src/cli.js",
|
|
41
47
|
"debug:codex-workers": "tsx apps/bridge-daemon/src/debug-codex-workers.ts",
|
|
42
48
|
"test": "vitest run",
|
|
43
49
|
"test:watch": "vitest",
|