qq-codex-bridge 0.1.0
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 +58 -0
- package/LICENSE +21 -0
- package/README.md +453 -0
- package/bin/qq-codex-bridge.js +11 -0
- package/dist/apps/bridge-daemon/src/bootstrap.js +100 -0
- package/dist/apps/bridge-daemon/src/cli.js +141 -0
- package/dist/apps/bridge-daemon/src/config.js +109 -0
- package/dist/apps/bridge-daemon/src/debug-codex-workers.js +309 -0
- package/dist/apps/bridge-daemon/src/dev-launch.js +73 -0
- package/dist/apps/bridge-daemon/src/dev.js +28 -0
- package/dist/apps/bridge-daemon/src/http-server.js +36 -0
- package/dist/apps/bridge-daemon/src/main.js +57 -0
- package/dist/apps/bridge-daemon/src/thread-command-handler.js +197 -0
- package/dist/packages/adapters/codex-desktop/src/cdp-session.js +189 -0
- package/dist/packages/adapters/codex-desktop/src/codex-desktop-driver.js +1259 -0
- package/dist/packages/adapters/codex-desktop/src/composer-heuristics.js +11 -0
- package/dist/packages/adapters/codex-desktop/src/health.js +7 -0
- package/dist/packages/adapters/codex-desktop/src/reply-parser.js +10 -0
- package/dist/packages/adapters/qq/src/qq-api-client.js +232 -0
- package/dist/packages/adapters/qq/src/qq-channel-adapter.js +22 -0
- package/dist/packages/adapters/qq/src/qq-gateway-client.js +295 -0
- package/dist/packages/adapters/qq/src/qq-gateway-session-store.js +64 -0
- package/dist/packages/adapters/qq/src/qq-gateway.js +62 -0
- package/dist/packages/adapters/qq/src/qq-media-downloader.js +246 -0
- package/dist/packages/adapters/qq/src/qq-media-parser.js +144 -0
- package/dist/packages/adapters/qq/src/qq-normalizer.js +35 -0
- package/dist/packages/adapters/qq/src/qq-sender.js +241 -0
- package/dist/packages/adapters/qq/src/qq-stt.js +189 -0
- package/dist/packages/domain/src/driver.js +7 -0
- package/dist/packages/domain/src/message.js +7 -0
- package/dist/packages/domain/src/session.js +7 -0
- package/dist/packages/orchestrator/src/bridge-orchestrator.js +143 -0
- package/dist/packages/orchestrator/src/job-runner.js +5 -0
- package/dist/packages/orchestrator/src/media-context.js +90 -0
- package/dist/packages/orchestrator/src/qq-outbound-draft.js +38 -0
- package/dist/packages/orchestrator/src/qq-outbound-format.js +51 -0
- package/dist/packages/orchestrator/src/qqbot-skill-context.js +13 -0
- package/dist/packages/orchestrator/src/session-key.js +6 -0
- package/dist/packages/ports/src/conversation.js +1 -0
- package/dist/packages/ports/src/qq.js +1 -0
- package/dist/packages/ports/src/store.js +1 -0
- package/dist/packages/store/src/message-repo.js +53 -0
- package/dist/packages/store/src/session-repo.js +80 -0
- package/dist/packages/store/src/sqlite.js +64 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1259 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { DesktopDriverError } from "../../../domain/src/driver.js";
|
|
3
|
+
import { MediaArtifactKind } from "../../../domain/src/message.js";
|
|
4
|
+
import { isLikelyComposerSubmitButton } from "./composer-heuristics.js";
|
|
5
|
+
import { parseAssistantReply } from "./reply-parser.js";
|
|
6
|
+
const TARGET_REF_PREFIX = "cdp-target:";
|
|
7
|
+
const THREAD_REF_PREFIX = "codex-thread:";
|
|
8
|
+
export class CodexDesktopDriver {
|
|
9
|
+
cdp;
|
|
10
|
+
replyPollAttempts;
|
|
11
|
+
maxReplyPollAttempts;
|
|
12
|
+
replyPollIntervalMs;
|
|
13
|
+
replyStablePolls;
|
|
14
|
+
partialReplyStablePolls;
|
|
15
|
+
sleep;
|
|
16
|
+
pendingReplyBaselines = new Map();
|
|
17
|
+
constructor(cdp, options = {}) {
|
|
18
|
+
this.cdp = cdp;
|
|
19
|
+
this.replyPollAttempts = Math.max(1, options.replyPollAttempts ?? 60);
|
|
20
|
+
this.maxReplyPollAttempts = Math.max(this.replyPollAttempts, options.maxReplyPollAttempts ?? this.replyPollAttempts * 10);
|
|
21
|
+
this.replyPollIntervalMs = options.replyPollIntervalMs ?? 500;
|
|
22
|
+
this.replyStablePolls = Math.max(1, options.replyStablePolls ?? 3);
|
|
23
|
+
this.partialReplyStablePolls = Math.max(1, options.partialReplyStablePolls ?? 2);
|
|
24
|
+
this.sleep =
|
|
25
|
+
options.sleep ??
|
|
26
|
+
((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
27
|
+
}
|
|
28
|
+
async ensureAppReady() {
|
|
29
|
+
await this.cdp.connect();
|
|
30
|
+
const targets = await this.cdp.listTargets();
|
|
31
|
+
const hasPageTarget = targets.some((target) => target.type === "page");
|
|
32
|
+
if (!hasPageTarget) {
|
|
33
|
+
throw new DesktopDriverError("Codex desktop app is not exposing any inspectable page target", "app_not_ready");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async openOrBindSession(sessionKey, binding) {
|
|
37
|
+
const pageTarget = await this.resolvePageTarget();
|
|
38
|
+
const pageId = pageTarget.id;
|
|
39
|
+
if (binding?.codexThreadRef === `${TARGET_REF_PREFIX}${pageId}`) {
|
|
40
|
+
return binding;
|
|
41
|
+
}
|
|
42
|
+
if (binding?.codexThreadRef?.startsWith(THREAD_REF_PREFIX)) {
|
|
43
|
+
const locator = this.decodeThreadRef(binding.codexThreadRef);
|
|
44
|
+
if (locator && locator.pageId === pageId) {
|
|
45
|
+
const threads = await this.listRecentThreads(200);
|
|
46
|
+
const matched = threads.find((thread) => thread.threadRef === binding.codexThreadRef);
|
|
47
|
+
if (matched) {
|
|
48
|
+
return binding;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const currentThread = (await this.listRecentThreads(200)).find((thread) => thread.isCurrent);
|
|
53
|
+
if (currentThread) {
|
|
54
|
+
return {
|
|
55
|
+
sessionKey,
|
|
56
|
+
codexThreadRef: currentThread.threadRef
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
sessionKey,
|
|
61
|
+
codexThreadRef: `${TARGET_REF_PREFIX}${pageId}`
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async listRecentThreads(limit) {
|
|
65
|
+
const pageTarget = await this.resolvePageTarget();
|
|
66
|
+
const rawThreads = (await this.cdp.evaluateOnPage(this.buildThreadListScript(), pageTarget.id));
|
|
67
|
+
if (!Array.isArray(rawThreads)) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
return rawThreads
|
|
71
|
+
.sort((left, right) => this.compareThreadActivity(left, right))
|
|
72
|
+
.slice(0, limit)
|
|
73
|
+
.map((thread, index) => ({
|
|
74
|
+
index: index + 1,
|
|
75
|
+
title: thread.title,
|
|
76
|
+
projectName: thread.projectName,
|
|
77
|
+
relativeTime: thread.relativeTime,
|
|
78
|
+
isCurrent: thread.isCurrent,
|
|
79
|
+
threadRef: this.encodeThreadRef({
|
|
80
|
+
pageId: pageTarget.id,
|
|
81
|
+
title: thread.title,
|
|
82
|
+
projectName: thread.projectName
|
|
83
|
+
})
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
async switchToThread(sessionKey, threadRef) {
|
|
87
|
+
const locator = this.decodeThreadRef(threadRef);
|
|
88
|
+
if (!locator) {
|
|
89
|
+
throw new DesktopDriverError("Codex thread binding is invalid", "session_not_found");
|
|
90
|
+
}
|
|
91
|
+
const result = (await this.cdp.evaluateOnPage(this.buildSelectThreadScript(locator), locator.pageId));
|
|
92
|
+
if (!result?.ok) {
|
|
93
|
+
throw new DesktopDriverError(`Codex desktop thread switch failed: ${result?.reason ?? "unknown"}`, "session_not_found");
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
sessionKey,
|
|
97
|
+
codexThreadRef: threadRef
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
async createThread(sessionKey, seedPrompt) {
|
|
101
|
+
const pageTarget = await this.resolvePageTarget();
|
|
102
|
+
const clickResult = (await this.cdp.evaluateOnPage(this.buildNewThreadScript(), pageTarget.id));
|
|
103
|
+
if (!clickResult?.ok) {
|
|
104
|
+
throw new DesktopDriverError(`Codex desktop new thread failed: ${clickResult?.reason ?? "unknown"}`, "session_not_found");
|
|
105
|
+
}
|
|
106
|
+
await this.waitForFreshThreadContext(pageTarget.id);
|
|
107
|
+
const temporaryBinding = {
|
|
108
|
+
sessionKey,
|
|
109
|
+
codexThreadRef: `${TARGET_REF_PREFIX}${pageTarget.id}`
|
|
110
|
+
};
|
|
111
|
+
if (seedPrompt.trim()) {
|
|
112
|
+
await this.sendUserMessage(temporaryBinding, {
|
|
113
|
+
messageId: `thread-seed:${randomUUID()}`,
|
|
114
|
+
accountKey: "qqbot:default",
|
|
115
|
+
sessionKey,
|
|
116
|
+
peerKey: "qq:c2c:thread-control",
|
|
117
|
+
chatType: "c2c",
|
|
118
|
+
senderId: "thread-control",
|
|
119
|
+
text: seedPrompt,
|
|
120
|
+
receivedAt: new Date().toISOString()
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return temporaryBinding;
|
|
124
|
+
}
|
|
125
|
+
async sendUserMessage(binding, message) {
|
|
126
|
+
const targetId = await this.ensureThreadSelected(binding);
|
|
127
|
+
const baselineReply = await this.readLatestAssistantSnapshot(targetId);
|
|
128
|
+
this.pendingReplyBaselines.set(binding.sessionKey, baselineReply);
|
|
129
|
+
const focusResult = (await this.cdp.evaluateOnPage(this.buildFocusComposerScript(), targetId));
|
|
130
|
+
if (!focusResult?.ok) {
|
|
131
|
+
this.pendingReplyBaselines.delete(binding.sessionKey);
|
|
132
|
+
throw new DesktopDriverError(`Codex desktop input box not found: ${focusResult?.reason ?? "unknown"}`, "input_not_found");
|
|
133
|
+
}
|
|
134
|
+
await this.cdp.dispatchKeyEvent({
|
|
135
|
+
type: "keyDown",
|
|
136
|
+
commands: ["selectAll"]
|
|
137
|
+
}, targetId);
|
|
138
|
+
await this.cdp.dispatchKeyEvent({
|
|
139
|
+
type: "keyDown",
|
|
140
|
+
key: "Backspace",
|
|
141
|
+
code: "Backspace",
|
|
142
|
+
windowsVirtualKeyCode: 8,
|
|
143
|
+
nativeVirtualKeyCode: 8
|
|
144
|
+
}, targetId);
|
|
145
|
+
await this.cdp.dispatchKeyEvent({
|
|
146
|
+
type: "keyUp",
|
|
147
|
+
key: "Backspace",
|
|
148
|
+
code: "Backspace",
|
|
149
|
+
windowsVirtualKeyCode: 8,
|
|
150
|
+
nativeVirtualKeyCode: 8
|
|
151
|
+
}, targetId);
|
|
152
|
+
await this.cdp.insertText(message.text, targetId);
|
|
153
|
+
const result = (await this.cdp.evaluateOnPage(this.buildSubmitComposerScript(), targetId));
|
|
154
|
+
if (result?.ok) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
await this.cdp.dispatchKeyEvent({
|
|
158
|
+
type: "keyDown",
|
|
159
|
+
key: "Enter",
|
|
160
|
+
code: "Enter",
|
|
161
|
+
windowsVirtualKeyCode: 13,
|
|
162
|
+
nativeVirtualKeyCode: 13
|
|
163
|
+
}, targetId);
|
|
164
|
+
await this.cdp.dispatchKeyEvent({
|
|
165
|
+
type: "keyUp",
|
|
166
|
+
key: "Enter",
|
|
167
|
+
code: "Enter",
|
|
168
|
+
windowsVirtualKeyCode: 13,
|
|
169
|
+
nativeVirtualKeyCode: 13
|
|
170
|
+
}, targetId);
|
|
171
|
+
const retryResult = (await this.cdp.evaluateOnPage(this.buildComposerSubmissionStateScript(), targetId));
|
|
172
|
+
if (retryResult?.submitted) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.pendingReplyBaselines.delete(binding.sessionKey);
|
|
176
|
+
throw new DesktopDriverError(`Codex desktop composer submit failed: ${retryResult?.reason ?? result?.reason ?? "unknown"}`, "submit_failed");
|
|
177
|
+
}
|
|
178
|
+
async collectAssistantReply(binding, options = {}) {
|
|
179
|
+
const targetId = await this.ensureThreadSelected(binding);
|
|
180
|
+
const baselineReply = this.pendingReplyBaselines.get(binding.sessionKey);
|
|
181
|
+
let candidateReply = null;
|
|
182
|
+
let latestNewReply = null;
|
|
183
|
+
let stablePolls = 0;
|
|
184
|
+
let emittedReplyText = "";
|
|
185
|
+
const emittedMediaReferences = new Set();
|
|
186
|
+
for (let attempt = 0; attempt < this.maxReplyPollAttempts; attempt += 1) {
|
|
187
|
+
const reply = await this.readLatestAssistantSnapshot(targetId);
|
|
188
|
+
const hasReplyText = typeof reply.reply === "string" && reply.reply.trim() !== "";
|
|
189
|
+
const hasReplyContent = hasReplyText || reply.mediaReferences.length > 0;
|
|
190
|
+
const isNewReply = hasReplyContent &&
|
|
191
|
+
(baselineReply === undefined || this.isNewAssistantReply(reply, baselineReply));
|
|
192
|
+
if (isNewReply) {
|
|
193
|
+
latestNewReply = reply;
|
|
194
|
+
if (!this.isSameAssistantReply(reply, candidateReply)) {
|
|
195
|
+
candidateReply = reply;
|
|
196
|
+
stablePolls = reply.isStreaming ? 0 : 1;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
stablePolls += 1;
|
|
200
|
+
}
|
|
201
|
+
if (candidateReply &&
|
|
202
|
+
this.hasAssistantContent(candidateReply) &&
|
|
203
|
+
!reply.isStreaming &&
|
|
204
|
+
stablePolls >= this.replyStablePolls) {
|
|
205
|
+
this.pendingReplyBaselines.delete(binding.sessionKey);
|
|
206
|
+
if (options.onDraft) {
|
|
207
|
+
const finalDeltaDraft = this.buildIncrementalDraftFromSnapshot(binding.sessionKey, candidateReply, emittedReplyText, emittedMediaReferences);
|
|
208
|
+
if (finalDeltaDraft) {
|
|
209
|
+
emittedReplyText = this.mergeObservedReply(emittedReplyText, candidateReply.reply ?? "");
|
|
210
|
+
this.mergeObservedMediaReferences(emittedMediaReferences, candidateReply.mediaReferences);
|
|
211
|
+
await options.onDraft(finalDeltaDraft);
|
|
212
|
+
}
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
return [this.buildOutboundDraftFromSnapshot(binding.sessionKey, candidateReply)];
|
|
216
|
+
}
|
|
217
|
+
if (options.onDraft &&
|
|
218
|
+
candidateReply &&
|
|
219
|
+
this.hasAssistantContent(candidateReply) &&
|
|
220
|
+
stablePolls >= this.partialReplyStablePolls) {
|
|
221
|
+
const deltaDraft = this.buildIncrementalDraftFromSnapshot(binding.sessionKey, candidateReply, emittedReplyText, emittedMediaReferences);
|
|
222
|
+
if (deltaDraft) {
|
|
223
|
+
emittedReplyText = this.mergeObservedReply(emittedReplyText, candidateReply.reply ?? "");
|
|
224
|
+
this.mergeObservedMediaReferences(emittedMediaReferences, candidateReply.mediaReferences);
|
|
225
|
+
await options.onDraft(deltaDraft);
|
|
226
|
+
stablePolls = 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (candidateReply) {
|
|
231
|
+
stablePolls = 0;
|
|
232
|
+
}
|
|
233
|
+
if (attempt + 1 < this.maxReplyPollAttempts) {
|
|
234
|
+
await this.sleep(this.replyPollIntervalMs);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
this.pendingReplyBaselines.delete(binding.sessionKey);
|
|
238
|
+
if (latestNewReply && this.hasAssistantContent(latestNewReply)) {
|
|
239
|
+
if (options.onDraft) {
|
|
240
|
+
const timeoutDraft = this.buildIncrementalDraftFromSnapshot(binding.sessionKey, latestNewReply, emittedReplyText, emittedMediaReferences);
|
|
241
|
+
if (timeoutDraft) {
|
|
242
|
+
await options.onDraft(timeoutDraft);
|
|
243
|
+
}
|
|
244
|
+
return [];
|
|
245
|
+
}
|
|
246
|
+
return [this.buildOutboundDraftFromSnapshot(binding.sessionKey, latestNewReply)];
|
|
247
|
+
}
|
|
248
|
+
throw new DesktopDriverError("Codex desktop reply did not arrive before timeout", "reply_timeout");
|
|
249
|
+
}
|
|
250
|
+
buildOutboundDraftFromSnapshot(sessionKey, snapshot) {
|
|
251
|
+
return {
|
|
252
|
+
draftId: randomUUID(),
|
|
253
|
+
sessionKey,
|
|
254
|
+
text: snapshot.reply ?? "",
|
|
255
|
+
...(snapshot.mediaReferences.length > 0
|
|
256
|
+
? {
|
|
257
|
+
mediaArtifacts: snapshot.mediaReferences.map((reference) => buildMediaArtifactFromReference(reference))
|
|
258
|
+
}
|
|
259
|
+
: {}),
|
|
260
|
+
createdAt: new Date().toISOString()
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
buildIncrementalDraftFromSnapshot(sessionKey, snapshot, emittedReplyText, emittedMediaReferences) {
|
|
264
|
+
const fullReply = snapshot.reply ?? "";
|
|
265
|
+
const deltaText = this.extractReplyDelta(emittedReplyText, fullReply).trim();
|
|
266
|
+
const incrementalMediaReferences = snapshot.mediaReferences.filter((reference) => !emittedMediaReferences.has(reference));
|
|
267
|
+
if (!deltaText && incrementalMediaReferences.length === 0) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
draftId: randomUUID(),
|
|
272
|
+
sessionKey,
|
|
273
|
+
text: deltaText,
|
|
274
|
+
...(incrementalMediaReferences.length > 0
|
|
275
|
+
? {
|
|
276
|
+
mediaArtifacts: incrementalMediaReferences.map((reference) => buildMediaArtifactFromReference(reference))
|
|
277
|
+
}
|
|
278
|
+
: {}),
|
|
279
|
+
createdAt: new Date().toISOString()
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
extractReplyDelta(previous, next) {
|
|
283
|
+
if (!previous) {
|
|
284
|
+
return next;
|
|
285
|
+
}
|
|
286
|
+
if (next.startsWith(previous)) {
|
|
287
|
+
return next.slice(previous.length);
|
|
288
|
+
}
|
|
289
|
+
return next;
|
|
290
|
+
}
|
|
291
|
+
mergeObservedReply(previous, next) {
|
|
292
|
+
if (!previous) {
|
|
293
|
+
return next;
|
|
294
|
+
}
|
|
295
|
+
if (next.startsWith(previous)) {
|
|
296
|
+
return next;
|
|
297
|
+
}
|
|
298
|
+
return next;
|
|
299
|
+
}
|
|
300
|
+
mergeObservedMediaReferences(emittedMediaReferences, mediaReferences) {
|
|
301
|
+
for (const reference of mediaReferences) {
|
|
302
|
+
emittedMediaReferences.add(reference);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
hasAssistantContent(snapshot) {
|
|
306
|
+
return Boolean(snapshot.reply && snapshot.reply.trim()) || snapshot.mediaReferences.length > 0;
|
|
307
|
+
}
|
|
308
|
+
async markSessionBroken(_sessionKey, _reason) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
async ensureThreadSelected(binding) {
|
|
312
|
+
const targetId = await this.resolveTargetId(binding);
|
|
313
|
+
const locator = binding.codexThreadRef
|
|
314
|
+
? this.decodeThreadRef(binding.codexThreadRef)
|
|
315
|
+
: null;
|
|
316
|
+
if (!locator) {
|
|
317
|
+
return targetId;
|
|
318
|
+
}
|
|
319
|
+
const threads = await this.listRecentThreads(200);
|
|
320
|
+
const currentThread = threads.find((thread) => thread.isCurrent);
|
|
321
|
+
if (currentThread?.threadRef === binding.codexThreadRef) {
|
|
322
|
+
return targetId;
|
|
323
|
+
}
|
|
324
|
+
const switchResult = (await this.cdp.evaluateOnPage(this.buildSelectThreadScript(locator), targetId));
|
|
325
|
+
if (!switchResult?.ok) {
|
|
326
|
+
throw new DesktopDriverError(`Codex desktop thread switch failed: ${switchResult?.reason ?? "unknown"}`, "session_not_found");
|
|
327
|
+
}
|
|
328
|
+
await this.sleep(100);
|
|
329
|
+
return targetId;
|
|
330
|
+
}
|
|
331
|
+
async readLatestAssistantSnapshot(targetId) {
|
|
332
|
+
const structuredReply = await this.cdp.evaluateOnPage(this.buildAssistantReplyProbeScript(), targetId);
|
|
333
|
+
if (structuredReply &&
|
|
334
|
+
typeof structuredReply === "object" &&
|
|
335
|
+
"reply" in structuredReply) {
|
|
336
|
+
const rawReply = structuredReply.reply;
|
|
337
|
+
const normalizedReply = typeof rawReply === "string" ? rawReply.trim() : "";
|
|
338
|
+
const unitKey = "unitKey" in structuredReply && typeof structuredReply.unitKey === "string"
|
|
339
|
+
? structuredReply.unitKey
|
|
340
|
+
: null;
|
|
341
|
+
const mediaReferences = "mediaReferences" in structuredReply && Array.isArray(structuredReply.mediaReferences)
|
|
342
|
+
? structuredReply.mediaReferences.filter((reference) => typeof reference === "string" && reference.trim().length > 0)
|
|
343
|
+
: [];
|
|
344
|
+
const isStreaming = "isStreaming" in structuredReply && typeof structuredReply.isStreaming === "boolean"
|
|
345
|
+
? structuredReply.isStreaming
|
|
346
|
+
: false;
|
|
347
|
+
return {
|
|
348
|
+
unitKey,
|
|
349
|
+
reply: normalizedReply || null,
|
|
350
|
+
mediaReferences,
|
|
351
|
+
isStreaming
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const snapshotText = await this.cdp.evaluateOnPage("document.body.innerText", targetId);
|
|
355
|
+
if (typeof snapshotText !== "string") {
|
|
356
|
+
throw new DesktopDriverError("Codex desktop reply snapshot was not a string", "reply_parse_failed");
|
|
357
|
+
}
|
|
358
|
+
const parsedReply = parseAssistantReply(snapshotText).trim();
|
|
359
|
+
return {
|
|
360
|
+
unitKey: null,
|
|
361
|
+
reply: parsedReply || null,
|
|
362
|
+
mediaReferences: [],
|
|
363
|
+
isStreaming: false
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
isNewAssistantReply(current, baseline) {
|
|
367
|
+
if (current.unitKey && baseline.unitKey) {
|
|
368
|
+
return current.unitKey !== baseline.unitKey;
|
|
369
|
+
}
|
|
370
|
+
return current.reply !== baseline.reply;
|
|
371
|
+
}
|
|
372
|
+
isSameAssistantReply(current, candidate) {
|
|
373
|
+
if (!candidate) {
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
if (current.unitKey && candidate.unitKey) {
|
|
377
|
+
return (current.unitKey === candidate.unitKey &&
|
|
378
|
+
current.reply === candidate.reply &&
|
|
379
|
+
current.isStreaming === candidate.isStreaming &&
|
|
380
|
+
JSON.stringify(current.mediaReferences) === JSON.stringify(candidate.mediaReferences));
|
|
381
|
+
}
|
|
382
|
+
return (current.reply === candidate.reply &&
|
|
383
|
+
current.isStreaming === candidate.isStreaming &&
|
|
384
|
+
JSON.stringify(current.mediaReferences) === JSON.stringify(candidate.mediaReferences));
|
|
385
|
+
}
|
|
386
|
+
async waitForFreshThreadContext(targetId) {
|
|
387
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
388
|
+
const probe = (await this.cdp.evaluateOnPage(this.buildFreshThreadProbeScript(), targetId));
|
|
389
|
+
if (probe?.ok) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (attempt + 1 < 20) {
|
|
393
|
+
await this.sleep(100);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
throw new DesktopDriverError("Codex desktop new thread did not become active", "session_not_found");
|
|
397
|
+
}
|
|
398
|
+
async resolvePageTarget() {
|
|
399
|
+
const targets = await this.cdp.listTargets();
|
|
400
|
+
const pageTarget = targets.find((target) => target.type === "page");
|
|
401
|
+
if (!pageTarget) {
|
|
402
|
+
throw new DesktopDriverError("Codex desktop app is not exposing any inspectable page target", "session_not_found");
|
|
403
|
+
}
|
|
404
|
+
return pageTarget;
|
|
405
|
+
}
|
|
406
|
+
async resolveTargetId(binding) {
|
|
407
|
+
if (binding.codexThreadRef?.startsWith(THREAD_REF_PREFIX)) {
|
|
408
|
+
const locator = this.decodeThreadRef(binding.codexThreadRef);
|
|
409
|
+
if (locator) {
|
|
410
|
+
return locator.pageId;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (binding.codexThreadRef?.startsWith(TARGET_REF_PREFIX)) {
|
|
414
|
+
return binding.codexThreadRef.slice(TARGET_REF_PREFIX.length);
|
|
415
|
+
}
|
|
416
|
+
const rebound = await this.openOrBindSession(binding.sessionKey, binding);
|
|
417
|
+
return this.resolveTargetId(rebound);
|
|
418
|
+
}
|
|
419
|
+
encodeThreadRef(locator) {
|
|
420
|
+
const encoded = Buffer.from(JSON.stringify({
|
|
421
|
+
title: locator.title,
|
|
422
|
+
projectName: locator.projectName
|
|
423
|
+
}), "utf8").toString("base64url");
|
|
424
|
+
return `${THREAD_REF_PREFIX}${locator.pageId}:${encoded}`;
|
|
425
|
+
}
|
|
426
|
+
decodeThreadRef(threadRef) {
|
|
427
|
+
if (!threadRef.startsWith(THREAD_REF_PREFIX)) {
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
const payload = threadRef.slice(THREAD_REF_PREFIX.length);
|
|
431
|
+
const separatorIndex = payload.indexOf(":");
|
|
432
|
+
if (separatorIndex <= 0) {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const pageId = payload.slice(0, separatorIndex);
|
|
436
|
+
const encodedLocator = payload.slice(separatorIndex + 1);
|
|
437
|
+
try {
|
|
438
|
+
const locator = JSON.parse(Buffer.from(encodedLocator, "base64url").toString("utf8"));
|
|
439
|
+
if (typeof locator.title !== "string" || locator.title.trim() === "") {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
return {
|
|
443
|
+
pageId,
|
|
444
|
+
title: locator.title,
|
|
445
|
+
projectName: typeof locator.projectName === "string" && locator.projectName.trim() !== ""
|
|
446
|
+
? locator.projectName
|
|
447
|
+
: null
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
buildThreadListScript() {
|
|
455
|
+
return `(() => {
|
|
456
|
+
const toText = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
457
|
+
const extractProjectName = (titleNode) => {
|
|
458
|
+
const row = titleNode.closest('[role="button"]');
|
|
459
|
+
if (!(row instanceof HTMLElement)) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
const candidates = [
|
|
463
|
+
row.closest('[role="listitem"]'),
|
|
464
|
+
row.parentElement,
|
|
465
|
+
row.parentElement?.parentElement,
|
|
466
|
+
row.parentElement?.parentElement?.parentElement
|
|
467
|
+
];
|
|
468
|
+
for (const candidate of candidates) {
|
|
469
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const aria = toText(candidate.getAttribute('aria-label'));
|
|
473
|
+
if (!aria) {
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const quotedMatch = aria.match(/[“"]([^”"]+)[”"]中的自动化操作/);
|
|
477
|
+
if (quotedMatch) {
|
|
478
|
+
return quotedMatch[1];
|
|
479
|
+
}
|
|
480
|
+
const plainMatch = aria.match(/^(.+?)中的自动化操作$/);
|
|
481
|
+
if (plainMatch) {
|
|
482
|
+
return plainMatch[1];
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return null;
|
|
486
|
+
};
|
|
487
|
+
const rows = Array.from(document.querySelectorAll('[data-thread-title="true"]'))
|
|
488
|
+
.map((titleNode) => {
|
|
489
|
+
if (!(titleNode instanceof HTMLElement)) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
const row = titleNode.closest('[role="button"]');
|
|
493
|
+
if (!(row instanceof HTMLElement)) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
const timeNode = row.querySelector('.text-token-description-foreground');
|
|
497
|
+
return {
|
|
498
|
+
title: toText(titleNode.innerText),
|
|
499
|
+
projectName: extractProjectName(titleNode),
|
|
500
|
+
relativeTime: timeNode instanceof HTMLElement ? toText(timeNode.innerText) || null : null,
|
|
501
|
+
isCurrent: row.getAttribute('aria-current') === 'page'
|
|
502
|
+
};
|
|
503
|
+
})
|
|
504
|
+
.filter((thread) => thread && thread.title);
|
|
505
|
+
return rows;
|
|
506
|
+
})();`;
|
|
507
|
+
}
|
|
508
|
+
compareThreadActivity(left, right) {
|
|
509
|
+
const leftRank = this.parseRelativeActivityRank(left.relativeTime);
|
|
510
|
+
const rightRank = this.parseRelativeActivityRank(right.relativeTime);
|
|
511
|
+
if (leftRank !== rightRank) {
|
|
512
|
+
return leftRank - rightRank;
|
|
513
|
+
}
|
|
514
|
+
if (left.isCurrent !== right.isCurrent) {
|
|
515
|
+
return left.isCurrent ? -1 : 1;
|
|
516
|
+
}
|
|
517
|
+
return left.title.localeCompare(right.title, "zh-CN");
|
|
518
|
+
}
|
|
519
|
+
parseRelativeActivityRank(relativeTime) {
|
|
520
|
+
if (!relativeTime) {
|
|
521
|
+
return Number.POSITIVE_INFINITY;
|
|
522
|
+
}
|
|
523
|
+
const value = relativeTime.trim().toLowerCase();
|
|
524
|
+
if (!value) {
|
|
525
|
+
return Number.POSITIVE_INFINITY;
|
|
526
|
+
}
|
|
527
|
+
if (value === "刚刚" ||
|
|
528
|
+
value === "现在" ||
|
|
529
|
+
value === "just now" ||
|
|
530
|
+
value === "now" ||
|
|
531
|
+
value === "today") {
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
534
|
+
const minuteMatch = value.match(/(\d+)\s*(分钟|分|min|mins|minute|minutes)/i);
|
|
535
|
+
if (minuteMatch) {
|
|
536
|
+
return Number(minuteMatch[1]);
|
|
537
|
+
}
|
|
538
|
+
const hourMatch = value.match(/(\d+)\s*(小时|时|hr|hrs|hour|hours)/i);
|
|
539
|
+
if (hourMatch) {
|
|
540
|
+
return Number(hourMatch[1]) * 60;
|
|
541
|
+
}
|
|
542
|
+
const dayMatch = value.match(/(\d+)\s*(天|day|days)/i);
|
|
543
|
+
if (dayMatch) {
|
|
544
|
+
return Number(dayMatch[1]) * 24 * 60;
|
|
545
|
+
}
|
|
546
|
+
const weekMatch = value.match(/(\d+)\s*(周|week|weeks)/i);
|
|
547
|
+
if (weekMatch) {
|
|
548
|
+
return Number(weekMatch[1]) * 7 * 24 * 60;
|
|
549
|
+
}
|
|
550
|
+
const monthMatch = value.match(/(\d+)\s*(月|month|months)/i);
|
|
551
|
+
if (monthMatch) {
|
|
552
|
+
return Number(monthMatch[1]) * 30 * 24 * 60;
|
|
553
|
+
}
|
|
554
|
+
return Number.POSITIVE_INFINITY;
|
|
555
|
+
}
|
|
556
|
+
buildSelectThreadScript(locator) {
|
|
557
|
+
const expectedTitle = JSON.stringify(locator.title);
|
|
558
|
+
const expectedProject = JSON.stringify(locator.projectName);
|
|
559
|
+
return `(() => {
|
|
560
|
+
const normalize = (value) => (value || '').replace(/\\s+/g, ' ').trim();
|
|
561
|
+
const extractProjectName = (titleNode) => {
|
|
562
|
+
const row = titleNode.closest('[role="button"]');
|
|
563
|
+
if (!(row instanceof HTMLElement)) {
|
|
564
|
+
return null;
|
|
565
|
+
}
|
|
566
|
+
const candidates = [
|
|
567
|
+
row.closest('[role="listitem"]'),
|
|
568
|
+
row.parentElement,
|
|
569
|
+
row.parentElement?.parentElement,
|
|
570
|
+
row.parentElement?.parentElement?.parentElement
|
|
571
|
+
];
|
|
572
|
+
for (const candidate of candidates) {
|
|
573
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const aria = normalize(candidate.getAttribute('aria-label'));
|
|
577
|
+
if (!aria) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const quotedMatch = aria.match(/[“"]([^”"]+)[”"]中的自动化操作/);
|
|
581
|
+
if (quotedMatch) {
|
|
582
|
+
return quotedMatch[1];
|
|
583
|
+
}
|
|
584
|
+
const plainMatch = aria.match(/^(.+?)中的自动化操作$/);
|
|
585
|
+
if (plainMatch) {
|
|
586
|
+
return plainMatch[1];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
};
|
|
591
|
+
const target = Array.from(document.querySelectorAll('[data-thread-title="true"]'))
|
|
592
|
+
.find((titleNode) => {
|
|
593
|
+
if (!(titleNode instanceof HTMLElement)) {
|
|
594
|
+
return false;
|
|
595
|
+
}
|
|
596
|
+
const row = titleNode.closest('[role="button"]');
|
|
597
|
+
if (!(row instanceof HTMLElement)) {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
const projectName = extractProjectName(titleNode);
|
|
601
|
+
return normalize(titleNode.innerText) === normalize(${expectedTitle})
|
|
602
|
+
&& normalize(projectName) === normalize(${expectedProject});
|
|
603
|
+
});
|
|
604
|
+
if (!(target instanceof HTMLElement)) {
|
|
605
|
+
return { ok: false, reason: 'thread_not_found' };
|
|
606
|
+
}
|
|
607
|
+
const row = target.closest('[role="button"]');
|
|
608
|
+
if (!(row instanceof HTMLElement)) {
|
|
609
|
+
return { ok: false, reason: 'row_not_found' };
|
|
610
|
+
}
|
|
611
|
+
if (row.getAttribute('aria-current') === 'page') {
|
|
612
|
+
return { ok: true, reason: 'already_current' };
|
|
613
|
+
}
|
|
614
|
+
row.focus();
|
|
615
|
+
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
|
616
|
+
row.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
|
|
617
|
+
}
|
|
618
|
+
return { ok: true, reason: 'clicked_thread' };
|
|
619
|
+
})();`;
|
|
620
|
+
}
|
|
621
|
+
buildNewThreadScript() {
|
|
622
|
+
return `(() => {
|
|
623
|
+
const controls = Array.from(document.querySelectorAll('button, [role="button"]'));
|
|
624
|
+
const button = controls.find((candidate) => {
|
|
625
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
const text = (candidate.textContent || '').replace(/\\s+/g, ' ').trim();
|
|
629
|
+
const aria = candidate.getAttribute('aria-label') || '';
|
|
630
|
+
return text === '新线程' || aria.includes('开始新线程');
|
|
631
|
+
});
|
|
632
|
+
if (!(button instanceof HTMLElement)) {
|
|
633
|
+
return { ok: false, reason: 'new_thread_button_not_found' };
|
|
634
|
+
}
|
|
635
|
+
button.focus();
|
|
636
|
+
if (typeof button.click === 'function') {
|
|
637
|
+
button.click();
|
|
638
|
+
}
|
|
639
|
+
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
|
640
|
+
button.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
|
|
641
|
+
}
|
|
642
|
+
return { ok: true, reason: 'clicked_new_thread' };
|
|
643
|
+
})();`;
|
|
644
|
+
}
|
|
645
|
+
buildFreshThreadProbeScript() {
|
|
646
|
+
return `(() => {
|
|
647
|
+
const composer = document.querySelector(
|
|
648
|
+
'[data-codex-composer="true"], textarea, input[type="text"], [contenteditable="true"]'
|
|
649
|
+
);
|
|
650
|
+
const readComposerText = (node) => {
|
|
651
|
+
if (!(node instanceof HTMLElement)) {
|
|
652
|
+
return '';
|
|
653
|
+
}
|
|
654
|
+
if ('value' in node && typeof node.value === 'string') {
|
|
655
|
+
return node.value;
|
|
656
|
+
}
|
|
657
|
+
return node.textContent || '';
|
|
658
|
+
};
|
|
659
|
+
const assistantUnits = document.querySelectorAll('[data-content-search-unit-key]').length;
|
|
660
|
+
const composerText = readComposerText(composer).trim();
|
|
661
|
+
const fresh = assistantUnits === 0 && composerText.length === 0;
|
|
662
|
+
return { ok: fresh, reason: fresh ? 'fresh_thread' : 'thread_not_ready' };
|
|
663
|
+
})();`;
|
|
664
|
+
}
|
|
665
|
+
buildFocusComposerScript() {
|
|
666
|
+
return `(() => {
|
|
667
|
+
const resolveComposer = () => {
|
|
668
|
+
const selectors = [
|
|
669
|
+
'[data-codex-composer="true"]',
|
|
670
|
+
'textarea',
|
|
671
|
+
'input[type="text"]',
|
|
672
|
+
'[contenteditable="true"]',
|
|
673
|
+
'[role="textbox"]'
|
|
674
|
+
];
|
|
675
|
+
const candidates = selectors
|
|
676
|
+
.flatMap((selector) => Array.from(document.querySelectorAll(selector)))
|
|
677
|
+
.filter((candidate) => {
|
|
678
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
if (candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true') {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
const rect = candidate.getBoundingClientRect();
|
|
685
|
+
return rect.width > 0 && rect.height > 0;
|
|
686
|
+
});
|
|
687
|
+
const activeElement = document.activeElement;
|
|
688
|
+
if (activeElement instanceof HTMLElement && candidates.includes(activeElement)) {
|
|
689
|
+
return activeElement;
|
|
690
|
+
}
|
|
691
|
+
return candidates
|
|
692
|
+
.sort((left, right) => right.getBoundingClientRect().y - left.getBoundingClientRect().y)
|
|
693
|
+
.at(0) ?? null;
|
|
694
|
+
};
|
|
695
|
+
const input = resolveComposer();
|
|
696
|
+
if (!input) {
|
|
697
|
+
return { ok: false, reason: 'input_not_found' };
|
|
698
|
+
}
|
|
699
|
+
input.focus();
|
|
700
|
+
return { ok: true, reason: 'focused_input' };
|
|
701
|
+
})();`;
|
|
702
|
+
}
|
|
703
|
+
buildSubmitComposerScript() {
|
|
704
|
+
const submitButtonMatcher = isLikelyComposerSubmitButton
|
|
705
|
+
.toString()
|
|
706
|
+
.replace(/^function\s+isLikelyComposerSubmitButton/, "function isLikelyComposerSubmitButton");
|
|
707
|
+
return `(() => {
|
|
708
|
+
${submitButtonMatcher}
|
|
709
|
+
const resolveComposer = () => {
|
|
710
|
+
const selectors = [
|
|
711
|
+
'[data-codex-composer="true"]',
|
|
712
|
+
'textarea',
|
|
713
|
+
'input[type="text"]',
|
|
714
|
+
'[contenteditable="true"]',
|
|
715
|
+
'[role="textbox"]'
|
|
716
|
+
];
|
|
717
|
+
const candidates = selectors
|
|
718
|
+
.flatMap((selector) => Array.from(document.querySelectorAll(selector)))
|
|
719
|
+
.filter((candidate) => {
|
|
720
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
721
|
+
return false;
|
|
722
|
+
}
|
|
723
|
+
if (candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true') {
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
const rect = candidate.getBoundingClientRect();
|
|
727
|
+
return rect.width > 0 && rect.height > 0;
|
|
728
|
+
});
|
|
729
|
+
const activeElement = document.activeElement;
|
|
730
|
+
if (activeElement instanceof HTMLElement && candidates.includes(activeElement)) {
|
|
731
|
+
return activeElement;
|
|
732
|
+
}
|
|
733
|
+
return candidates
|
|
734
|
+
.sort((left, right) => right.getBoundingClientRect().y - left.getBoundingClientRect().y)
|
|
735
|
+
.at(0) ?? null;
|
|
736
|
+
};
|
|
737
|
+
const readComposerText = (node) => {
|
|
738
|
+
if (!(node instanceof HTMLElement)) {
|
|
739
|
+
return '';
|
|
740
|
+
}
|
|
741
|
+
if ('value' in node && typeof node.value === 'string') {
|
|
742
|
+
return node.value;
|
|
743
|
+
}
|
|
744
|
+
return node.textContent || '';
|
|
745
|
+
};
|
|
746
|
+
const input = resolveComposer();
|
|
747
|
+
if (!(input instanceof HTMLElement)) {
|
|
748
|
+
return { ok: false, reason: 'input_not_found' };
|
|
749
|
+
}
|
|
750
|
+
const inputRect = input.getBoundingClientRect();
|
|
751
|
+
const currentText = readComposerText(input).trim();
|
|
752
|
+
if (!currentText) {
|
|
753
|
+
return { ok: false, reason: 'empty_input' };
|
|
754
|
+
}
|
|
755
|
+
const sendButton = Array.from(document.querySelectorAll('button, [role="button"]'))
|
|
756
|
+
.filter((candidate) => {
|
|
757
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
const rect = candidate.getBoundingClientRect();
|
|
761
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
if (candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true') {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
return isLikelyComposerSubmitButton({
|
|
768
|
+
text: candidate.textContent ?? '',
|
|
769
|
+
aria: candidate.getAttribute('aria-label'),
|
|
770
|
+
title: candidate.getAttribute('title'),
|
|
771
|
+
className: candidate.className ?? ''
|
|
772
|
+
});
|
|
773
|
+
})
|
|
774
|
+
.sort((left, right) => {
|
|
775
|
+
const leftRect = left.getBoundingClientRect();
|
|
776
|
+
const rightRect = right.getBoundingClientRect();
|
|
777
|
+
const leftDistance = Math.abs(leftRect.y - inputRect.y) + Math.max(0, inputRect.x - leftRect.x);
|
|
778
|
+
const rightDistance = Math.abs(rightRect.y - inputRect.y) + Math.max(0, inputRect.x - rightRect.x);
|
|
779
|
+
return leftDistance - rightDistance;
|
|
780
|
+
})
|
|
781
|
+
.find((candidate) => {
|
|
782
|
+
const rect = candidate.getBoundingClientRect();
|
|
783
|
+
return rect.x >= inputRect.x - 24 && Math.abs(rect.y - inputRect.y) <= 120;
|
|
784
|
+
});
|
|
785
|
+
const beforeButtonHtml = sendButton instanceof HTMLElement ? sendButton.innerHTML : '';
|
|
786
|
+
const confirmSubmission = (reason) => new Promise((resolve) => {
|
|
787
|
+
window.setTimeout(() => {
|
|
788
|
+
const afterText = readComposerText(input).trim();
|
|
789
|
+
const afterButtonHtml = sendButton instanceof HTMLElement ? sendButton.innerHTML : '';
|
|
790
|
+
const buttonChanged = beforeButtonHtml !== '' && beforeButtonHtml !== afterButtonHtml;
|
|
791
|
+
resolve({
|
|
792
|
+
ok: afterText.length === 0 || buttonChanged,
|
|
793
|
+
reason: afterText.length === 0
|
|
794
|
+
? reason
|
|
795
|
+
: (buttonChanged ? 'entered_streaming_state' : 'submit_not_confirmed')
|
|
796
|
+
});
|
|
797
|
+
}, 300);
|
|
798
|
+
});
|
|
799
|
+
const form = input.closest('form');
|
|
800
|
+
if (form && typeof form.requestSubmit === 'function') {
|
|
801
|
+
form.requestSubmit();
|
|
802
|
+
return confirmSubmission('submitted_form');
|
|
803
|
+
}
|
|
804
|
+
if (sendButton instanceof HTMLElement) {
|
|
805
|
+
if (typeof sendButton.click === 'function') {
|
|
806
|
+
sendButton.click();
|
|
807
|
+
}
|
|
808
|
+
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
|
809
|
+
sendButton.dispatchEvent(
|
|
810
|
+
new MouseEvent(type, {
|
|
811
|
+
bubbles: true,
|
|
812
|
+
cancelable: true,
|
|
813
|
+
view: window
|
|
814
|
+
})
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return confirmSubmission('clicked_send_button');
|
|
818
|
+
}
|
|
819
|
+
input.focus();
|
|
820
|
+
const keyboardEventInit = {
|
|
821
|
+
bubbles: true,
|
|
822
|
+
cancelable: true,
|
|
823
|
+
key: 'Enter',
|
|
824
|
+
code: 'Enter',
|
|
825
|
+
keyCode: 13,
|
|
826
|
+
which: 13
|
|
827
|
+
};
|
|
828
|
+
input.dispatchEvent(new KeyboardEvent('keydown', keyboardEventInit));
|
|
829
|
+
input.dispatchEvent(new KeyboardEvent('keypress', keyboardEventInit));
|
|
830
|
+
input.dispatchEvent(new KeyboardEvent('keyup', keyboardEventInit));
|
|
831
|
+
return confirmSubmission('pressed_enter');
|
|
832
|
+
})();`;
|
|
833
|
+
}
|
|
834
|
+
buildComposerSubmissionStateScript() {
|
|
835
|
+
return `(() => {
|
|
836
|
+
const selectors = [
|
|
837
|
+
'[data-codex-composer="true"]',
|
|
838
|
+
'textarea',
|
|
839
|
+
'input[type="text"]',
|
|
840
|
+
'[contenteditable="true"]',
|
|
841
|
+
'[role="textbox"]'
|
|
842
|
+
];
|
|
843
|
+
const input = selectors
|
|
844
|
+
.flatMap((selector) => Array.from(document.querySelectorAll(selector)))
|
|
845
|
+
.find((candidate) => {
|
|
846
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
if (candidate.hasAttribute('disabled') || candidate.getAttribute('aria-disabled') === 'true') {
|
|
850
|
+
return false;
|
|
851
|
+
}
|
|
852
|
+
const rect = candidate.getBoundingClientRect();
|
|
853
|
+
return rect.width > 0 && rect.height > 0;
|
|
854
|
+
});
|
|
855
|
+
if (!(input instanceof HTMLElement)) {
|
|
856
|
+
return { submitted: false, reason: 'input_not_found' };
|
|
857
|
+
}
|
|
858
|
+
const currentText =
|
|
859
|
+
'value' in input && typeof input.value === 'string'
|
|
860
|
+
? input.value.trim()
|
|
861
|
+
: (input.textContent || '').trim();
|
|
862
|
+
const sendButton = Array.from(document.querySelectorAll('button, [role="button"]')).find((candidate) => {
|
|
863
|
+
if (!(candidate instanceof HTMLElement)) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
const className = typeof candidate.className === 'string' ? candidate.className : '';
|
|
867
|
+
return className.includes('size-token-button-composer') && className.includes('bg-token-foreground');
|
|
868
|
+
});
|
|
869
|
+
const buttonHtml = sendButton instanceof HTMLElement ? sendButton.innerHTML : '';
|
|
870
|
+
const isStreamingButton = buttonHtml.includes('M4.5 5.75C4.5 5.05964');
|
|
871
|
+
return {
|
|
872
|
+
submitted: currentText.length === 0 || isStreamingButton,
|
|
873
|
+
reason: currentText.length === 0 ? 'composer_cleared' : (isStreamingButton ? 'entered_streaming_state' : 'submit_not_confirmed')
|
|
874
|
+
};
|
|
875
|
+
})();`;
|
|
876
|
+
}
|
|
877
|
+
buildAssistantReplyProbeScript() {
|
|
878
|
+
return `(() => {
|
|
879
|
+
const assistantUnits = Array.from(
|
|
880
|
+
document.querySelectorAll('[data-content-search-unit-key$=":assistant"]')
|
|
881
|
+
);
|
|
882
|
+
const latestAssistantUnit = assistantUnits.at(-1);
|
|
883
|
+
if (!(latestAssistantUnit instanceof HTMLElement)) {
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
const normalizeReference = (value) => {
|
|
887
|
+
if (!value || typeof value !== 'string') {
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
if (value.startsWith('file://')) {
|
|
891
|
+
try {
|
|
892
|
+
return decodeURIComponent(new URL(value).pathname);
|
|
893
|
+
} catch {
|
|
894
|
+
return value;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
if (
|
|
898
|
+
value.startsWith('http://') ||
|
|
899
|
+
value.startsWith('https://') ||
|
|
900
|
+
value.startsWith('/') ||
|
|
901
|
+
value.startsWith('data:')
|
|
902
|
+
) {
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
return null;
|
|
906
|
+
};
|
|
907
|
+
const isLocalReference = (value) =>
|
|
908
|
+
typeof value === 'string' &&
|
|
909
|
+
(
|
|
910
|
+
value.startsWith('/') ||
|
|
911
|
+
value.startsWith('./') ||
|
|
912
|
+
value.startsWith('../') ||
|
|
913
|
+
/^[A-Za-z]:[\\\\/]/.test(value)
|
|
914
|
+
);
|
|
915
|
+
const serializeRichContent = (root) => {
|
|
916
|
+
const clone = root.cloneNode(true);
|
|
917
|
+
if (!(clone instanceof HTMLElement)) {
|
|
918
|
+
return root.innerText.trim();
|
|
919
|
+
}
|
|
920
|
+
clone.querySelectorAll('a[href]').forEach((link) => {
|
|
921
|
+
if (!(link instanceof HTMLAnchorElement)) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const href = normalizeReference(link.href) || link.getAttribute('href') || '';
|
|
925
|
+
const text = (link.textContent || '').trim();
|
|
926
|
+
const replacement = href && text && text !== href
|
|
927
|
+
? text + '\\n' + href
|
|
928
|
+
: (href || text);
|
|
929
|
+
link.textContent = replacement;
|
|
930
|
+
});
|
|
931
|
+
const serializeNode = (node, listContext) => {
|
|
932
|
+
if (node instanceof HTMLBRElement) {
|
|
933
|
+
return '\\n';
|
|
934
|
+
}
|
|
935
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
936
|
+
return node.textContent || '';
|
|
937
|
+
}
|
|
938
|
+
if (!(node instanceof HTMLElement)) {
|
|
939
|
+
return '';
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const tagName = node.tagName;
|
|
943
|
+
if (
|
|
944
|
+
tagName === 'DIV' &&
|
|
945
|
+
typeof node.className === 'string' &&
|
|
946
|
+
node.className.includes('bg-token-text-code-block-background')
|
|
947
|
+
) {
|
|
948
|
+
const codeElement = node.querySelector('code');
|
|
949
|
+
if (codeElement instanceof HTMLElement) {
|
|
950
|
+
const codeSource = codeElement.innerText || '';
|
|
951
|
+
const normalizedCode = codeSource
|
|
952
|
+
.replace(/\\r\\n/g, '\\n')
|
|
953
|
+
.replace(/\\u00a0/g, ' ')
|
|
954
|
+
.replace(/\\n+$/g, '');
|
|
955
|
+
if (normalizedCode.trim()) {
|
|
956
|
+
const languageNode = node.querySelector('.min-w-0.truncate');
|
|
957
|
+
const languageText = languageNode instanceof HTMLElement
|
|
958
|
+
? (languageNode.textContent || '').trim()
|
|
959
|
+
: '';
|
|
960
|
+
const language = /^[A-Za-z0-9_+#.-]{1,24}$/.test(languageText) ? languageText : '';
|
|
961
|
+
return '\`\`\`' + language + '\\n' + normalizedCode + '\\n\`\`\`' + '\\n';
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
if (tagName === 'PRE') {
|
|
967
|
+
const codeElement = node.querySelector('code');
|
|
968
|
+
const codeSource = codeElement instanceof HTMLElement ? codeElement.innerText : node.innerText;
|
|
969
|
+
const normalizedCode = (codeSource || '')
|
|
970
|
+
.replace(/\\r\\n/g, '\\n')
|
|
971
|
+
.replace(/\\u00a0/g, ' ')
|
|
972
|
+
.replace(/\\n+$/g, '');
|
|
973
|
+
if (!normalizedCode.trim()) {
|
|
974
|
+
return '';
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
let language = '';
|
|
978
|
+
if (codeElement instanceof HTMLElement) {
|
|
979
|
+
const classNames = Array.from(codeElement.classList.values());
|
|
980
|
+
const languageClass = classNames.find((value) => /^language[-:]/i.test(value));
|
|
981
|
+
if (languageClass) {
|
|
982
|
+
language = languageClass.replace(/^language[-:]/i, '').trim();
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const lines = normalizedCode.split('\\n');
|
|
987
|
+
if (!language && lines.length > 1) {
|
|
988
|
+
const firstLine = lines[0].trim();
|
|
989
|
+
if (/^[A-Za-z0-9_+#.-]{1,24}$/.test(firstLine)) {
|
|
990
|
+
language = firstLine;
|
|
991
|
+
lines.shift();
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
const fencedBody = lines.join('\\n').replace(/\\n+$/g, '');
|
|
996
|
+
return '\`\`\`' + language + '\\n' + fencedBody + '\\n\`\`\`' + '\\n';
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
if (tagName === 'TABLE') {
|
|
1000
|
+
const rows = Array.from(node.querySelectorAll('tr'))
|
|
1001
|
+
.map((row) =>
|
|
1002
|
+
Array.from(row.querySelectorAll('th, td'))
|
|
1003
|
+
.map((cell) => (cell.textContent || '').replace(/\\s+/g, ' ').trim())
|
|
1004
|
+
)
|
|
1005
|
+
.filter((cells) => cells.length > 0);
|
|
1006
|
+
if (!rows.length) {
|
|
1007
|
+
return '';
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const header = rows[0];
|
|
1011
|
+
const separator = header.map(() => '---');
|
|
1012
|
+
const bodyRows = rows.slice(1);
|
|
1013
|
+
const markdownRows = [
|
|
1014
|
+
'| ' + header.join(' | ') + ' |',
|
|
1015
|
+
'| ' + separator.join(' | ') + ' |',
|
|
1016
|
+
...bodyRows.map((cells) => '| ' + cells.join(' | ') + ' |')
|
|
1017
|
+
];
|
|
1018
|
+
return markdownRows.join('\\n') + '\\n';
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (tagName === 'OL') {
|
|
1022
|
+
return Array.from(node.children)
|
|
1023
|
+
.map((child, index) => serializeNode(child, { type: 'ol', index }))
|
|
1024
|
+
.filter(Boolean)
|
|
1025
|
+
.join('\\n');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (tagName === 'UL') {
|
|
1029
|
+
return Array.from(node.children)
|
|
1030
|
+
.map((child) => serializeNode(child, { type: 'ul' }))
|
|
1031
|
+
.filter(Boolean)
|
|
1032
|
+
.join('\\n');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
if (tagName === 'LI') {
|
|
1036
|
+
const content = Array.from(node.childNodes)
|
|
1037
|
+
.map((child) => serializeNode(child, null))
|
|
1038
|
+
.join('')
|
|
1039
|
+
.replace(/\\s+\\n/g, '\\n')
|
|
1040
|
+
.replace(/\\n\\s+/g, '\\n')
|
|
1041
|
+
.replace(/[ \\t]+/g, ' ')
|
|
1042
|
+
.trim();
|
|
1043
|
+
if (!content) {
|
|
1044
|
+
return '';
|
|
1045
|
+
}
|
|
1046
|
+
if (listContext?.type === 'ol') {
|
|
1047
|
+
const index = typeof listContext.index === 'number' ? listContext.index : 0;
|
|
1048
|
+
return String(index + 1) + '. ' + content;
|
|
1049
|
+
}
|
|
1050
|
+
return '- ' + content;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const serializedChildren = Array.from(node.childNodes)
|
|
1054
|
+
.map((child) => serializeNode(child, null))
|
|
1055
|
+
.join('');
|
|
1056
|
+
if (['P', 'DIV', 'SECTION', 'ARTICLE', 'BLOCKQUOTE'].includes(tagName)) {
|
|
1057
|
+
return serializedChildren.trim() ? serializedChildren.trim() + '\\n' : '';
|
|
1058
|
+
}
|
|
1059
|
+
return serializedChildren;
|
|
1060
|
+
};
|
|
1061
|
+
return serializeNode(clone, null)
|
|
1062
|
+
.replace(/[ \\t]+\\n/g, '\\n')
|
|
1063
|
+
.replace(/\\n{3,}/g, '\\n\\n')
|
|
1064
|
+
.trim();
|
|
1065
|
+
};
|
|
1066
|
+
const mediaReferences = Array.from(
|
|
1067
|
+
latestAssistantUnit.querySelectorAll('img[src], audio[src], audio source[src], video[src], video source[src], a[href]')
|
|
1068
|
+
)
|
|
1069
|
+
.map((node) => {
|
|
1070
|
+
if (!(node instanceof HTMLElement)) {
|
|
1071
|
+
return null;
|
|
1072
|
+
}
|
|
1073
|
+
if ('src' in node && typeof node.src === 'string' && node.src) {
|
|
1074
|
+
return normalizeReference(node.src);
|
|
1075
|
+
}
|
|
1076
|
+
if ('href' in node && typeof node.href === 'string' && node.href) {
|
|
1077
|
+
const normalizedHref = normalizeReference(node.href);
|
|
1078
|
+
return normalizedHref && isLocalReference(normalizedHref)
|
|
1079
|
+
? normalizedHref
|
|
1080
|
+
: null;
|
|
1081
|
+
}
|
|
1082
|
+
return null;
|
|
1083
|
+
})
|
|
1084
|
+
.filter((value, index, values) => typeof value === 'string' && values.indexOf(value) === index);
|
|
1085
|
+
const composer = document.querySelector(
|
|
1086
|
+
'[data-codex-composer="true"], textarea, input[type="text"], [contenteditable="true"], [role="textbox"]'
|
|
1087
|
+
);
|
|
1088
|
+
const composerRect = composer instanceof HTMLElement
|
|
1089
|
+
? composer.getBoundingClientRect()
|
|
1090
|
+
: null;
|
|
1091
|
+
const streamingMatcher = /(\\bstop\\b|\\bthinking\\b|\\bworking\\b|\\brunning\\b|停止|中止|取消|思考中|生成中)/i;
|
|
1092
|
+
const assistantStatusMatcher = /(Reconnecting\\.{3}|Searching\\.{3}|Running\\.{3}|Working\\.{3}|连接中\\.{0,3}|重新连接中\\.{0,3}|搜索中\\.{0,3}|执行中\\.{0,3}|处理中\\.{0,3})/i;
|
|
1093
|
+
const isComposerBusyButton = (node) => {
|
|
1094
|
+
if (!(node instanceof HTMLElement)) {
|
|
1095
|
+
return false;
|
|
1096
|
+
}
|
|
1097
|
+
const className = String(node.className || '');
|
|
1098
|
+
if (!className.includes('size-token-button-composer')) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
const html = node.innerHTML || '';
|
|
1102
|
+
return html.includes('M4.5 5.75C4.5 5.05964')
|
|
1103
|
+
|| html.includes('M4.5 5.75C4.5 5.0596');
|
|
1104
|
+
};
|
|
1105
|
+
const isStreaming = Array.from(document.querySelectorAll('button, [role="button"], [aria-busy="true"]'))
|
|
1106
|
+
.some((node) => {
|
|
1107
|
+
if (!(node instanceof HTMLElement)) {
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
const rect = node.getBoundingClientRect();
|
|
1111
|
+
const isNearComposer = composerRect
|
|
1112
|
+
? rect.y >= composerRect.y - 48 && rect.y <= composerRect.bottom + 48
|
|
1113
|
+
: rect.y >= window.innerHeight - 160;
|
|
1114
|
+
if (node.getAttribute('aria-busy') === 'true') {
|
|
1115
|
+
return true;
|
|
1116
|
+
}
|
|
1117
|
+
if (isComposerBusyButton(node)) {
|
|
1118
|
+
return true;
|
|
1119
|
+
}
|
|
1120
|
+
if (!isNearComposer) {
|
|
1121
|
+
return false;
|
|
1122
|
+
}
|
|
1123
|
+
const label = [
|
|
1124
|
+
node.textContent || '',
|
|
1125
|
+
node.getAttribute('aria-label') || '',
|
|
1126
|
+
node.getAttribute('title') || ''
|
|
1127
|
+
].join(' ').trim();
|
|
1128
|
+
return streamingMatcher.test(label);
|
|
1129
|
+
});
|
|
1130
|
+
const assistantStatusText = Array.from(
|
|
1131
|
+
latestAssistantUnit.querySelectorAll('.text-xs, [aria-live], [data-state], [class*="status"], [class*="loading"]')
|
|
1132
|
+
)
|
|
1133
|
+
.map((node) => (node instanceof HTMLElement ? node.innerText || '' : ''))
|
|
1134
|
+
.join('\\n');
|
|
1135
|
+
const hasAssistantActivity = assistantStatusMatcher.test(assistantStatusText)
|
|
1136
|
+
|| assistantStatusMatcher.test(latestAssistantUnit.innerText || '');
|
|
1137
|
+
|
|
1138
|
+
const richContent = latestAssistantUnit.querySelector('[class*="_markdownContent_"]');
|
|
1139
|
+
if (richContent instanceof HTMLElement) {
|
|
1140
|
+
const text = serializeRichContent(richContent);
|
|
1141
|
+
if (text) {
|
|
1142
|
+
return {
|
|
1143
|
+
unitKey: latestAssistantUnit.getAttribute('data-content-search-unit-key'),
|
|
1144
|
+
reply: text,
|
|
1145
|
+
mediaReferences,
|
|
1146
|
+
isStreaming: isStreaming || hasAssistantActivity
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const sanitizedUnit = latestAssistantUnit.cloneNode(true);
|
|
1152
|
+
if (!(sanitizedUnit instanceof HTMLElement)) {
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
sanitizedUnit
|
|
1156
|
+
.querySelectorAll('button, [role="button"], [aria-label], .text-xs')
|
|
1157
|
+
.forEach((node) => node.remove());
|
|
1158
|
+
const text = sanitizedUnit.innerText
|
|
1159
|
+
.split('\\n')
|
|
1160
|
+
.map((line) => line.trim())
|
|
1161
|
+
.filter(Boolean)
|
|
1162
|
+
.join('\\n')
|
|
1163
|
+
.trim();
|
|
1164
|
+
return text || mediaReferences.length > 0
|
|
1165
|
+
? {
|
|
1166
|
+
unitKey: latestAssistantUnit.getAttribute('data-content-search-unit-key'),
|
|
1167
|
+
reply: text || null,
|
|
1168
|
+
mediaReferences,
|
|
1169
|
+
isStreaming: isStreaming || hasAssistantActivity
|
|
1170
|
+
}
|
|
1171
|
+
: null;
|
|
1172
|
+
})();`;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function buildMediaArtifactFromReference(reference) {
|
|
1176
|
+
const normalizedReference = reference.trim();
|
|
1177
|
+
const strippedReference = normalizedReference.split("?")[0] ?? normalizedReference;
|
|
1178
|
+
const lowerReference = strippedReference.toLowerCase();
|
|
1179
|
+
const originalName = inferOriginalName(strippedReference);
|
|
1180
|
+
const mimeType = inferMimeType(lowerReference);
|
|
1181
|
+
return {
|
|
1182
|
+
kind: inferMediaArtifactKind(lowerReference, mimeType),
|
|
1183
|
+
sourceUrl: normalizedReference,
|
|
1184
|
+
localPath: normalizedReference,
|
|
1185
|
+
mimeType,
|
|
1186
|
+
fileSize: 0,
|
|
1187
|
+
originalName
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function inferMediaArtifactKind(reference, mimeType) {
|
|
1191
|
+
if (mimeType.startsWith("image/") || /\.(png|jpg|jpeg|gif|webp|bmp)$/i.test(reference)) {
|
|
1192
|
+
return MediaArtifactKind.Image;
|
|
1193
|
+
}
|
|
1194
|
+
if (mimeType.startsWith("audio/") || /\.(mp3|wav|ogg|aac|flac|silk)$/i.test(reference)) {
|
|
1195
|
+
return MediaArtifactKind.Audio;
|
|
1196
|
+
}
|
|
1197
|
+
if (mimeType.startsWith("video/") || /\.(mp4|mov|avi|mkv|webm)$/i.test(reference)) {
|
|
1198
|
+
return MediaArtifactKind.Video;
|
|
1199
|
+
}
|
|
1200
|
+
return MediaArtifactKind.File;
|
|
1201
|
+
}
|
|
1202
|
+
function inferMimeType(reference) {
|
|
1203
|
+
if (reference.startsWith("data:image/")) {
|
|
1204
|
+
const match = reference.match(/^data:(image\/[^;]+);/i);
|
|
1205
|
+
return match?.[1] ?? "image/png";
|
|
1206
|
+
}
|
|
1207
|
+
if (/\.png$/i.test(reference))
|
|
1208
|
+
return "image/png";
|
|
1209
|
+
if (/\.(jpg|jpeg)$/i.test(reference))
|
|
1210
|
+
return "image/jpeg";
|
|
1211
|
+
if (/\.gif$/i.test(reference))
|
|
1212
|
+
return "image/gif";
|
|
1213
|
+
if (/\.webp$/i.test(reference))
|
|
1214
|
+
return "image/webp";
|
|
1215
|
+
if (/\.bmp$/i.test(reference))
|
|
1216
|
+
return "image/bmp";
|
|
1217
|
+
if (/\.mp3$/i.test(reference))
|
|
1218
|
+
return "audio/mpeg";
|
|
1219
|
+
if (/\.wav$/i.test(reference))
|
|
1220
|
+
return "audio/wav";
|
|
1221
|
+
if (/\.ogg$/i.test(reference))
|
|
1222
|
+
return "audio/ogg";
|
|
1223
|
+
if (/\.aac$/i.test(reference))
|
|
1224
|
+
return "audio/aac";
|
|
1225
|
+
if (/\.flac$/i.test(reference))
|
|
1226
|
+
return "audio/flac";
|
|
1227
|
+
if (/\.silk$/i.test(reference))
|
|
1228
|
+
return "audio/silk";
|
|
1229
|
+
if (/\.mp4$/i.test(reference))
|
|
1230
|
+
return "video/mp4";
|
|
1231
|
+
if (/\.mov$/i.test(reference))
|
|
1232
|
+
return "video/quicktime";
|
|
1233
|
+
if (/\.avi$/i.test(reference))
|
|
1234
|
+
return "video/x-msvideo";
|
|
1235
|
+
if (/\.mkv$/i.test(reference))
|
|
1236
|
+
return "video/x-matroska";
|
|
1237
|
+
if (/\.webm$/i.test(reference))
|
|
1238
|
+
return "video/webm";
|
|
1239
|
+
if (/\.pdf$/i.test(reference))
|
|
1240
|
+
return "application/pdf";
|
|
1241
|
+
return "application/octet-stream";
|
|
1242
|
+
}
|
|
1243
|
+
function inferOriginalName(reference) {
|
|
1244
|
+
try {
|
|
1245
|
+
if (reference.startsWith("data:image/")) {
|
|
1246
|
+
return "codex-inline-image";
|
|
1247
|
+
}
|
|
1248
|
+
const url = reference.startsWith("http://") || reference.startsWith("https://")
|
|
1249
|
+
? new URL(reference)
|
|
1250
|
+
: null;
|
|
1251
|
+
const pathname = url?.pathname ?? reference;
|
|
1252
|
+
const segments = pathname.split("/");
|
|
1253
|
+
return segments.at(-1) || "codex-media";
|
|
1254
|
+
}
|
|
1255
|
+
catch {
|
|
1256
|
+
const segments = reference.split("/");
|
|
1257
|
+
return segments.at(-1) || "codex-media";
|
|
1258
|
+
}
|
|
1259
|
+
}
|