decant-core 1.4.0 → 1.6.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/ai/chatgpt.js +19 -4
- package/ai/gemini.js +194 -23
- package/ai/perplexity.js +275 -23
- package/package.json +4 -1
package/ai/chatgpt.js
CHANGED
|
@@ -759,6 +759,19 @@ export class ChatGPTParser extends ChatParser {
|
|
|
759
759
|
}
|
|
760
760
|
});
|
|
761
761
|
|
|
762
|
+
clone
|
|
763
|
+
.querySelectorAll(
|
|
764
|
+
'[data-testid="webpage-citation-pill"], [data-testid*="citation-pill"]',
|
|
765
|
+
)
|
|
766
|
+
.forEach((pill) => {
|
|
767
|
+
pill.querySelectorAll("img").forEach((img) => img.remove());
|
|
768
|
+
pill.querySelectorAll("span").forEach((span) => {
|
|
769
|
+
if (/^\+\d+$/.test(span.textContent.trim())) {
|
|
770
|
+
span.remove();
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
|
|
762
775
|
noiseSelectors.forEach((selector) => {
|
|
763
776
|
clone.querySelectorAll(selector).forEach((node) => node.remove());
|
|
764
777
|
});
|
|
@@ -783,11 +796,13 @@ export class ChatGPTParser extends ChatParser {
|
|
|
783
796
|
}
|
|
784
797
|
|
|
785
798
|
extractMountedMessages() {
|
|
786
|
-
const
|
|
799
|
+
const turns = getConversationTurns(document);
|
|
787
800
|
const containers =
|
|
788
|
-
|
|
789
|
-
?
|
|
790
|
-
: Array.from(
|
|
801
|
+
turns.length > 0
|
|
802
|
+
? turns
|
|
803
|
+
: Array.from(
|
|
804
|
+
document.querySelectorAll("article, [data-message-author-role]"),
|
|
805
|
+
);
|
|
791
806
|
|
|
792
807
|
return containers
|
|
793
808
|
.map((container) => this.extractMessage(container))
|
package/ai/gemini.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ChatParser } from "./base.js";
|
|
2
2
|
import { convertToMarkdown } from "../utils/html-to-markdown.js";
|
|
3
|
+
import { normalizeLatexMath } from "../utils/latex-math.js";
|
|
3
4
|
|
|
4
5
|
const GEMINI_RPC_ID = "hNvQHb";
|
|
5
6
|
const DEFAULT_BARD_PATH = "/_/BardChatUi";
|
|
@@ -94,23 +95,7 @@ export class GeminiParser extends ChatParser {
|
|
|
94
95
|
|
|
95
96
|
const mode = options.parserMode || "auto";
|
|
96
97
|
|
|
97
|
-
// 1.
|
|
98
|
-
if (typeof document !== "undefined" && document.querySelector) {
|
|
99
|
-
const hasDomMessages = document.querySelector(
|
|
100
|
-
".conversation-container, user-query, model-response, deep-research-immersive-panel",
|
|
101
|
-
);
|
|
102
|
-
if (hasDomMessages && mode !== "api") {
|
|
103
|
-
const domResult = this.parseFromDom(currentUrl, options);
|
|
104
|
-
if (domResult && domResult.messages && domResult.messages.length > 0) {
|
|
105
|
-
console.log(
|
|
106
|
-
`[Gemini Parser] Successfully parsed ${domResult.messages.length} messages from DOM`,
|
|
107
|
-
);
|
|
108
|
-
return domResult;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// 2. Attempt API / RPC extraction if DOM parsing didn't find messages or mode is API
|
|
98
|
+
// 1. Attempt API / RPC extraction first when not explicitly in 'dom' mode
|
|
114
99
|
if (mode !== "dom" && typeof fetch === "function") {
|
|
115
100
|
try {
|
|
116
101
|
const convoId = this.getConversationId(currentUrl);
|
|
@@ -149,7 +134,7 @@ export class GeminiParser extends ChatParser {
|
|
|
149
134
|
}
|
|
150
135
|
}
|
|
151
136
|
|
|
152
|
-
// Fall back to robust DOM parsing
|
|
137
|
+
// 2. Fall back to robust DOM parsing
|
|
153
138
|
return this.parseFromDom(currentUrl, options);
|
|
154
139
|
}
|
|
155
140
|
|
|
@@ -175,14 +160,17 @@ export class GeminiParser extends ChatParser {
|
|
|
175
160
|
`&_reqid=${encodeURIComponent(reqId)}` +
|
|
176
161
|
`&rt=c`;
|
|
177
162
|
|
|
163
|
+
const formattedConvoId = convoId.startsWith("c_")
|
|
164
|
+
? convoId
|
|
165
|
+
: `c_${convoId}`;
|
|
178
166
|
const allItems = [];
|
|
179
167
|
let cursor = null;
|
|
180
168
|
let pageCount = 0;
|
|
181
169
|
|
|
182
|
-
while (pageCount <
|
|
170
|
+
while (pageCount < 250) {
|
|
183
171
|
pageCount++;
|
|
184
172
|
const payloadArg = JSON.stringify([
|
|
185
|
-
|
|
173
|
+
formattedConvoId,
|
|
186
174
|
100,
|
|
187
175
|
cursor,
|
|
188
176
|
1,
|
|
@@ -227,11 +215,11 @@ export class GeminiParser extends ChatParser {
|
|
|
227
215
|
const continueCursor = payload[1] || null;
|
|
228
216
|
|
|
229
217
|
if (items.length > 0) {
|
|
230
|
-
// Items are in reverse chronological order from API
|
|
218
|
+
// Items are in reverse chronological order from API; reverse to maintain oldest-first order
|
|
231
219
|
allItems.unshift(...items.slice().reverse());
|
|
232
220
|
}
|
|
233
221
|
|
|
234
|
-
if (!continueCursor
|
|
222
|
+
if (!continueCursor) {
|
|
235
223
|
break;
|
|
236
224
|
}
|
|
237
225
|
cursor = continueCursor;
|
|
@@ -241,7 +229,158 @@ export class GeminiParser extends ChatParser {
|
|
|
241
229
|
return null;
|
|
242
230
|
}
|
|
243
231
|
|
|
232
|
+
return this.formatApiResult(allItems, currentUrl, options);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
enrichMessagesWithDomAttachments(messages) {
|
|
236
|
+
try {
|
|
237
|
+
if (typeof document === "undefined" || !document.querySelectorAll) {
|
|
238
|
+
return messages;
|
|
239
|
+
}
|
|
240
|
+
const parentContainers = document.querySelectorAll(
|
|
241
|
+
".conversation-container",
|
|
242
|
+
);
|
|
243
|
+
const domTurns = [];
|
|
244
|
+
|
|
245
|
+
if (parentContainers.length > 0) {
|
|
246
|
+
parentContainers.forEach((container, idx) => {
|
|
247
|
+
const rawId = (container.id || "").replace(/^r_/, "");
|
|
248
|
+
const fileElements = container.querySelectorAll(
|
|
249
|
+
"user-query-file-preview, .file-preview, .attachment-preview, [data-testid='file-preview']",
|
|
250
|
+
);
|
|
251
|
+
const fileNames = [];
|
|
252
|
+
fileElements.forEach((fe) => {
|
|
253
|
+
const name = (fe.textContent || "").trim();
|
|
254
|
+
if (name && !fileNames.includes(name)) fileNames.push(name);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
const userQuery = container.querySelector("user-query") || container;
|
|
258
|
+
const clone = userQuery.cloneNode(true);
|
|
259
|
+
clone
|
|
260
|
+
.querySelectorAll(
|
|
261
|
+
"user-query-file-preview, .file-preview, .attachment-preview, button, .edit-button, model-response",
|
|
262
|
+
)
|
|
263
|
+
.forEach((el) => el.remove());
|
|
264
|
+
const queryText = (
|
|
265
|
+
clone.innerText !== undefined
|
|
266
|
+
? clone.innerText
|
|
267
|
+
: clone.textContent || ""
|
|
268
|
+
)
|
|
269
|
+
.replace(/^You said\s*/i, "")
|
|
270
|
+
.trim();
|
|
271
|
+
|
|
272
|
+
if (fileNames.length > 0) {
|
|
273
|
+
domTurns.push({
|
|
274
|
+
turnId: rawId,
|
|
275
|
+
queryText,
|
|
276
|
+
domIndex: idx,
|
|
277
|
+
totalDom: parentContainers.length,
|
|
278
|
+
fileNames,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
} else {
|
|
283
|
+
const userQueries = document.querySelectorAll("user-query");
|
|
284
|
+
userQueries.forEach((uq, idx) => {
|
|
285
|
+
const fileElements = uq.querySelectorAll(
|
|
286
|
+
"user-query-file-preview, .file-preview, .attachment-preview, [data-testid='file-preview']",
|
|
287
|
+
);
|
|
288
|
+
const fileNames = [];
|
|
289
|
+
fileElements.forEach((fe) => {
|
|
290
|
+
const name = (fe.textContent || "").trim();
|
|
291
|
+
if (name && !fileNames.includes(name)) fileNames.push(name);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const clone = uq.cloneNode(true);
|
|
295
|
+
clone
|
|
296
|
+
.querySelectorAll(
|
|
297
|
+
"user-query-file-preview, .file-preview, .attachment-preview, button, .edit-button",
|
|
298
|
+
)
|
|
299
|
+
.forEach((el) => el.remove());
|
|
300
|
+
const queryText = (
|
|
301
|
+
clone.innerText !== undefined
|
|
302
|
+
? clone.innerText
|
|
303
|
+
: clone.textContent || ""
|
|
304
|
+
)
|
|
305
|
+
.replace(/^You said\s*/i, "")
|
|
306
|
+
.trim();
|
|
307
|
+
|
|
308
|
+
if (fileNames.length > 0) {
|
|
309
|
+
domTurns.push({
|
|
310
|
+
domIndex: idx,
|
|
311
|
+
queryText,
|
|
312
|
+
totalDom: userQueries.length,
|
|
313
|
+
fileNames,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (domTurns.length === 0) return messages;
|
|
320
|
+
|
|
321
|
+
const userMessages = messages.filter((m) => m.role === "User");
|
|
322
|
+
|
|
323
|
+
for (const domTurn of domTurns) {
|
|
324
|
+
let matchedMsg = null;
|
|
325
|
+
|
|
326
|
+
// 1. Match by exact turnId if present
|
|
327
|
+
if (domTurn.turnId) {
|
|
328
|
+
matchedMsg = userMessages.find((m) => m.turnId === domTurn.turnId);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 2. Fallback: match by queryText if present and non-empty
|
|
332
|
+
if (!matchedMsg && domTurn.queryText) {
|
|
333
|
+
matchedMsg = userMessages.find(
|
|
334
|
+
(m) =>
|
|
335
|
+
m.content &&
|
|
336
|
+
(m.content.includes(domTurn.queryText) ||
|
|
337
|
+
domTurn.queryText.includes(m.content)),
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 3. Fallback: match by trailing turn ordinal (mounted DOM elements align with latest turns)
|
|
342
|
+
if (!matchedMsg && typeof domTurn.domIndex === "number") {
|
|
343
|
+
const offset = userMessages.length - domTurn.totalDom;
|
|
344
|
+
const targetIndex =
|
|
345
|
+
offset >= 0 ? offset + domTurn.domIndex : domTurn.domIndex;
|
|
346
|
+
matchedMsg = userMessages[targetIndex];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (matchedMsg) {
|
|
350
|
+
const attachBlock =
|
|
351
|
+
`\n\n**Attachments:**\n` +
|
|
352
|
+
domTurn.fileNames.map((fn) => `- ${fn}`).join("\n");
|
|
353
|
+
if (!matchedMsg.content.includes("**Attachments:**")) {
|
|
354
|
+
matchedMsg.content = matchedMsg.content
|
|
355
|
+
? `${matchedMsg.content}${attachBlock}`
|
|
356
|
+
: `**Attachments:**\n` +
|
|
357
|
+
domTurn.fileNames.map((fn) => `- ${fn}`).join("\n");
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
} catch (e) {
|
|
362
|
+
console.warn(
|
|
363
|
+
"[Gemini Parser] Failed to enrich messages with DOM attachments:",
|
|
364
|
+
e,
|
|
365
|
+
);
|
|
366
|
+
} finally {
|
|
367
|
+
// Clean internal turnId tracking before returning messages
|
|
368
|
+
for (const m of messages) {
|
|
369
|
+
delete m.turnId;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return messages;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
formatApiResult(allItems, currentUrl, options = {}) {
|
|
377
|
+
if (!Array.isArray(allItems) || allItems.length === 0) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
244
381
|
const messages = this.convertApiItemsToMessages(allItems, options);
|
|
382
|
+
this.enrichMessagesWithDomAttachments(messages);
|
|
383
|
+
|
|
245
384
|
let title = this.extractTitleFromPage();
|
|
246
385
|
if (!title || title === "Gemini Conversation") {
|
|
247
386
|
const firstUserMsg = messages.find((m) => m.role === "User");
|
|
@@ -296,17 +435,28 @@ export class GeminiParser extends ChatParser {
|
|
|
296
435
|
return null;
|
|
297
436
|
}
|
|
298
437
|
|
|
438
|
+
extractTurnId(item) {
|
|
439
|
+
try {
|
|
440
|
+
const raw = item[0]?.[1] || item[1]?.[1] || "";
|
|
441
|
+
return String(raw).replace(/^r_/, "");
|
|
442
|
+
} catch {
|
|
443
|
+
return "";
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
299
447
|
convertApiItemsToMessages(items, options = {}) {
|
|
300
448
|
const messages = [];
|
|
301
449
|
|
|
302
450
|
for (const item of items) {
|
|
303
451
|
if (!Array.isArray(item)) continue;
|
|
304
452
|
|
|
453
|
+
const turnId = this.extractTurnId(item);
|
|
305
454
|
const userText = this.findUserTextInApiItem(item);
|
|
306
455
|
if (userText) {
|
|
307
456
|
messages.push({
|
|
308
457
|
role: "User",
|
|
309
458
|
content: userText.trim(),
|
|
459
|
+
turnId,
|
|
310
460
|
});
|
|
311
461
|
}
|
|
312
462
|
|
|
@@ -314,7 +464,8 @@ export class GeminiParser extends ChatParser {
|
|
|
314
464
|
if (modelText) {
|
|
315
465
|
messages.push({
|
|
316
466
|
role: "Model",
|
|
317
|
-
content: modelText.trim(),
|
|
467
|
+
content: normalizeLatexMath(modelText.trim()),
|
|
468
|
+
turnId,
|
|
318
469
|
});
|
|
319
470
|
}
|
|
320
471
|
}
|
|
@@ -324,6 +475,7 @@ export class GeminiParser extends ChatParser {
|
|
|
324
475
|
|
|
325
476
|
findUserTextInApiItem(item) {
|
|
326
477
|
try {
|
|
478
|
+
if (typeof item[2]?.[0]?.[0] === "string") return item[2][0][0];
|
|
327
479
|
if (typeof item[2]?.[0] === "string") return item[2][0];
|
|
328
480
|
if (typeof item[1]?.[0] === "string" && !Array.isArray(item[1][0]))
|
|
329
481
|
return item[1][0];
|
|
@@ -336,6 +488,25 @@ export class GeminiParser extends ChatParser {
|
|
|
336
488
|
|
|
337
489
|
findModelTextInApiItem(item) {
|
|
338
490
|
try {
|
|
491
|
+
// 1. Candidate responses in item[3]
|
|
492
|
+
if (Array.isArray(item[3])) {
|
|
493
|
+
const candidates = Array.isArray(item[3][0]) ? item[3][0] : item[3];
|
|
494
|
+
for (const cand of candidates) {
|
|
495
|
+
if (!Array.isArray(cand)) continue;
|
|
496
|
+
// Shape: ["rc_...", ["markdown text", ...], ...]
|
|
497
|
+
if (Array.isArray(cand[1]) && typeof cand[1][0] === "string") {
|
|
498
|
+
return cand[1][0];
|
|
499
|
+
}
|
|
500
|
+
if (typeof cand[1] === "string") {
|
|
501
|
+
return cand[1];
|
|
502
|
+
}
|
|
503
|
+
if (typeof cand[0] === "string" && cand[0].length > 50) {
|
|
504
|
+
return cand[0];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// 2. Fallback candidate in item[1]
|
|
339
510
|
if (Array.isArray(item[1])) {
|
|
340
511
|
const candidate = item[1][0];
|
|
341
512
|
if (typeof candidate === "string") return candidate;
|
package/ai/perplexity.js
CHANGED
|
@@ -1,5 +1,191 @@
|
|
|
1
1
|
import { ChatParser } from "./base.js";
|
|
2
2
|
import { convertToMarkdown } from "../utils/html-to-markdown.js";
|
|
3
|
+
import { normalizeLatexMath } from "../utils/latex-math.js";
|
|
4
|
+
|
|
5
|
+
export function getThreadSlug(url) {
|
|
6
|
+
if (!url || typeof url !== "string") return null;
|
|
7
|
+
const match = url.match(/\/search\/([a-zA-Z0-9_-]+)/);
|
|
8
|
+
if (match) return match[1];
|
|
9
|
+
const matchPage = url.match(/\/page\/([a-zA-Z0-9_-]+)/);
|
|
10
|
+
if (matchPage) return matchPage[1];
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function extractAssistantAnswer(entry) {
|
|
15
|
+
if (!entry) return "";
|
|
16
|
+
// 1. Check blocks -> plan_block.goals for markdown answer
|
|
17
|
+
if (Array.isArray(entry.blocks)) {
|
|
18
|
+
for (const b of entry.blocks) {
|
|
19
|
+
if (b.intended_usage === "plan" && b.plan_block?.goals) {
|
|
20
|
+
for (const g of b.plan_block.goals) {
|
|
21
|
+
if (g.description && g.description.length > 100) {
|
|
22
|
+
return g.description.trim();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (b.answer_block?.answer) {
|
|
27
|
+
return b.answer_block.answer.trim();
|
|
28
|
+
}
|
|
29
|
+
if (b.markdown_block?.markdown) {
|
|
30
|
+
return b.markdown_block.markdown.trim();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// 2. Check direct answer or text fields
|
|
35
|
+
if (entry.answer && typeof entry.answer === "string") {
|
|
36
|
+
return entry.answer.trim();
|
|
37
|
+
}
|
|
38
|
+
if (entry.text && typeof entry.text === "string") {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(entry.text);
|
|
41
|
+
if (parsed.answer) return parsed.answer.trim();
|
|
42
|
+
} catch {
|
|
43
|
+
return entry.text.trim();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatApiResult(threadData, currentUrl, fallbackTitle) {
|
|
50
|
+
const title =
|
|
51
|
+
threadData.thread_title ||
|
|
52
|
+
threadData.first_entry?.query_str ||
|
|
53
|
+
fallbackTitle ||
|
|
54
|
+
"Perplexity Search";
|
|
55
|
+
|
|
56
|
+
const allEntries = [];
|
|
57
|
+
if (threadData.first_entry) {
|
|
58
|
+
allEntries.push(threadData.first_entry);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(threadData.entries)) {
|
|
61
|
+
allEntries.push(...threadData.entries);
|
|
62
|
+
}
|
|
63
|
+
if (threadData.latest_entry) {
|
|
64
|
+
allEntries.push(threadData.latest_entry);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Deduplicate entries by uuid / backend_uuid
|
|
68
|
+
const seenUuids = new Set();
|
|
69
|
+
const uniqueEntries = [];
|
|
70
|
+
for (const entry of allEntries) {
|
|
71
|
+
const id = entry.uuid || entry.backend_uuid || entry.query_str;
|
|
72
|
+
if (id && !seenUuids.has(id)) {
|
|
73
|
+
seenUuids.add(id);
|
|
74
|
+
uniqueEntries.push(entry);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const messages = [];
|
|
79
|
+
for (const entry of uniqueEntries) {
|
|
80
|
+
const prompt = (entry.query_str || "").trim();
|
|
81
|
+
const timestamp =
|
|
82
|
+
entry.entry_created_datetime || entry.updated_datetime || undefined;
|
|
83
|
+
|
|
84
|
+
if (prompt) {
|
|
85
|
+
const userMsg = { role: "User", content: prompt };
|
|
86
|
+
if (timestamp) userMsg.timestamp = timestamp;
|
|
87
|
+
messages.push(userMsg);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rawAnswer = extractAssistantAnswer(entry);
|
|
91
|
+
if (rawAnswer) {
|
|
92
|
+
const normalizedAnswer = normalizeLatexMath(rawAnswer);
|
|
93
|
+
const assistantMsg = { role: "Perplexity", content: normalizedAnswer };
|
|
94
|
+
if (timestamp) assistantMsg.timestamp = timestamp;
|
|
95
|
+
messages.push(assistantMsg);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const effectiveUrl =
|
|
100
|
+
currentUrl ||
|
|
101
|
+
(threadData.first_entry?.thread_url_slug
|
|
102
|
+
? `https://www.perplexity.ai/search/${threadData.first_entry.thread_url_slug}`
|
|
103
|
+
: "");
|
|
104
|
+
|
|
105
|
+
const metadata = {
|
|
106
|
+
Source: "Perplexity",
|
|
107
|
+
Date: new Date().toLocaleString(),
|
|
108
|
+
Link: effectiveUrl,
|
|
109
|
+
Method: "API",
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return { title, messages, url: effectiveUrl, metadata };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function getPerplexityAccount() {
|
|
116
|
+
if (typeof window === "undefined") return null;
|
|
117
|
+
|
|
118
|
+
const isUuid = (str) =>
|
|
119
|
+
typeof str === "string" &&
|
|
120
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
|
|
121
|
+
str.trim(),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// 1. Check sessionStorage ('pplx-active-account')
|
|
125
|
+
try {
|
|
126
|
+
const sessionAcc = window.sessionStorage?.getItem("pplx-active-account");
|
|
127
|
+
if (sessionAcc && isUuid(sessionAcc)) return sessionAcc.trim();
|
|
128
|
+
} catch {
|
|
129
|
+
// Ignore storage errors
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 2. Check localStorage ('pplx-last-active-account')
|
|
133
|
+
try {
|
|
134
|
+
const localAcc = window.localStorage?.getItem("pplx-last-active-account");
|
|
135
|
+
if (localAcc && isUuid(localAcc)) return localAcc.trim();
|
|
136
|
+
} catch {
|
|
137
|
+
// Ignore storage errors
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 3. Scan keys in sessionStorage and localStorage for pplx account UUIDs
|
|
141
|
+
try {
|
|
142
|
+
for (const storage of [window.sessionStorage, window.localStorage]) {
|
|
143
|
+
if (!storage) continue;
|
|
144
|
+
for (let i = 0; i < storage.length; i++) {
|
|
145
|
+
const key = storage.key(i);
|
|
146
|
+
if (key && (key.includes("pplx") || key.includes("account"))) {
|
|
147
|
+
const val = storage.getItem(key);
|
|
148
|
+
if (val && isUuid(val)) return val.trim();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore storage scanning errors
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 4. Check document.cookie if available
|
|
157
|
+
try {
|
|
158
|
+
const match = document.cookie?.match(
|
|
159
|
+
/(?:pplx-last-active-account|pplx-active-account)=([0-9a-f-]{36})/i,
|
|
160
|
+
);
|
|
161
|
+
if (match && isUuid(match[1])) return match[1].trim();
|
|
162
|
+
} catch {
|
|
163
|
+
// Ignore cookie errors
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 5. Fallback: query https://www.perplexity.ai/api/auth/linked-accounts using session cookies
|
|
167
|
+
try {
|
|
168
|
+
if (typeof fetch === "function") {
|
|
169
|
+
const res = await fetch(
|
|
170
|
+
"https://www.perplexity.ai/api/auth/linked-accounts",
|
|
171
|
+
{
|
|
172
|
+
method: "GET",
|
|
173
|
+
credentials: "include",
|
|
174
|
+
headers: { Accept: "application/json" },
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
if (res.ok) {
|
|
178
|
+
const data = await res.json();
|
|
179
|
+
const accountId = data?.accounts?.[0]?.user_id;
|
|
180
|
+
if (accountId && isUuid(accountId)) return accountId.trim();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Ignore linked-accounts network error
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
3
189
|
|
|
4
190
|
export class PerplexityParser extends ChatParser {
|
|
5
191
|
name = "Perplexity";
|
|
@@ -7,7 +193,38 @@ export class PerplexityParser extends ChatParser {
|
|
|
7
193
|
return url.includes("perplexity.ai");
|
|
8
194
|
}
|
|
9
195
|
|
|
10
|
-
async
|
|
196
|
+
async fetchThread(slug) {
|
|
197
|
+
const accountId = await getPerplexityAccount();
|
|
198
|
+
const url = `https://www.perplexity.ai/rest/thread/${slug}?with_parent_info=true&with_schematized_response=true&version=2.18&source=default&limit=100&offset=0&from_first=true`;
|
|
199
|
+
const headers = {
|
|
200
|
+
Accept: "application/json",
|
|
201
|
+
"x-app-apiclient": "default",
|
|
202
|
+
"x-app-apiversion": "2.18",
|
|
203
|
+
};
|
|
204
|
+
if (accountId) {
|
|
205
|
+
headers["x-pplx-account"] = accountId;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const response = await fetch(url, {
|
|
209
|
+
method: "GET",
|
|
210
|
+
credentials: "include",
|
|
211
|
+
headers,
|
|
212
|
+
});
|
|
213
|
+
if (!response.ok) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Perplexity API request failed with status ${response.status}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return response.json();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async parse(options = {}) {
|
|
222
|
+
const parserMode = options.parserMode || "prefer_api";
|
|
223
|
+
const currentUrl =
|
|
224
|
+
typeof window !== "undefined" && window.location
|
|
225
|
+
? window.location.href || ""
|
|
226
|
+
: "";
|
|
227
|
+
|
|
11
228
|
const rawTitle =
|
|
12
229
|
document.querySelector(".share-title-section h1")?.textContent ||
|
|
13
230
|
document.querySelector("h1")?.textContent ||
|
|
@@ -15,6 +232,25 @@ export class PerplexityParser extends ChatParser {
|
|
|
15
232
|
"Perplexity Search";
|
|
16
233
|
const title = rawTitle.trim().replace(/\s+/g, " ");
|
|
17
234
|
|
|
235
|
+
// 1. Attempt API-first extraction when in prefer_api mode
|
|
236
|
+
if (parserMode !== "prefer_dom") {
|
|
237
|
+
const slug = getThreadSlug(currentUrl);
|
|
238
|
+
if (slug) {
|
|
239
|
+
try {
|
|
240
|
+
const apiData = await this.fetchThread(slug);
|
|
241
|
+
if (apiData && (apiData.entries?.length || apiData.first_entry)) {
|
|
242
|
+
return formatApiResult(apiData, currentUrl, title);
|
|
243
|
+
}
|
|
244
|
+
} catch (err) {
|
|
245
|
+
console.warn(
|
|
246
|
+
"[AI Exporter] Perplexity API fetch failed, falling back to DOM:",
|
|
247
|
+
err,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 2. DOM extraction fallback
|
|
18
254
|
const messages = [];
|
|
19
255
|
|
|
20
256
|
// Perplexity Container
|
|
@@ -23,6 +259,7 @@ export class PerplexityParser extends ChatParser {
|
|
|
23
259
|
|
|
24
260
|
// Candidates for User Messages
|
|
25
261
|
const userSelectors = [
|
|
262
|
+
".group\\/user-bubble",
|
|
26
263
|
"h1.group\\/query",
|
|
27
264
|
".group\\/query",
|
|
28
265
|
".whitespace-pre-line.text-pretty",
|
|
@@ -40,43 +277,58 @@ export class PerplexityParser extends ChatParser {
|
|
|
40
277
|
const elements = threadContainer.querySelectorAll(selectorString);
|
|
41
278
|
|
|
42
279
|
// Helper to determine role
|
|
43
|
-
const
|
|
280
|
+
const isUser = (el) => {
|
|
44
281
|
for (const s of userSelectors) {
|
|
45
|
-
if (el.matches(s)) return
|
|
282
|
+
if (el.matches(s)) return true;
|
|
46
283
|
}
|
|
284
|
+
return false;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const isAssistant = (el) => {
|
|
47
288
|
for (const s of assistantSelectors) {
|
|
48
|
-
if (el.matches(s)) return
|
|
289
|
+
if (el.matches(s)) return true;
|
|
49
290
|
}
|
|
50
|
-
return
|
|
291
|
+
return false;
|
|
51
292
|
};
|
|
52
293
|
|
|
53
|
-
const seenText = new Set();
|
|
54
|
-
|
|
55
294
|
elements.forEach((el) => {
|
|
56
|
-
// Perplexity nests things. Avoid duplicates if we selected a parent and a child.
|
|
57
|
-
// Also avoid "Related" section queries if possible (usually separate container, check parents?)
|
|
58
|
-
|
|
59
295
|
// Check if inside "related" or "sources"
|
|
60
296
|
if (el.closest('[class*="related"], [class*="sources"]')) return;
|
|
61
297
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
298
|
+
if (isUser(el)) {
|
|
299
|
+
if (el.parentElement?.closest(".group\\/user-bubble, .group\\/query")) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Extract timestamp if present in bubble
|
|
304
|
+
const timeEl = el.querySelector(
|
|
305
|
+
".text-tertiary, [class*='text-tertiary']",
|
|
306
|
+
);
|
|
307
|
+
const timestamp = timeEl ? timeEl.textContent.trim() : undefined;
|
|
65
308
|
|
|
66
|
-
|
|
309
|
+
// Clean user text container
|
|
310
|
+
const textEl = el.querySelector(".whitespace-pre-line") || el;
|
|
311
|
+
const text = convertToMarkdown(textEl).trim();
|
|
312
|
+
if (!text) return;
|
|
67
313
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
314
|
+
const msgObj = { role: "User", content: text };
|
|
315
|
+
if (timestamp) {
|
|
316
|
+
msgObj.timestamp = timestamp;
|
|
317
|
+
}
|
|
318
|
+
messages.push(msgObj);
|
|
319
|
+
} else if (isAssistant(el)) {
|
|
320
|
+
// Avoid duplicate nested assistant containers (e.g. .prose inside div[id^="markdown-content-"])
|
|
321
|
+
if (el.parentElement?.closest('div[id^="markdown-content-"], .prose')) {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
71
324
|
|
|
72
|
-
|
|
73
|
-
|
|
325
|
+
const text = convertToMarkdown(el).trim();
|
|
326
|
+
if (!text) return;
|
|
327
|
+
|
|
328
|
+
messages.push({ role: "Perplexity", content: text });
|
|
329
|
+
}
|
|
74
330
|
});
|
|
75
331
|
|
|
76
|
-
const currentUrl =
|
|
77
|
-
typeof window !== "undefined" && window.location
|
|
78
|
-
? window.location.href || ""
|
|
79
|
-
: "";
|
|
80
332
|
const metadata = {
|
|
81
333
|
Source: "Perplexity",
|
|
82
334
|
Date: new Date().toLocaleString(),
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decant-core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "Shared AI chat platform parsers and detection for Covai browser extensions.",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=26.0.0"
|
|
8
|
+
},
|
|
6
9
|
"main": "ai/index.js",
|
|
7
10
|
"exports": {
|
|
8
11
|
".": "./ai/index.js",
|