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.
- 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 +320 -23
- 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
|
@@ -12,8 +12,12 @@ export class CodexDesktopDriver {
|
|
|
12
12
|
replyPollIntervalMs;
|
|
13
13
|
replyStablePolls;
|
|
14
14
|
partialReplyStablePolls;
|
|
15
|
+
composerSubmitPollIntervalMs;
|
|
15
16
|
sleep;
|
|
17
|
+
localRolloutReader;
|
|
18
|
+
localSubmissionReader;
|
|
16
19
|
pendingReplyBaselines = new Map();
|
|
20
|
+
pendingLocalRolloutCursors = new Map();
|
|
17
21
|
constructor(cdp, options = {}) {
|
|
18
22
|
this.cdp = cdp;
|
|
19
23
|
this.replyPollAttempts = Math.max(1, options.replyPollAttempts ?? 60);
|
|
@@ -21,9 +25,12 @@ export class CodexDesktopDriver {
|
|
|
21
25
|
this.replyPollIntervalMs = options.replyPollIntervalMs ?? 500;
|
|
22
26
|
this.replyStablePolls = Math.max(1, options.replyStablePolls ?? 3);
|
|
23
27
|
this.partialReplyStablePolls = Math.max(1, options.partialReplyStablePolls ?? 2);
|
|
28
|
+
this.composerSubmitPollIntervalMs = Math.max(50, options.composerSubmitPollIntervalMs ?? 300);
|
|
24
29
|
this.sleep =
|
|
25
30
|
options.sleep ??
|
|
26
31
|
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
32
|
+
this.localRolloutReader = options.localRolloutReader ?? null;
|
|
33
|
+
this.localSubmissionReader = options.localSubmissionReader ?? null;
|
|
27
34
|
}
|
|
28
35
|
async ensureAppReady() {
|
|
29
36
|
await this.cdp.connect();
|
|
@@ -152,9 +159,12 @@ export class CodexDesktopDriver {
|
|
|
152
159
|
const targetId = await this.ensureThreadSelected(binding);
|
|
153
160
|
const baselineReply = await this.readLatestAssistantSnapshot(targetId);
|
|
154
161
|
this.pendingReplyBaselines.set(binding.sessionKey, baselineReply);
|
|
162
|
+
await this.capturePendingLocalRolloutCursor(binding);
|
|
163
|
+
const submissionCursor = this.capturePendingLocalSubmissionCursor(binding);
|
|
155
164
|
const focusResult = (await this.cdp.evaluateOnPage(this.buildFocusComposerScript(), targetId));
|
|
156
165
|
if (!focusResult?.ok) {
|
|
157
166
|
this.pendingReplyBaselines.delete(binding.sessionKey);
|
|
167
|
+
this.pendingLocalRolloutCursors.delete(binding.sessionKey);
|
|
158
168
|
throw new DesktopDriverError(`Codex desktop input box not found: ${focusResult?.reason ?? "unknown"}`, "input_not_found");
|
|
159
169
|
}
|
|
160
170
|
await this.cdp.dispatchKeyEvent({
|
|
@@ -177,9 +187,20 @@ export class CodexDesktopDriver {
|
|
|
177
187
|
}, targetId);
|
|
178
188
|
await this.cdp.insertText(message.text, targetId);
|
|
179
189
|
const result = (await this.cdp.evaluateOnPage(this.buildSubmitComposerScript(), targetId));
|
|
180
|
-
if (result?.ok) {
|
|
190
|
+
if (result?.ok && !submissionCursor) {
|
|
181
191
|
return;
|
|
182
192
|
}
|
|
193
|
+
const confirmedAfterInitialSubmit = await this.waitForSubmissionConfirmation(binding.sessionKey, targetId, submissionCursor, 4);
|
|
194
|
+
if (confirmedAfterInitialSubmit.submitted) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
console.warn("[qq-codex-bridge] codex composer submit not yet confirmed", {
|
|
198
|
+
sessionKey: binding.sessionKey,
|
|
199
|
+
messageId: message.messageId,
|
|
200
|
+
targetId,
|
|
201
|
+
initialResult: result ?? null,
|
|
202
|
+
confirmedAfterInitialSubmit
|
|
203
|
+
});
|
|
183
204
|
await this.cdp.dispatchKeyEvent({
|
|
184
205
|
type: "keyDown",
|
|
185
206
|
key: "Enter",
|
|
@@ -194,14 +215,62 @@ export class CodexDesktopDriver {
|
|
|
194
215
|
windowsVirtualKeyCode: 13,
|
|
195
216
|
nativeVirtualKeyCode: 13
|
|
196
217
|
}, targetId);
|
|
197
|
-
const retryResult =
|
|
218
|
+
const retryResult = await this.waitForSubmissionConfirmation(binding.sessionKey, targetId, submissionCursor, 4);
|
|
198
219
|
if (retryResult?.submitted) {
|
|
199
220
|
return;
|
|
200
221
|
}
|
|
201
222
|
this.pendingReplyBaselines.delete(binding.sessionKey);
|
|
223
|
+
this.pendingLocalRolloutCursors.delete(binding.sessionKey);
|
|
224
|
+
console.error("[qq-codex-bridge] codex composer submit failed", {
|
|
225
|
+
sessionKey: binding.sessionKey,
|
|
226
|
+
messageId: message.messageId,
|
|
227
|
+
targetId,
|
|
228
|
+
initialResult: result ?? null,
|
|
229
|
+
confirmedAfterInitialSubmit,
|
|
230
|
+
retryResult
|
|
231
|
+
});
|
|
202
232
|
throw new DesktopDriverError(`Codex desktop composer submit failed: ${retryResult?.reason ?? result?.reason ?? "unknown"}`, "submit_failed");
|
|
203
233
|
}
|
|
234
|
+
async waitForSubmissionConfirmation(sessionKey, targetId, submissionCursor, attempts) {
|
|
235
|
+
if (submissionCursor && this.localSubmissionReader) {
|
|
236
|
+
const result = await this.localSubmissionReader.waitForTurnSubmission(submissionCursor, {
|
|
237
|
+
pollAttempts: attempts,
|
|
238
|
+
pollIntervalMs: this.composerSubmitPollIntervalMs
|
|
239
|
+
});
|
|
240
|
+
if (result.submitted) {
|
|
241
|
+
this.attachPendingTurnId(sessionKey, result.turnId);
|
|
242
|
+
}
|
|
243
|
+
return result;
|
|
244
|
+
}
|
|
245
|
+
return this.waitForComposerSubmission(targetId, attempts);
|
|
246
|
+
}
|
|
247
|
+
async waitForComposerSubmission(targetId, attempts) {
|
|
248
|
+
let lastResult;
|
|
249
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
250
|
+
lastResult = (await this.cdp.evaluateOnPage(this.buildComposerSubmissionStateScript(), targetId));
|
|
251
|
+
if (lastResult?.submitted) {
|
|
252
|
+
return lastResult;
|
|
253
|
+
}
|
|
254
|
+
if (attempt + 1 < attempts) {
|
|
255
|
+
await this.sleep(this.composerSubmitPollIntervalMs);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return lastResult ?? { submitted: false, reason: "submit_not_confirmed" };
|
|
259
|
+
}
|
|
204
260
|
async collectAssistantReply(binding, options = {}) {
|
|
261
|
+
const localCursor = this.pendingLocalRolloutCursors.get(binding.sessionKey)
|
|
262
|
+
?? this.captureAdHocLocalRolloutCursor(binding);
|
|
263
|
+
if (localCursor) {
|
|
264
|
+
const localReply = await this.localRolloutReader?.waitForTurnCompletion(localCursor, {
|
|
265
|
+
pollAttempts: this.maxReplyPollAttempts,
|
|
266
|
+
pollIntervalMs: this.replyPollIntervalMs
|
|
267
|
+
});
|
|
268
|
+
this.pendingLocalRolloutCursors.delete(binding.sessionKey);
|
|
269
|
+
if (localReply) {
|
|
270
|
+
this.pendingReplyBaselines.delete(binding.sessionKey);
|
|
271
|
+
return this.collectAssistantReplyFromLocalRollout(binding, localReply, options);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
205
274
|
const targetId = await this.ensureThreadSelected(binding);
|
|
206
275
|
const baselineReply = this.pendingReplyBaselines.get(binding.sessionKey);
|
|
207
276
|
let candidateReply = null;
|
|
@@ -346,6 +415,20 @@ export class CodexDesktopDriver {
|
|
|
346
415
|
createdAt: new Date().toISOString()
|
|
347
416
|
};
|
|
348
417
|
}
|
|
418
|
+
buildOutboundDraftFromText(sessionKey, text, mediaReferences, turnId) {
|
|
419
|
+
return {
|
|
420
|
+
draftId: randomUUID(),
|
|
421
|
+
...(turnId ? { turnId } : {}),
|
|
422
|
+
sessionKey,
|
|
423
|
+
text,
|
|
424
|
+
...(mediaReferences.length > 0
|
|
425
|
+
? {
|
|
426
|
+
mediaArtifacts: mediaReferences.map((reference) => buildMediaArtifactFromReference(reference))
|
|
427
|
+
}
|
|
428
|
+
: {}),
|
|
429
|
+
createdAt: new Date().toISOString()
|
|
430
|
+
};
|
|
431
|
+
}
|
|
349
432
|
buildIncrementalDraftFromSnapshot(sessionKey, snapshot, emittedReplyText, emittedMediaReferences, turnId) {
|
|
350
433
|
const fullReply = snapshot.reply ?? "";
|
|
351
434
|
const deltaText = this.extractReplyDelta(emittedReplyText, fullReply).trim();
|
|
@@ -392,29 +475,187 @@ export class CodexDesktopDriver {
|
|
|
392
475
|
hasAssistantContent(snapshot) {
|
|
393
476
|
return Boolean(snapshot.reply && snapshot.reply.trim()) || snapshot.mediaReferences.length > 0;
|
|
394
477
|
}
|
|
478
|
+
async collectAssistantReplyFromLocalRollout(binding, localReply, options) {
|
|
479
|
+
const turnId = localReply.turnId ?? randomUUID();
|
|
480
|
+
let turnSequence = 0;
|
|
481
|
+
const emitTurnEvent = async (eventType, payload, isFinal) => {
|
|
482
|
+
if (!options.onTurnEvent) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
turnSequence += 1;
|
|
486
|
+
await options.onTurnEvent({
|
|
487
|
+
sessionKey: binding.sessionKey,
|
|
488
|
+
turnId,
|
|
489
|
+
sequence: turnSequence,
|
|
490
|
+
eventType,
|
|
491
|
+
createdAt: new Date().toISOString(),
|
|
492
|
+
isFinal,
|
|
493
|
+
payload
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
if (options.onDraft) {
|
|
497
|
+
let assembledText = "";
|
|
498
|
+
for (const commentary of localReply.commentaryMessages) {
|
|
499
|
+
const draft = this.buildOutboundDraftFromText(binding.sessionKey, commentary, [], turnId);
|
|
500
|
+
assembledText = assembledText ? `${assembledText}\n${commentary}` : commentary;
|
|
501
|
+
await emitTurnEvent(TurnEventType.Delta, {
|
|
502
|
+
text: commentary,
|
|
503
|
+
fullText: assembledText,
|
|
504
|
+
mediaReferences: []
|
|
505
|
+
}, false);
|
|
506
|
+
await options.onDraft(draft);
|
|
507
|
+
}
|
|
508
|
+
await emitTurnEvent(TurnEventType.Completed, {
|
|
509
|
+
fullText: localReply.fullText,
|
|
510
|
+
mediaReferences: localReply.mediaReferences,
|
|
511
|
+
completionReason: "stable"
|
|
512
|
+
}, true);
|
|
513
|
+
return [];
|
|
514
|
+
}
|
|
515
|
+
await emitTurnEvent(TurnEventType.Delta, {
|
|
516
|
+
fullText: localReply.finalText,
|
|
517
|
+
mediaReferences: localReply.mediaReferences
|
|
518
|
+
}, false);
|
|
519
|
+
await emitTurnEvent(TurnEventType.Completed, {
|
|
520
|
+
fullText: localReply.finalText,
|
|
521
|
+
mediaReferences: localReply.mediaReferences,
|
|
522
|
+
completionReason: "stable"
|
|
523
|
+
}, true);
|
|
524
|
+
return [
|
|
525
|
+
this.buildOutboundDraftFromText(binding.sessionKey, localReply.finalText, localReply.mediaReferences, turnId)
|
|
526
|
+
];
|
|
527
|
+
}
|
|
395
528
|
async markSessionBroken(_sessionKey, _reason) {
|
|
396
529
|
return;
|
|
397
530
|
}
|
|
398
|
-
async
|
|
399
|
-
|
|
400
|
-
|
|
531
|
+
async capturePendingLocalRolloutCursor(binding) {
|
|
532
|
+
if (!this.localRolloutReader) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const locator = typeof binding.codexThreadRef === "string" && binding.codexThreadRef.startsWith(THREAD_REF_PREFIX)
|
|
401
536
|
? this.decodeThreadRef(binding.codexThreadRef)
|
|
402
537
|
: null;
|
|
538
|
+
if (!locator?.title) {
|
|
539
|
+
this.pendingLocalRolloutCursors.delete(binding.sessionKey);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const cursor = this.localRolloutReader.captureCursorForThreadTitle(locator.title);
|
|
543
|
+
if (!cursor) {
|
|
544
|
+
this.pendingLocalRolloutCursors.delete(binding.sessionKey);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
this.pendingLocalRolloutCursors.set(binding.sessionKey, cursor);
|
|
548
|
+
}
|
|
549
|
+
capturePendingLocalSubmissionCursor(binding) {
|
|
550
|
+
if (!this.localSubmissionReader) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const rolloutCursor = this.pendingLocalRolloutCursors.get(binding.sessionKey);
|
|
554
|
+
if (!rolloutCursor?.threadId) {
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
557
|
+
return this.localSubmissionReader.captureCursorForThreadId(rolloutCursor.threadId);
|
|
558
|
+
}
|
|
559
|
+
attachPendingTurnId(sessionKey, turnId) {
|
|
560
|
+
if (!turnId) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const cursor = this.pendingLocalRolloutCursors.get(sessionKey);
|
|
564
|
+
if (!cursor) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
cursor.targetTurnId = turnId;
|
|
568
|
+
cursor.competingTurnStarted = false;
|
|
569
|
+
}
|
|
570
|
+
captureAdHocLocalRolloutCursor(binding) {
|
|
571
|
+
if (!this.localRolloutReader) {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
const locator = typeof binding.codexThreadRef === "string" && binding.codexThreadRef.startsWith(THREAD_REF_PREFIX)
|
|
575
|
+
? this.decodeThreadRef(binding.codexThreadRef)
|
|
576
|
+
: null;
|
|
577
|
+
if (!locator?.title) {
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
return this.localRolloutReader.captureCursorForThreadTitle(locator.title);
|
|
581
|
+
}
|
|
582
|
+
async ensureThreadSelected(binding) {
|
|
583
|
+
const targetId = await this.resolveTargetId(binding);
|
|
584
|
+
const boundThreadRef = binding.codexThreadRef;
|
|
585
|
+
if (!boundThreadRef) {
|
|
586
|
+
return targetId;
|
|
587
|
+
}
|
|
588
|
+
const locator = this.decodeThreadRef(boundThreadRef);
|
|
403
589
|
if (!locator) {
|
|
404
590
|
return targetId;
|
|
405
591
|
}
|
|
406
592
|
const threads = await this.listRecentThreads(200);
|
|
407
593
|
const currentThread = threads.find((thread) => thread.isCurrent);
|
|
408
|
-
if (currentThread?.threadRef ===
|
|
594
|
+
if (currentThread?.threadRef === boundThreadRef) {
|
|
409
595
|
return targetId;
|
|
410
596
|
}
|
|
597
|
+
const previousFingerprint = await this.readConversationViewportFingerprint(targetId);
|
|
411
598
|
const switchResult = (await this.cdp.evaluateOnPage(this.buildSelectThreadScript(locator), targetId));
|
|
412
599
|
if (!switchResult?.ok) {
|
|
413
600
|
throw new DesktopDriverError(`Codex desktop thread switch failed: ${switchResult?.reason ?? "unknown"}`, "session_not_found");
|
|
414
601
|
}
|
|
415
|
-
await this.
|
|
602
|
+
await this.waitForThreadActivation(boundThreadRef, targetId, previousFingerprint);
|
|
416
603
|
return targetId;
|
|
417
604
|
}
|
|
605
|
+
async waitForThreadActivation(threadRef, targetId, previousFingerprint) {
|
|
606
|
+
let currentThreadStablePolls = 0;
|
|
607
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
608
|
+
const currentThread = (await this.listRecentThreads(200)).find((thread) => thread.isCurrent);
|
|
609
|
+
const currentMatches = currentThread?.threadRef === threadRef;
|
|
610
|
+
if (currentMatches) {
|
|
611
|
+
currentThreadStablePolls += 1;
|
|
612
|
+
const currentFingerprint = await this.readConversationViewportFingerprint(targetId);
|
|
613
|
+
if (!this.isSameConversationViewportFingerprint(currentFingerprint, previousFingerprint)
|
|
614
|
+
|| currentThreadStablePolls >= 4) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
else {
|
|
619
|
+
currentThreadStablePolls = 0;
|
|
620
|
+
}
|
|
621
|
+
if (attempt + 1 < 20) {
|
|
622
|
+
await this.sleep(100);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
throw new DesktopDriverError("Codex desktop thread switch failed: thread_activation_timeout", "session_not_found");
|
|
626
|
+
}
|
|
627
|
+
async readConversationViewportFingerprint(targetId) {
|
|
628
|
+
const fingerprint = await this.cdp.evaluateOnPage(this.buildConversationViewportFingerprintProbeScript(), targetId);
|
|
629
|
+
if (!fingerprint ||
|
|
630
|
+
typeof fingerprint !== "object" ||
|
|
631
|
+
!("latestUnitKey" in fingerprint) ||
|
|
632
|
+
!("latestSnippet" in fingerprint) ||
|
|
633
|
+
!("unitCount" in fingerprint)) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
return {
|
|
637
|
+
latestUnitKey: typeof fingerprint.latestUnitKey === "string" && fingerprint.latestUnitKey.trim()
|
|
638
|
+
? fingerprint.latestUnitKey
|
|
639
|
+
: null,
|
|
640
|
+
latestSnippet: typeof fingerprint.latestSnippet === "string" && fingerprint.latestSnippet.trim()
|
|
641
|
+
? fingerprint.latestSnippet
|
|
642
|
+
: null,
|
|
643
|
+
unitCount: typeof fingerprint.unitCount === "number" && Number.isFinite(fingerprint.unitCount)
|
|
644
|
+
? fingerprint.unitCount
|
|
645
|
+
: 0
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
isSameConversationViewportFingerprint(left, right) {
|
|
649
|
+
if (!left && !right) {
|
|
650
|
+
return true;
|
|
651
|
+
}
|
|
652
|
+
if (!left || !right) {
|
|
653
|
+
return false;
|
|
654
|
+
}
|
|
655
|
+
return (left.latestUnitKey === right.latestUnitKey
|
|
656
|
+
&& left.latestSnippet === right.latestSnippet
|
|
657
|
+
&& left.unitCount === right.unitCount);
|
|
658
|
+
}
|
|
418
659
|
async readLatestAssistantSnapshot(targetId) {
|
|
419
660
|
const structuredReply = await this.cdp.evaluateOnPage(this.buildAssistantReplyProbeScript(), targetId);
|
|
420
661
|
if (structuredReply &&
|
|
@@ -793,6 +1034,38 @@ export class CodexDesktopDriver {
|
|
|
793
1034
|
.replace(/^function\s+isLikelyComposerSubmitButton/, "function isLikelyComposerSubmitButton");
|
|
794
1035
|
return `(() => {
|
|
795
1036
|
${submitButtonMatcher}
|
|
1037
|
+
const readConversationFingerprint = () => {
|
|
1038
|
+
const units = Array.from(document.querySelectorAll('[data-content-search-unit-key]'))
|
|
1039
|
+
.filter((node) => node instanceof HTMLElement)
|
|
1040
|
+
.map((node) => {
|
|
1041
|
+
if (!(node instanceof HTMLElement)) {
|
|
1042
|
+
return null;
|
|
1043
|
+
}
|
|
1044
|
+
const rect = node.getBoundingClientRect();
|
|
1045
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
1046
|
+
return null;
|
|
1047
|
+
}
|
|
1048
|
+
return node;
|
|
1049
|
+
})
|
|
1050
|
+
.filter((node) => node instanceof HTMLElement);
|
|
1051
|
+
const latestUnit = units.at(-1);
|
|
1052
|
+
return {
|
|
1053
|
+
latestUnitKey:
|
|
1054
|
+
latestUnit instanceof HTMLElement
|
|
1055
|
+
? latestUnit.getAttribute('data-content-search-unit-key')
|
|
1056
|
+
: null,
|
|
1057
|
+
latestSnippet:
|
|
1058
|
+
latestUnit instanceof HTMLElement
|
|
1059
|
+
? (latestUnit.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 200)
|
|
1060
|
+
: null,
|
|
1061
|
+
unitCount: units.length
|
|
1062
|
+
};
|
|
1063
|
+
};
|
|
1064
|
+
const isSameConversationFingerprint = (left, right) =>
|
|
1065
|
+
Boolean(left && right)
|
|
1066
|
+
&& left.latestUnitKey === right.latestUnitKey
|
|
1067
|
+
&& left.latestSnippet === right.latestSnippet
|
|
1068
|
+
&& left.unitCount === right.unitCount;
|
|
796
1069
|
const resolveComposer = () => {
|
|
797
1070
|
const selectors = [
|
|
798
1071
|
'[data-codex-composer="true"]',
|
|
@@ -830,6 +1103,67 @@ export class CodexDesktopDriver {
|
|
|
830
1103
|
}
|
|
831
1104
|
return node.textContent || '';
|
|
832
1105
|
};
|
|
1106
|
+
const resolveComposerSubmitButton = (allowDisabled, strictMatch) =>
|
|
1107
|
+
Array.from(document.querySelectorAll('button, [role="button"]'))
|
|
1108
|
+
.filter((candidate) => {
|
|
1109
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
const rect = candidate.getBoundingClientRect();
|
|
1113
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
1114
|
+
return false;
|
|
1115
|
+
}
|
|
1116
|
+
if (
|
|
1117
|
+
!allowDisabled
|
|
1118
|
+
&& (candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true')
|
|
1119
|
+
) {
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
const centerX = rect.x + rect.width / 2;
|
|
1123
|
+
const centerY = rect.y + rect.height / 2;
|
|
1124
|
+
const nearComposerBottomRight =
|
|
1125
|
+
centerX >= inputRect.right - 140
|
|
1126
|
+
&& centerY >= inputRect.y - 24
|
|
1127
|
+
&& centerY <= inputRect.bottom + 40;
|
|
1128
|
+
if (!nearComposerBottomRight) {
|
|
1129
|
+
return false;
|
|
1130
|
+
}
|
|
1131
|
+
if (!strictMatch) {
|
|
1132
|
+
return true;
|
|
1133
|
+
}
|
|
1134
|
+
return isLikelyComposerSubmitButton({
|
|
1135
|
+
text: candidate.textContent ?? '',
|
|
1136
|
+
aria: candidate.getAttribute('aria-label'),
|
|
1137
|
+
title: candidate.getAttribute('title'),
|
|
1138
|
+
className: candidate.className ?? ''
|
|
1139
|
+
});
|
|
1140
|
+
})
|
|
1141
|
+
.sort((left, right) => {
|
|
1142
|
+
const leftRect = left.getBoundingClientRect();
|
|
1143
|
+
const rightRect = right.getBoundingClientRect();
|
|
1144
|
+
const leftLabel = {
|
|
1145
|
+
text: left.textContent ?? '',
|
|
1146
|
+
aria: left.getAttribute('aria-label'),
|
|
1147
|
+
title: left.getAttribute('title'),
|
|
1148
|
+
className: left.className ?? ''
|
|
1149
|
+
};
|
|
1150
|
+
const rightLabel = {
|
|
1151
|
+
text: right.textContent ?? '',
|
|
1152
|
+
aria: right.getAttribute('aria-label'),
|
|
1153
|
+
title: right.getAttribute('title'),
|
|
1154
|
+
className: right.className ?? ''
|
|
1155
|
+
};
|
|
1156
|
+
const leftStrictScore = isLikelyComposerSubmitButton(leftLabel) ? 1000 : 0;
|
|
1157
|
+
const rightStrictScore = isLikelyComposerSubmitButton(rightLabel) ? 1000 : 0;
|
|
1158
|
+
const leftPrimaryScore = /\bsize-token-button-composer\b/i.test(leftLabel.className) ? 100 : 0;
|
|
1159
|
+
const rightPrimaryScore = /\bsize-token-button-composer\b/i.test(rightLabel.className) ? 100 : 0;
|
|
1160
|
+
const leftScore =
|
|
1161
|
+
leftStrictScore + leftPrimaryScore + leftRect.x - Math.abs(leftRect.y - inputRect.bottom);
|
|
1162
|
+
const rightScore =
|
|
1163
|
+
rightStrictScore + rightPrimaryScore + rightRect.x - Math.abs(rightRect.y - inputRect.bottom);
|
|
1164
|
+
return rightScore - leftScore;
|
|
1165
|
+
})
|
|
1166
|
+
.at(0) ?? null;
|
|
833
1167
|
const input = resolveComposer();
|
|
834
1168
|
if (!(input instanceof HTMLElement)) {
|
|
835
1169
|
return { ok: false, reason: 'input_not_found' };
|
|
@@ -839,47 +1173,32 @@ export class CodexDesktopDriver {
|
|
|
839
1173
|
if (!currentText) {
|
|
840
1174
|
return { ok: false, reason: 'empty_input' };
|
|
841
1175
|
}
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
return false;
|
|
846
|
-
}
|
|
847
|
-
const rect = candidate.getBoundingClientRect();
|
|
848
|
-
if (rect.width <= 0 || rect.height <= 0) {
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
if (candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true') {
|
|
852
|
-
return false;
|
|
853
|
-
}
|
|
854
|
-
return isLikelyComposerSubmitButton({
|
|
855
|
-
text: candidate.textContent ?? '',
|
|
856
|
-
aria: candidate.getAttribute('aria-label'),
|
|
857
|
-
title: candidate.getAttribute('title'),
|
|
858
|
-
className: candidate.className ?? ''
|
|
859
|
-
});
|
|
860
|
-
})
|
|
861
|
-
.sort((left, right) => {
|
|
862
|
-
const leftRect = left.getBoundingClientRect();
|
|
863
|
-
const rightRect = right.getBoundingClientRect();
|
|
864
|
-
const leftDistance = Math.abs(leftRect.y - inputRect.y) + Math.max(0, inputRect.x - leftRect.x);
|
|
865
|
-
const rightDistance = Math.abs(rightRect.y - inputRect.y) + Math.max(0, inputRect.x - rightRect.x);
|
|
866
|
-
return leftDistance - rightDistance;
|
|
867
|
-
})
|
|
868
|
-
.find((candidate) => {
|
|
869
|
-
const rect = candidate.getBoundingClientRect();
|
|
870
|
-
return rect.x >= inputRect.x - 24 && Math.abs(rect.y - inputRect.y) <= 120;
|
|
871
|
-
});
|
|
1176
|
+
const beforeConversationFingerprint = readConversationFingerprint();
|
|
1177
|
+
window.__qqCodexLastSubmitConversationFingerprint = beforeConversationFingerprint;
|
|
1178
|
+
const sendButton = resolveComposerSubmitButton(false, true);
|
|
872
1179
|
const beforeButtonHtml = sendButton instanceof HTMLElement ? sendButton.innerHTML : '';
|
|
1180
|
+
window.__qqCodexLastSubmitButtonHtml = beforeButtonHtml;
|
|
873
1181
|
const confirmSubmission = (reason) => new Promise((resolve) => {
|
|
874
1182
|
window.setTimeout(() => {
|
|
875
1183
|
const afterText = readComposerText(input).trim();
|
|
876
|
-
const
|
|
1184
|
+
const currentSendButton = resolveComposerSubmitButton(true, false);
|
|
1185
|
+
const afterButtonHtml =
|
|
1186
|
+
currentSendButton instanceof HTMLElement ? currentSendButton.innerHTML : '';
|
|
877
1187
|
const buttonChanged = beforeButtonHtml !== '' && beforeButtonHtml !== afterButtonHtml;
|
|
1188
|
+
const afterConversationFingerprint = readConversationFingerprint();
|
|
1189
|
+
const conversationAdvanced = !isSameConversationFingerprint(
|
|
1190
|
+
beforeConversationFingerprint,
|
|
1191
|
+
afterConversationFingerprint
|
|
1192
|
+
);
|
|
878
1193
|
resolve({
|
|
879
|
-
ok: afterText.length === 0 || buttonChanged,
|
|
1194
|
+
ok: afterText.length === 0 || buttonChanged || conversationAdvanced,
|
|
880
1195
|
reason: afterText.length === 0
|
|
881
1196
|
? reason
|
|
882
|
-
: (
|
|
1197
|
+
: (
|
|
1198
|
+
buttonChanged
|
|
1199
|
+
? 'entered_streaming_state'
|
|
1200
|
+
: (conversationAdvanced ? 'conversation_advanced' : 'submit_not_confirmed')
|
|
1201
|
+
)
|
|
883
1202
|
});
|
|
884
1203
|
}, 300);
|
|
885
1204
|
});
|
|
@@ -891,10 +1210,9 @@ export class CodexDesktopDriver {
|
|
|
891
1210
|
if (sendButton instanceof HTMLElement) {
|
|
892
1211
|
if (typeof sendButton.click === 'function') {
|
|
893
1212
|
sendButton.click();
|
|
894
|
-
}
|
|
895
|
-
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
|
1213
|
+
} else {
|
|
896
1214
|
sendButton.dispatchEvent(
|
|
897
|
-
new MouseEvent(
|
|
1215
|
+
new MouseEvent('click', {
|
|
898
1216
|
bubbles: true,
|
|
899
1217
|
cancelable: true,
|
|
900
1218
|
view: window
|
|
@@ -919,7 +1237,43 @@ export class CodexDesktopDriver {
|
|
|
919
1237
|
})();`;
|
|
920
1238
|
}
|
|
921
1239
|
buildComposerSubmissionStateScript() {
|
|
1240
|
+
const submitButtonMatcher = isLikelyComposerSubmitButton
|
|
1241
|
+
.toString()
|
|
1242
|
+
.replace(/^function\s+isLikelyComposerSubmitButton/, "function isLikelyComposerSubmitButton");
|
|
922
1243
|
return `(() => {
|
|
1244
|
+
${submitButtonMatcher}
|
|
1245
|
+
const readConversationFingerprint = () => {
|
|
1246
|
+
const units = Array.from(document.querySelectorAll('[data-content-search-unit-key]'))
|
|
1247
|
+
.filter((node) => node instanceof HTMLElement)
|
|
1248
|
+
.map((node) => {
|
|
1249
|
+
if (!(node instanceof HTMLElement)) {
|
|
1250
|
+
return null;
|
|
1251
|
+
}
|
|
1252
|
+
const rect = node.getBoundingClientRect();
|
|
1253
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
return node;
|
|
1257
|
+
})
|
|
1258
|
+
.filter((node) => node instanceof HTMLElement);
|
|
1259
|
+
const latestUnit = units.at(-1);
|
|
1260
|
+
return {
|
|
1261
|
+
latestUnitKey:
|
|
1262
|
+
latestUnit instanceof HTMLElement
|
|
1263
|
+
? latestUnit.getAttribute('data-content-search-unit-key')
|
|
1264
|
+
: null,
|
|
1265
|
+
latestSnippet:
|
|
1266
|
+
latestUnit instanceof HTMLElement
|
|
1267
|
+
? (latestUnit.innerText || '').replace(/\\s+/g, ' ').trim().slice(0, 200)
|
|
1268
|
+
: null,
|
|
1269
|
+
unitCount: units.length
|
|
1270
|
+
};
|
|
1271
|
+
};
|
|
1272
|
+
const isSameConversationFingerprint = (left, right) =>
|
|
1273
|
+
Boolean(left && right)
|
|
1274
|
+
&& left.latestUnitKey === right.latestUnitKey
|
|
1275
|
+
&& left.latestSnippet === right.latestSnippet
|
|
1276
|
+
&& left.unitCount === right.unitCount;
|
|
923
1277
|
const selectors = [
|
|
924
1278
|
'[data-codex-composer="true"]',
|
|
925
1279
|
'textarea',
|
|
@@ -927,9 +1281,9 @@ export class CodexDesktopDriver {
|
|
|
927
1281
|
'[contenteditable="true"]',
|
|
928
1282
|
'[role="textbox"]'
|
|
929
1283
|
];
|
|
930
|
-
const
|
|
1284
|
+
const inputCandidates = selectors
|
|
931
1285
|
.flatMap((selector) => Array.from(document.querySelectorAll(selector)))
|
|
932
|
-
.
|
|
1286
|
+
.filter((candidate) => {
|
|
933
1287
|
if (!(candidate instanceof HTMLElement)) {
|
|
934
1288
|
return false;
|
|
935
1289
|
}
|
|
@@ -939,25 +1293,112 @@ export class CodexDesktopDriver {
|
|
|
939
1293
|
const rect = candidate.getBoundingClientRect();
|
|
940
1294
|
return rect.width > 0 && rect.height > 0;
|
|
941
1295
|
});
|
|
1296
|
+
const activeElement = document.activeElement;
|
|
1297
|
+
const input =
|
|
1298
|
+
activeElement instanceof HTMLElement && inputCandidates.includes(activeElement)
|
|
1299
|
+
? activeElement
|
|
1300
|
+
: (inputCandidates
|
|
1301
|
+
.sort((left, right) => right.getBoundingClientRect().y - left.getBoundingClientRect().y)
|
|
1302
|
+
.at(0) ?? null);
|
|
942
1303
|
if (!(input instanceof HTMLElement)) {
|
|
943
1304
|
return { submitted: false, reason: 'input_not_found' };
|
|
944
1305
|
}
|
|
1306
|
+
const inputRect = input.getBoundingClientRect();
|
|
1307
|
+
const resolveComposerSubmitButton = (allowDisabled) =>
|
|
1308
|
+
Array.from(document.querySelectorAll('button, [role="button"]'))
|
|
1309
|
+
.filter((candidate) => {
|
|
1310
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
const rect = candidate.getBoundingClientRect();
|
|
1314
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
1315
|
+
return false;
|
|
1316
|
+
}
|
|
1317
|
+
if (
|
|
1318
|
+
!allowDisabled
|
|
1319
|
+
&& (candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true')
|
|
1320
|
+
) {
|
|
1321
|
+
return false;
|
|
1322
|
+
}
|
|
1323
|
+
const centerX = rect.x + rect.width / 2;
|
|
1324
|
+
const centerY = rect.y + rect.height / 2;
|
|
1325
|
+
return (
|
|
1326
|
+
centerX >= inputRect.right - 140
|
|
1327
|
+
&& centerY >= inputRect.y - 24
|
|
1328
|
+
&& centerY <= inputRect.bottom + 40
|
|
1329
|
+
);
|
|
1330
|
+
})
|
|
1331
|
+
.sort((left, right) => {
|
|
1332
|
+
const leftRect = left.getBoundingClientRect();
|
|
1333
|
+
const rightRect = right.getBoundingClientRect();
|
|
1334
|
+
const leftLabel = {
|
|
1335
|
+
text: left.textContent ?? '',
|
|
1336
|
+
aria: left.getAttribute('aria-label'),
|
|
1337
|
+
title: left.getAttribute('title'),
|
|
1338
|
+
className: left.className ?? ''
|
|
1339
|
+
};
|
|
1340
|
+
const rightLabel = {
|
|
1341
|
+
text: right.textContent ?? '',
|
|
1342
|
+
aria: right.getAttribute('aria-label'),
|
|
1343
|
+
title: right.getAttribute('title'),
|
|
1344
|
+
className: right.className ?? ''
|
|
1345
|
+
};
|
|
1346
|
+
const leftStrictScore = isLikelyComposerSubmitButton(leftLabel) ? 1000 : 0;
|
|
1347
|
+
const rightStrictScore = isLikelyComposerSubmitButton(rightLabel) ? 1000 : 0;
|
|
1348
|
+
const leftPrimaryScore = /\bsize-token-button-composer\b/i.test(leftLabel.className) ? 100 : 0;
|
|
1349
|
+
const rightPrimaryScore = /\bsize-token-button-composer\b/i.test(rightLabel.className) ? 100 : 0;
|
|
1350
|
+
const leftScore =
|
|
1351
|
+
leftStrictScore + leftPrimaryScore + leftRect.x - Math.abs(leftRect.y - inputRect.bottom);
|
|
1352
|
+
const rightScore =
|
|
1353
|
+
rightStrictScore + rightPrimaryScore + rightRect.x - Math.abs(rightRect.y - inputRect.bottom);
|
|
1354
|
+
return rightScore - leftScore;
|
|
1355
|
+
})
|
|
1356
|
+
.at(0) ?? null;
|
|
945
1357
|
const currentText =
|
|
946
1358
|
'value' in input && typeof input.value === 'string'
|
|
947
1359
|
? input.value.trim()
|
|
948
1360
|
: (input.textContent || '').trim();
|
|
949
|
-
const sendButton =
|
|
950
|
-
if (!(candidate instanceof HTMLElement)) {
|
|
951
|
-
return false;
|
|
952
|
-
}
|
|
953
|
-
const className = typeof candidate.className === 'string' ? candidate.className : '';
|
|
954
|
-
return className.includes('size-token-button-composer') && className.includes('bg-token-foreground');
|
|
955
|
-
});
|
|
1361
|
+
const sendButton = resolveComposerSubmitButton(true);
|
|
956
1362
|
const buttonHtml = sendButton instanceof HTMLElement ? sendButton.innerHTML : '';
|
|
957
|
-
const
|
|
1363
|
+
const buttonClassName = sendButton instanceof HTMLElement ? String(sendButton.className || '') : '';
|
|
1364
|
+
const baselineButtonHtml =
|
|
1365
|
+
typeof window.__qqCodexLastSubmitButtonHtml === 'string'
|
|
1366
|
+
? window.__qqCodexLastSubmitButtonHtml
|
|
1367
|
+
: '';
|
|
1368
|
+
const isStreamingButton = baselineButtonHtml !== '' && buttonHtml !== '' && baselineButtonHtml !== buttonHtml;
|
|
1369
|
+
const baselineConversationFingerprint = window.__qqCodexLastSubmitConversationFingerprint;
|
|
1370
|
+
const currentConversationFingerprint = readConversationFingerprint();
|
|
1371
|
+
const conversationAdvanced =
|
|
1372
|
+
baselineConversationFingerprint
|
|
1373
|
+
&& !isSameConversationFingerprint(
|
|
1374
|
+
baselineConversationFingerprint,
|
|
1375
|
+
currentConversationFingerprint
|
|
1376
|
+
);
|
|
958
1377
|
return {
|
|
959
|
-
submitted: currentText.length === 0 || isStreamingButton,
|
|
960
|
-
reason: currentText.length === 0
|
|
1378
|
+
submitted: currentText.length === 0 || isStreamingButton || conversationAdvanced,
|
|
1379
|
+
reason: currentText.length === 0
|
|
1380
|
+
? 'composer_cleared'
|
|
1381
|
+
: (
|
|
1382
|
+
isStreamingButton
|
|
1383
|
+
? 'entered_streaming_state'
|
|
1384
|
+
: (conversationAdvanced ? 'conversation_advanced' : 'submit_not_confirmed')
|
|
1385
|
+
),
|
|
1386
|
+
diagnostics: {
|
|
1387
|
+
currentTextLength: currentText.length,
|
|
1388
|
+
inputRect: {
|
|
1389
|
+
x: inputRect.x,
|
|
1390
|
+
y: inputRect.y,
|
|
1391
|
+
width: inputRect.width,
|
|
1392
|
+
height: inputRect.height,
|
|
1393
|
+
right: inputRect.right,
|
|
1394
|
+
bottom: inputRect.bottom
|
|
1395
|
+
},
|
|
1396
|
+
baselineButtonHtml: baselineButtonHtml.slice(0, 160),
|
|
1397
|
+
buttonHtml: buttonHtml.slice(0, 160),
|
|
1398
|
+
buttonClassName,
|
|
1399
|
+
sendButtonFound: Boolean(sendButton),
|
|
1400
|
+
conversationAdvanced
|
|
1401
|
+
}
|
|
961
1402
|
};
|
|
962
1403
|
})();`;
|
|
963
1404
|
}
|
|
@@ -965,73 +1406,171 @@ export class CodexDesktopDriver {
|
|
|
965
1406
|
return `
|
|
966
1407
|
(() => {
|
|
967
1408
|
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
968
|
-
const buttons = Array.from(document.querySelectorAll('button,[role="button"],[aria-label]'))
|
|
969
|
-
.map((node) => {
|
|
970
|
-
const rect = node instanceof HTMLElement ? node.getBoundingClientRect() : null;
|
|
971
|
-
return {
|
|
972
|
-
text: normalize(node.textContent || ''),
|
|
973
|
-
aria: normalize(node.getAttribute('aria-label') || ''),
|
|
974
|
-
title: normalize(node.getAttribute('title') || ''),
|
|
975
|
-
className: String(node.className || ''),
|
|
976
|
-
x: rect ? rect.x : 0,
|
|
977
|
-
y: rect ? rect.y : 0,
|
|
978
|
-
width: rect ? rect.width : 0,
|
|
979
|
-
height: rect ? rect.height : 0
|
|
980
|
-
};
|
|
981
|
-
})
|
|
982
|
-
.filter((item) => (item.text || item.aria || item.title) && item.y >= window.innerHeight - 180 && item.x >= 320)
|
|
983
|
-
.sort((left, right) => (left.y - right.y) || (left.x - right.x));
|
|
984
|
-
|
|
985
1409
|
const modelPattern = /(?:gpt|o1|o3|o4|4\\.1|4\\.5|5\\.4|mini|nano|sonnet|haiku|opus|gemini|claude|qwen|deepseek)/i;
|
|
986
1410
|
const effortPattern = /^(?:低|中|高|minimal|low|medium|high)$/i;
|
|
987
1411
|
const permissionPattern = /(?:访问权限|permission|sandbox)/i;
|
|
988
|
-
const
|
|
989
|
-
const
|
|
1412
|
+
const workspacePattern = /^(?:本地工作|在本地处理|本地项目|本地|云端|local|cloud|worktree)$/i;
|
|
1413
|
+
const branchPattern = /^(?:[A-Za-z0-9._-]+\\/[A-Za-z0-9._/-]+|main|master|develop|development|dev|staging|production|release\\/[A-Za-z0-9._/-]+|hotfix\\/[A-Za-z0-9._/-]+|feature\\/[A-Za-z0-9._/-]+|bugfix\\/[A-Za-z0-9._/-]+)$/;
|
|
1414
|
+
const ignoredLinePattern = /(?:QQBOT_RUNTIME_CONTEXT|<qqmedia>|<!--|-->|会话类型|runtime\\/media|内部实现|相对路径)/i;
|
|
1415
|
+
const ignoredBranchPattern = /^(?:https?:\\/\\/|app:\\/\\/|\\/|继续使用|在本地处理|本地工作|本地项目|升级至\\s*Pro|了解更多|移至工作树|剩余额度|remaining usage|quota|usage|额度|配额|GPT-|Claude|Gemini|完全访问权限|听写)$/i;
|
|
1416
|
+
const isVisible = (node) => {
|
|
1417
|
+
if (!(node instanceof HTMLElement)) {
|
|
1418
|
+
return false;
|
|
1419
|
+
}
|
|
1420
|
+
const rect = node.getBoundingClientRect();
|
|
1421
|
+
return rect.width > 0 && rect.height > 0;
|
|
1422
|
+
};
|
|
1423
|
+
const clickNode = (node) => {
|
|
1424
|
+
node.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
|
|
1425
|
+
node.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
|
|
1426
|
+
node.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
|
|
1427
|
+
node.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
1428
|
+
};
|
|
1429
|
+
const collectControls = () =>
|
|
1430
|
+
Array.from(document.querySelectorAll('button,[role="button"],[role="menuitem"],[role="option"],[aria-label]'))
|
|
1431
|
+
.filter(isVisible)
|
|
1432
|
+
.map((node) => {
|
|
1433
|
+
const rect = node instanceof HTMLElement ? node.getBoundingClientRect() : null;
|
|
1434
|
+
return {
|
|
1435
|
+
node,
|
|
1436
|
+
text: normalize(node.textContent || ''),
|
|
1437
|
+
aria: normalize(node.getAttribute('aria-label') || ''),
|
|
1438
|
+
title: normalize(node.getAttribute('title') || ''),
|
|
1439
|
+
className: String(node.className || ''),
|
|
1440
|
+
x: rect ? rect.x : 0,
|
|
1441
|
+
y: rect ? rect.y : 0,
|
|
1442
|
+
width: rect ? rect.width : 0,
|
|
1443
|
+
height: rect ? rect.height : 0
|
|
1444
|
+
};
|
|
1445
|
+
});
|
|
1446
|
+
const collectFooterControls = () =>
|
|
1447
|
+
collectControls()
|
|
1448
|
+
.filter((item) => {
|
|
1449
|
+
const text = item.text || item.aria || item.title;
|
|
1450
|
+
if (!text) {
|
|
1451
|
+
return false;
|
|
1452
|
+
}
|
|
990
1453
|
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
let branch = null;
|
|
995
|
-
let permissionMode = null;
|
|
1454
|
+
if (item.y < window.innerHeight - 120 || item.y > window.innerHeight || item.x < 380) {
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
996
1457
|
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1458
|
+
return /(?:h-token-button-composer|size-token-button-composer)/.test(item.className);
|
|
1459
|
+
})
|
|
1460
|
+
.sort((left, right) => (left.y - right.y) || (left.x - right.x));
|
|
1461
|
+
const getBodyLines = () =>
|
|
1462
|
+
(document.body ? document.body.innerText : '')
|
|
1463
|
+
.split('\\n')
|
|
1464
|
+
.map((line) => normalize(line))
|
|
1465
|
+
.filter((line) => line && !ignoredLinePattern.test(line));
|
|
1466
|
+
const isQuotaHeader = (line) => line === '剩余额度' || /^remaining usage$/i.test(line);
|
|
1467
|
+
const parseQuotaEntries = (lines) => {
|
|
1468
|
+
const entries = [];
|
|
1469
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1470
|
+
const line = lines[index];
|
|
1471
|
+
if (
|
|
1472
|
+
!line ||
|
|
1473
|
+
isQuotaHeader(line) ||
|
|
1474
|
+
/^\\d+%$/.test(line) ||
|
|
1475
|
+
/^(?:继续使用|本地项目|本地工作|在本地处理|云端|升级至\\s*Pro|了解更多|移至工作树)$/i.test(line)
|
|
1476
|
+
) {
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
const combinedMatch = line.match(/^(.+?(?:分钟|小时|天|周|月|minutes?|hours?|days?|weeks?|months?))\\s+(\\d+%)\\s+(.+)$/i);
|
|
1481
|
+
if (combinedMatch) {
|
|
1482
|
+
entries.push(\`\${combinedMatch[1]} \${combinedMatch[2]}(\${combinedMatch[3]} 重置)\`);
|
|
1483
|
+
continue;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
const timeframeMatch = line.match(/^(.+?(?:分钟|小时|天|周|月|minutes?|hours?|days?|weeks?|months?))$/i);
|
|
1487
|
+
if (timeframeMatch && index + 2 < lines.length) {
|
|
1488
|
+
const percentLine = lines[index + 1];
|
|
1489
|
+
const resetLine = lines[index + 2];
|
|
1490
|
+
if (/^\\d+%$/.test(percentLine) && resetLine) {
|
|
1491
|
+
entries.push(\`\${timeframeMatch[1]} \${percentLine}(\${resetLine} 重置)\`);
|
|
1492
|
+
index += 2;
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1010
1495
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1496
|
+
|
|
1497
|
+
return entries;
|
|
1498
|
+
};
|
|
1499
|
+
const parseBranch = (lines) =>
|
|
1500
|
+
lines.find((line) => branchPattern.test(line) && !ignoredBranchPattern.test(line)) || null;
|
|
1501
|
+
const findWorkspaceButton = (controls) =>
|
|
1502
|
+
controls.find((item) => workspacePattern.test(item.text || item.aria || item.title));
|
|
1503
|
+
const findFooterBranch = (controls) =>
|
|
1504
|
+
controls.find((item) => {
|
|
1505
|
+
const text = item.text || item.aria || item.title;
|
|
1506
|
+
return branchPattern.test(text) && !ignoredBranchPattern.test(text);
|
|
1507
|
+
});
|
|
1508
|
+
const readState = () => {
|
|
1509
|
+
const footerControls = collectFooterControls();
|
|
1510
|
+
const workspaceButton = findWorkspaceButton(footerControls);
|
|
1511
|
+
const footerBranch = findFooterBranch(footerControls);
|
|
1512
|
+
const lines = getBodyLines();
|
|
1513
|
+
const quotaEntries = parseQuotaEntries(lines);
|
|
1514
|
+
|
|
1515
|
+
let model = null;
|
|
1516
|
+
let reasoningEffort = null;
|
|
1517
|
+
let workspace = null;
|
|
1518
|
+
let permissionMode = null;
|
|
1519
|
+
|
|
1520
|
+
for (const item of footerControls) {
|
|
1521
|
+
const text = item.text || item.aria || item.title;
|
|
1522
|
+
if (!model && modelPattern.test(text)) {
|
|
1523
|
+
model = text;
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
if (!reasoningEffort && effortPattern.test(text)) {
|
|
1527
|
+
reasoningEffort = text;
|
|
1528
|
+
continue;
|
|
1529
|
+
}
|
|
1530
|
+
if (!permissionMode && permissionPattern.test(text)) {
|
|
1531
|
+
permissionMode = text;
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
if (!workspace && workspacePattern.test(text)) {
|
|
1535
|
+
workspace = text;
|
|
1536
|
+
}
|
|
1014
1537
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1538
|
+
|
|
1539
|
+
return {
|
|
1540
|
+
workspaceButton,
|
|
1541
|
+
state: {
|
|
1542
|
+
model,
|
|
1543
|
+
reasoningEffort,
|
|
1544
|
+
workspace,
|
|
1545
|
+
branch: footerBranch ? (footerBranch.text || footerBranch.aria || footerBranch.title) : parseBranch(lines),
|
|
1546
|
+
permissionMode,
|
|
1547
|
+
quotaSummary: quotaEntries.length > 0 ? quotaEntries.join('\\n') : null
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
return new Promise((resolve) => {
|
|
1553
|
+
const initial = readState();
|
|
1554
|
+
if (!initial.workspaceButton) {
|
|
1555
|
+
resolve(initial.state);
|
|
1556
|
+
return;
|
|
1017
1557
|
}
|
|
1018
|
-
}
|
|
1019
1558
|
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1559
|
+
clickNode(initial.workspaceButton.node);
|
|
1560
|
+
setTimeout(() => {
|
|
1561
|
+
const opened = readState();
|
|
1562
|
+
const quotaToggle = collectControls().find((item) => /剩余额度|remaining usage/i.test(item.text || item.aria || item.title));
|
|
1563
|
+
if (!(quotaToggle && quotaToggle.node instanceof HTMLElement)) {
|
|
1564
|
+
resolve(opened.state);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1026
1567
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
quotaSummary: quotaLine
|
|
1034
|
-
};
|
|
1568
|
+
clickNode(quotaToggle.node);
|
|
1569
|
+
setTimeout(() => {
|
|
1570
|
+
resolve(readState().state);
|
|
1571
|
+
}, 120);
|
|
1572
|
+
}, 120);
|
|
1573
|
+
});
|
|
1035
1574
|
})()
|
|
1036
1575
|
`;
|
|
1037
1576
|
}
|
|
@@ -1098,8 +1637,8 @@ export class CodexDesktopDriver {
|
|
|
1098
1637
|
return (
|
|
1099
1638
|
rect.y >= window.innerHeight - 120 &&
|
|
1100
1639
|
rect.height <= 40 &&
|
|
1101
|
-
rect.width <=
|
|
1102
|
-
/^(
|
|
1640
|
+
rect.width <= 140 &&
|
|
1641
|
+
/^(?:本地工作|在本地处理|本地项目|本地|云端|local|cloud|worktree)(?:\\d+%)?$/i.test(text)
|
|
1103
1642
|
);
|
|
1104
1643
|
});
|
|
1105
1644
|
|
|
@@ -1178,7 +1717,7 @@ export class CodexDesktopDriver {
|
|
|
1178
1717
|
}
|
|
1179
1718
|
const rect = node.getBoundingClientRect();
|
|
1180
1719
|
const text = (node.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
1181
|
-
return rect.y >= window.innerHeight -
|
|
1720
|
+
return rect.y >= window.innerHeight - 200 && matchesModelText(text);
|
|
1182
1721
|
});
|
|
1183
1722
|
const clickNode = (node) => {
|
|
1184
1723
|
node.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
|
|
@@ -1226,10 +1765,48 @@ export class CodexDesktopDriver {
|
|
|
1226
1765
|
}
|
|
1227
1766
|
buildAssistantReplyProbeScript() {
|
|
1228
1767
|
return `(() => {
|
|
1229
|
-
const
|
|
1768
|
+
const allAssistantUnits = Array.from(
|
|
1230
1769
|
document.querySelectorAll('[data-content-search-unit-key$=":assistant"]')
|
|
1231
1770
|
);
|
|
1232
|
-
const
|
|
1771
|
+
const composer = document.querySelector(
|
|
1772
|
+
'[data-codex-composer="true"], textarea, input[type="text"], [contenteditable="true"], [role="textbox"]'
|
|
1773
|
+
);
|
|
1774
|
+
const composerRect = composer instanceof HTMLElement
|
|
1775
|
+
? composer.getBoundingClientRect()
|
|
1776
|
+
: null;
|
|
1777
|
+
const visibleAssistantUnits = allAssistantUnits
|
|
1778
|
+
.filter((node) => node instanceof HTMLElement)
|
|
1779
|
+
.filter((node) => {
|
|
1780
|
+
const rect = node.getBoundingClientRect();
|
|
1781
|
+
return (
|
|
1782
|
+
rect.width > 0 &&
|
|
1783
|
+
rect.height > 0 &&
|
|
1784
|
+
rect.bottom >= -120 &&
|
|
1785
|
+
rect.top <= window.innerHeight + 480
|
|
1786
|
+
);
|
|
1787
|
+
});
|
|
1788
|
+
const viewportAnchoredAssistantUnits = visibleAssistantUnits.filter((node) => {
|
|
1789
|
+
if (!(node instanceof HTMLElement) || !composerRect) {
|
|
1790
|
+
return true;
|
|
1791
|
+
}
|
|
1792
|
+
const rect = node.getBoundingClientRect();
|
|
1793
|
+
return (
|
|
1794
|
+
rect.bottom >= composerRect.top - window.innerHeight * 0.75 &&
|
|
1795
|
+
rect.top <= composerRect.bottom + window.innerHeight
|
|
1796
|
+
);
|
|
1797
|
+
});
|
|
1798
|
+
const candidateAssistantUnits =
|
|
1799
|
+
viewportAnchoredAssistantUnits.length > 0
|
|
1800
|
+
? viewportAnchoredAssistantUnits
|
|
1801
|
+
: (visibleAssistantUnits.length > 0 ? visibleAssistantUnits : allAssistantUnits);
|
|
1802
|
+
const latestAssistantUnit = candidateAssistantUnits
|
|
1803
|
+
.filter((node) => node instanceof HTMLElement)
|
|
1804
|
+
.sort((left, right) => {
|
|
1805
|
+
const leftRect = left.getBoundingClientRect();
|
|
1806
|
+
const rightRect = right.getBoundingClientRect();
|
|
1807
|
+
return rightRect.bottom - leftRect.bottom;
|
|
1808
|
+
})
|
|
1809
|
+
.at(0);
|
|
1233
1810
|
if (!(latestAssistantUnit instanceof HTMLElement)) {
|
|
1234
1811
|
return null;
|
|
1235
1812
|
}
|
|
@@ -1432,12 +2009,6 @@ export class CodexDesktopDriver {
|
|
|
1432
2009
|
return null;
|
|
1433
2010
|
})
|
|
1434
2011
|
.filter((value, index, values) => typeof value === 'string' && values.indexOf(value) === index);
|
|
1435
|
-
const composer = document.querySelector(
|
|
1436
|
-
'[data-codex-composer="true"], textarea, input[type="text"], [contenteditable="true"], [role="textbox"]'
|
|
1437
|
-
);
|
|
1438
|
-
const composerRect = composer instanceof HTMLElement
|
|
1439
|
-
? composer.getBoundingClientRect()
|
|
1440
|
-
: null;
|
|
1441
2012
|
const streamingMatcher = /(\\bstop\\b|\\bthinking\\b|\\bworking\\b|\\brunning\\b|停止|中止|取消|思考中|生成中)/i;
|
|
1442
2013
|
const assistantStatusMatcher = /(Reconnecting\\.{3}|Searching\\.{3}|Running\\.{3}|Working\\.{3}|连接中\\.{0,3}|重新连接中\\.{0,3}|搜索中\\.{0,3}|执行中\\.{0,3}|处理中\\.{0,3})/i;
|
|
1443
2014
|
const isComposerBusyButton = (node) => {
|
|
@@ -1521,6 +2092,39 @@ export class CodexDesktopDriver {
|
|
|
1521
2092
|
: null;
|
|
1522
2093
|
})();`;
|
|
1523
2094
|
}
|
|
2095
|
+
buildConversationViewportFingerprintProbeScript() {
|
|
2096
|
+
return `(() => {
|
|
2097
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
2098
|
+
const units = Array.from(document.querySelectorAll('[data-content-search-unit-key]'))
|
|
2099
|
+
.filter((node) => node instanceof HTMLElement)
|
|
2100
|
+
.map((node) => {
|
|
2101
|
+
if (!(node instanceof HTMLElement)) {
|
|
2102
|
+
return null;
|
|
2103
|
+
}
|
|
2104
|
+
const rect = node.getBoundingClientRect();
|
|
2105
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
2106
|
+
return null;
|
|
2107
|
+
}
|
|
2108
|
+
return node;
|
|
2109
|
+
})
|
|
2110
|
+
.filter((node) => node instanceof HTMLElement);
|
|
2111
|
+
const latestUnit = units.at(-1);
|
|
2112
|
+
if (!(latestUnit instanceof HTMLElement)) {
|
|
2113
|
+
return {
|
|
2114
|
+
latestUnitKey: null,
|
|
2115
|
+
latestSnippet: null,
|
|
2116
|
+
unitCount: 0
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
const snippet = normalize(latestUnit.innerText)
|
|
2120
|
+
.slice(0, 200);
|
|
2121
|
+
return {
|
|
2122
|
+
latestUnitKey: latestUnit.getAttribute('data-content-search-unit-key'),
|
|
2123
|
+
latestSnippet: snippet || null,
|
|
2124
|
+
unitCount: units.length
|
|
2125
|
+
};
|
|
2126
|
+
})();`;
|
|
2127
|
+
}
|
|
1524
2128
|
}
|
|
1525
2129
|
function buildMediaArtifactFromReference(reference) {
|
|
1526
2130
|
const normalizedReference = reference.trim();
|