|
|
|
@@ -14,6 +14,26 @@ const KIMI_INCOMPLETE_ANSWER_GRACE_MS = 18_000;
|
|
|
|
|
const KIMI_EDITOR_SELECTOR = "div[role=\"textbox\"].chat-input-editor, .chat-input-editor[role=\"textbox\"], [role=\"textbox\"][class*=\"chat-input-editor\"]";
|
|
|
|
|
const KIMI_SEND_BUTTON_SELECTOR = ".send-button-container";
|
|
|
|
|
const KIMI_THINKING_MODE_LABEL = "K2.6 思考";
|
|
|
|
|
const KIMI_REDIRECT_QUERY_KEYS = [
|
|
|
|
|
"url",
|
|
|
|
|
"u",
|
|
|
|
|
"target",
|
|
|
|
|
"targetUrl",
|
|
|
|
|
"target_url",
|
|
|
|
|
"link",
|
|
|
|
|
"href",
|
|
|
|
|
"sourceUrl",
|
|
|
|
|
"source_url",
|
|
|
|
|
"originUrl",
|
|
|
|
|
"origin_url",
|
|
|
|
|
"realUrl",
|
|
|
|
|
"real_url",
|
|
|
|
|
"redirect",
|
|
|
|
|
"redirect_url",
|
|
|
|
|
"jump",
|
|
|
|
|
"jump_url",
|
|
|
|
|
"to",
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
type KimiDomLink = {
|
|
|
|
|
url: string;
|
|
|
|
@@ -45,6 +65,13 @@ type KimiPageSnapshot = {
|
|
|
|
|
signature: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type KimiClassifiedSources = {
|
|
|
|
|
citations: MonitoringSourceItem[];
|
|
|
|
|
searchResults: MonitoringSourceItem[];
|
|
|
|
|
inlineCitationCandidates: MonitoringSourceItem[];
|
|
|
|
|
panelCitations: MonitoringSourceItem[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type KimiModeSwitchResult =
|
|
|
|
|
| {
|
|
|
|
|
ok: true;
|
|
|
|
@@ -87,19 +114,85 @@ function normalizeOptionalString(value: unknown): string | null {
|
|
|
|
|
return trimmed ? trimmed : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function decodeUrlCandidate(value: string): string {
|
|
|
|
|
let decoded = value.trim();
|
|
|
|
|
for (let index = 0; index < 3; index += 1) {
|
|
|
|
|
try {
|
|
|
|
|
const next = decodeURIComponent(decoded);
|
|
|
|
|
if (next === decoded) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
decoded = next;
|
|
|
|
|
} catch {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return decoded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function coercePotentialUrl(value: string): string {
|
|
|
|
|
const trimmed = decodeUrlCandidate(value);
|
|
|
|
|
if (/^\/\//.test(trimmed)) {
|
|
|
|
|
return `https:${trimmed}`;
|
|
|
|
|
}
|
|
|
|
|
if (/^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(trimmed)) {
|
|
|
|
|
return `https://${trimmed}`;
|
|
|
|
|
}
|
|
|
|
|
return trimmed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeAbsoluteUrl(value: string): string | null {
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(coercePotentialUrl(value));
|
|
|
|
|
url.hash = "";
|
|
|
|
|
return url.toString();
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function extractRedirectTargetUrl(value: string): string | null {
|
|
|
|
|
const direct = normalizeAbsoluteUrl(value);
|
|
|
|
|
if (!direct) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(direct);
|
|
|
|
|
for (const key of KIMI_REDIRECT_QUERY_KEYS) {
|
|
|
|
|
const raw = url.searchParams.get(key);
|
|
|
|
|
if (!raw) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const normalized = normalizeAbsoluteUrl(raw);
|
|
|
|
|
if (normalized) {
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const encodedMatch = direct.match(/https?%3A%2F%2F[^&\s"'<>]+/i);
|
|
|
|
|
if (encodedMatch) {
|
|
|
|
|
return normalizeAbsoluteUrl(encodedMatch[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const plainMatch = direct.match(/https?:\/\/[^&\s"'<>]+/i);
|
|
|
|
|
if (plainMatch && plainMatch[0] !== direct) {
|
|
|
|
|
return normalizeAbsoluteUrl(plainMatch[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeUrl(value: unknown): string | null {
|
|
|
|
|
const input = normalizeText(value);
|
|
|
|
|
if (!input) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(input);
|
|
|
|
|
url.hash = "";
|
|
|
|
|
return url.toString();
|
|
|
|
|
} catch {
|
|
|
|
|
return input;
|
|
|
|
|
}
|
|
|
|
|
return extractRedirectTargetUrl(input) ?? normalizeAbsoluteUrl(input);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSourceItem(source: KimiDomLink): MonitoringSourceItem | null {
|
|
|
|
@@ -152,6 +245,99 @@ function dedupeSourceItems(items: MonitoringSourceItem[]): MonitoringSourceItem[
|
|
|
|
|
return Array.from(keyed.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dedupeKimiDomLinks(items: KimiDomLink[]): KimiDomLink[] {
|
|
|
|
|
const keyed = new Map<string, KimiDomLink>();
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
const key = normalizeUrl(item.url);
|
|
|
|
|
if (!key) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existing = keyed.get(key);
|
|
|
|
|
if (!existing) {
|
|
|
|
|
keyed.set(key, {
|
|
|
|
|
...item,
|
|
|
|
|
url: key,
|
|
|
|
|
});
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
keyed.set(key, {
|
|
|
|
|
url: key,
|
|
|
|
|
title: existing.title ?? item.title ?? null,
|
|
|
|
|
text: existing.text ?? item.text ?? null,
|
|
|
|
|
siteName: existing.siteName ?? item.siteName ?? null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return Array.from(keyed.values());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeKimiDomLinks(left: KimiDomLink[], right: KimiDomLink[]): KimiDomLink[] {
|
|
|
|
|
return dedupeKimiDomLinks([...left, ...right]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeKimiSnapshots(base: KimiPageSnapshot, extra: KimiPageSnapshot): KimiPageSnapshot {
|
|
|
|
|
const citationLinks = mergeKimiDomLinks(base.citationLinks, extra.citationLinks);
|
|
|
|
|
const panelCitationLinks = mergeKimiDomLinks(base.panelCitationLinks, extra.panelCitationLinks);
|
|
|
|
|
const inlineCitationCandidateLinks = mergeKimiDomLinks(
|
|
|
|
|
base.inlineCitationCandidateLinks,
|
|
|
|
|
extra.inlineCitationCandidateLinks,
|
|
|
|
|
);
|
|
|
|
|
const searchResultLinks = mergeKimiDomLinks(base.searchResultLinks, extra.searchResultLinks);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
citationLinks,
|
|
|
|
|
panelCitationLinks,
|
|
|
|
|
inlineCitationCandidateLinks,
|
|
|
|
|
searchResultLinks,
|
|
|
|
|
signature: [
|
|
|
|
|
base.signature,
|
|
|
|
|
extra.signature,
|
|
|
|
|
...citationLinks.map((item) => item.url),
|
|
|
|
|
...searchResultLinks.map((item) => item.url),
|
|
|
|
|
].filter(Boolean).join("\n").slice(-6_000) || base.signature || extra.signature,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mergeKimiSourceLinksIntoContentSnapshot(
|
|
|
|
|
contentSnapshot: KimiPageSnapshot,
|
|
|
|
|
sourceSnapshot: KimiPageSnapshot,
|
|
|
|
|
): KimiPageSnapshot {
|
|
|
|
|
return mergeKimiSnapshots(contentSnapshot, sourceSnapshot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function classifyKimiSources(snapshot: KimiPageSnapshot): KimiClassifiedSources {
|
|
|
|
|
const searchResults = dedupeSourceItems(
|
|
|
|
|
snapshot.searchResultLinks
|
|
|
|
|
.map((item) => buildSourceItem(item))
|
|
|
|
|
.filter((item): item is MonitoringSourceItem => item !== null),
|
|
|
|
|
);
|
|
|
|
|
const panelCitations = dedupeSourceItems(
|
|
|
|
|
snapshot.panelCitationLinks
|
|
|
|
|
.map((item) => buildSourceItem(item))
|
|
|
|
|
.filter((item): item is MonitoringSourceItem => item !== null),
|
|
|
|
|
);
|
|
|
|
|
const inlineAnswerCitations = dedupeSourceItems(
|
|
|
|
|
snapshot.inlineCitationCandidateLinks
|
|
|
|
|
.map((item) => buildSourceItem(item))
|
|
|
|
|
.filter((item): item is MonitoringSourceItem => item !== null),
|
|
|
|
|
);
|
|
|
|
|
const inlineCitationCandidates = dedupeSourceItems([
|
|
|
|
|
...panelCitations,
|
|
|
|
|
...inlineAnswerCitations,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
// Kimi's citation-source list is the "搜索网页" result set. The footer "引用" panel
|
|
|
|
|
// is kept as inline/content citation metadata instead.
|
|
|
|
|
citations: searchResults.length ? searchResults : panelCitations,
|
|
|
|
|
searchResults,
|
|
|
|
|
inlineCitationCandidates,
|
|
|
|
|
panelCitations,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toJsonValue(value: unknown, depth = 0): JsonValue {
|
|
|
|
|
if (value == null || depth > 12) {
|
|
|
|
|
return null;
|
|
|
|
@@ -405,6 +591,231 @@ async function ensureKimiCitationPanelOpen(page: PlaywrightPage, signal?: AbortS
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function ensureKimiSearchPanelOpen(page: PlaywrightPage, signal?: AbortSignal): Promise<void> {
|
|
|
|
|
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
|
|
|
const state = await page.evaluate(() => {
|
|
|
|
|
const normalize = (value: unknown): string | null => {
|
|
|
|
|
if (typeof value !== "string") {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const trimmed = value
|
|
|
|
|
.replace(/\u00a0/g, " ")
|
|
|
|
|
.split(/\n+/)
|
|
|
|
|
.map((line) => line.trim())
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join(" ");
|
|
|
|
|
return trimmed ? trimmed : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isVisible = (element: Element | null | undefined): element is HTMLElement => {
|
|
|
|
|
if (!(element instanceof HTMLElement)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const style = window.getComputedStyle(element);
|
|
|
|
|
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
return rect.width > 0 && rect.height > 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const panelReady = Array.from(document.querySelectorAll("div, section, aside"))
|
|
|
|
|
.some((element) => {
|
|
|
|
|
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const text = normalize(element.innerText) ?? "";
|
|
|
|
|
return /^搜索网页\s+\d+/m.test(text) && element.querySelectorAll("a[href], [data-url], [data-href]").length > 0;
|
|
|
|
|
});
|
|
|
|
|
if (panelReady) {
|
|
|
|
|
return {
|
|
|
|
|
ready: true,
|
|
|
|
|
clicked: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const candidates = Array.from(document.querySelectorAll("button, [role=\"button\"], a, [tabindex], div, li, span"))
|
|
|
|
|
.filter((element): element is HTMLElement => isVisible(element))
|
|
|
|
|
.map((element) => {
|
|
|
|
|
const text = normalize(element.innerText || element.textContent || "") ?? "";
|
|
|
|
|
if (!/搜索网页/.test(text) || !/结果/.test(text)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const target = element.closest("button, [role=\"button\"], a, [tabindex], li, [class*=\"search\"], [class*=\"Search\"], [class*=\"web\"], [class*=\"Web\"]") ?? element;
|
|
|
|
|
if (!(target instanceof HTMLElement) || !isVisible(target)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const rect = target.getBoundingClientRect();
|
|
|
|
|
const className = String(target.className || "");
|
|
|
|
|
const inSideConsole = Boolean(target.closest(".side-console, .side-console-container"));
|
|
|
|
|
let score = 0;
|
|
|
|
|
if (/^\s*搜索网页/.test(text)) {
|
|
|
|
|
score += 120;
|
|
|
|
|
}
|
|
|
|
|
if (/\d+\s*个?结果/.test(text)) {
|
|
|
|
|
score += 100;
|
|
|
|
|
}
|
|
|
|
|
if (target.matches("button, [role=\"button\"], a, [tabindex]")) {
|
|
|
|
|
score += 60;
|
|
|
|
|
}
|
|
|
|
|
if (/(search|web|result|思考|research|ole)/i.test(className)) {
|
|
|
|
|
score += 30;
|
|
|
|
|
}
|
|
|
|
|
if (inSideConsole) {
|
|
|
|
|
score -= 200;
|
|
|
|
|
}
|
|
|
|
|
score -= Math.min(rect.width * rect.height / 20_000, 60);
|
|
|
|
|
return {
|
|
|
|
|
target,
|
|
|
|
|
score,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((item): item is { target: HTMLElement; score: number } => Boolean(item))
|
|
|
|
|
.sort((left, right) => right.score - left.score);
|
|
|
|
|
|
|
|
|
|
const seen = new Set<HTMLElement>();
|
|
|
|
|
const trigger = candidates
|
|
|
|
|
.map((item) => item.target)
|
|
|
|
|
.find((target) => {
|
|
|
|
|
if (seen.has(target)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
seen.add(target);
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
if (!trigger) {
|
|
|
|
|
return {
|
|
|
|
|
ready: false,
|
|
|
|
|
clicked: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trigger.scrollIntoView({ block: "center", inline: "center" });
|
|
|
|
|
trigger.dispatchEvent(new MouseEvent("mouseover", { bubbles: true }));
|
|
|
|
|
trigger.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, cancelable: true }));
|
|
|
|
|
trigger.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, cancelable: true }));
|
|
|
|
|
trigger.click();
|
|
|
|
|
return {
|
|
|
|
|
ready: false,
|
|
|
|
|
clicked: true,
|
|
|
|
|
};
|
|
|
|
|
}).catch(() => ({
|
|
|
|
|
ready: false,
|
|
|
|
|
clicked: false,
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
if (state.ready) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!state.clicked && attempt > 0) {
|
|
|
|
|
await pinKimiConversationToBottom(page);
|
|
|
|
|
}
|
|
|
|
|
await sleep(450, signal).catch(() => undefined);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function scrollKimiSearchPanel(page: PlaywrightPage): Promise<{ atBottom: boolean; moved: boolean }> {
|
|
|
|
|
return page.evaluate(() => {
|
|
|
|
|
const normalize = (value: unknown): string | null => {
|
|
|
|
|
if (typeof value !== "string") {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const trimmed = value
|
|
|
|
|
.replace(/\u00a0/g, " ")
|
|
|
|
|
.split(/\n+/)
|
|
|
|
|
.map((line) => line.trim())
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
.join("\n");
|
|
|
|
|
return trimmed ? trimmed : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isVisible = (element: Element | null | undefined): element is HTMLElement => {
|
|
|
|
|
if (!(element instanceof HTMLElement)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const style = window.getComputedStyle(element);
|
|
|
|
|
if (style.display === "none" || style.visibility === "hidden" || style.opacity === "0") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
return rect.width > 0 && rect.height > 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const candidates = Array.from(document.querySelectorAll("div, section, aside"))
|
|
|
|
|
.filter((element): element is HTMLElement => {
|
|
|
|
|
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
const text = normalize(element.innerText) ?? "";
|
|
|
|
|
return text.includes("搜索网页") && element.scrollHeight - element.clientHeight > 32;
|
|
|
|
|
})
|
|
|
|
|
.sort((left, right) => {
|
|
|
|
|
const leftScrollable = left.scrollHeight - left.clientHeight;
|
|
|
|
|
const rightScrollable = right.scrollHeight - right.clientHeight;
|
|
|
|
|
return rightScrollable - leftScrollable;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const panel = candidates[0] ?? null;
|
|
|
|
|
if (!panel) {
|
|
|
|
|
return {
|
|
|
|
|
atBottom: true,
|
|
|
|
|
moved: false,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const before = panel.scrollTop;
|
|
|
|
|
const maxScrollTop = Math.max(panel.scrollHeight - panel.clientHeight, 0);
|
|
|
|
|
panel.scrollTop = Math.min(maxScrollTop, panel.scrollTop + Math.max(panel.clientHeight - 96, 420));
|
|
|
|
|
return {
|
|
|
|
|
atBottom: panel.scrollTop >= maxScrollTop - 2,
|
|
|
|
|
moved: panel.scrollTop !== before,
|
|
|
|
|
};
|
|
|
|
|
}).catch(() => ({
|
|
|
|
|
atBottom: true,
|
|
|
|
|
moved: false,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function collectKimiSearchPanelLinks(
|
|
|
|
|
page: PlaywrightPage,
|
|
|
|
|
questionText: string,
|
|
|
|
|
signal: AbortSignal,
|
|
|
|
|
): Promise<KimiDomLink[]> {
|
|
|
|
|
await ensureKimiSearchPanelOpen(page, signal);
|
|
|
|
|
|
|
|
|
|
let collected: KimiDomLink[] = [];
|
|
|
|
|
let stalledPolls = 0;
|
|
|
|
|
for (let index = 0; index < 80; index += 1) {
|
|
|
|
|
if (signal.aborted) {
|
|
|
|
|
throw new Error("adapter_aborted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const snapshot = await readKimiPageSnapshot(page, questionText);
|
|
|
|
|
const beforeCount = collected.length;
|
|
|
|
|
collected = mergeKimiDomLinks(collected, snapshot.searchResultLinks);
|
|
|
|
|
const state = await scrollKimiSearchPanel(page);
|
|
|
|
|
|
|
|
|
|
if (collected.length === beforeCount) {
|
|
|
|
|
stalledPolls += 1;
|
|
|
|
|
} else {
|
|
|
|
|
stalledPolls = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state.atBottom && stalledPolls >= 2) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (!state.moved && stalledPolls >= 3) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await sleep(180, signal).catch(() => undefined);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return collected;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function countMatches(input: string, pattern: RegExp): number {
|
|
|
|
|
const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`;
|
|
|
|
|
return input.match(new RegExp(pattern.source, flags))?.length ?? 0;
|
|
|
|
@@ -979,7 +1390,7 @@ async function ensureKimiThinkingMode(page: PlaywrightPage): Promise<KimiModeSwi
|
|
|
|
|
|
|
|
|
|
async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string): Promise<KimiPageSnapshot> {
|
|
|
|
|
return page.evaluate(
|
|
|
|
|
({ question }) => {
|
|
|
|
|
({ question, redirectQueryKeys }) => {
|
|
|
|
|
type CandidateKind = "answer" | "reasoning";
|
|
|
|
|
type Candidate = {
|
|
|
|
|
element: HTMLElement;
|
|
|
|
@@ -1062,6 +1473,27 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
return rect.width > 0 && rect.height > 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const isAuxiliaryPanelElement = (element: Element | null | undefined): boolean => {
|
|
|
|
|
let current: Element | null = element ?? null;
|
|
|
|
|
for (let depth = 0; current && depth < 10; depth += 1) {
|
|
|
|
|
const descriptor = [
|
|
|
|
|
current.className || "",
|
|
|
|
|
current.getAttribute("role") || "",
|
|
|
|
|
current.getAttribute("aria-label") || "",
|
|
|
|
|
current.getAttribute("data-testid") || "",
|
|
|
|
|
].join(" ");
|
|
|
|
|
const text = current instanceof HTMLElement ? normalizeInline(current.innerText)?.slice(0, 160) ?? "" : "";
|
|
|
|
|
if (
|
|
|
|
|
/(side-console|console|drawer|modal|popover|popup|tooltip|floating|hover-card|dropdown|search-result|search-panel|reference-panel|ref-panel|citation-panel)/i.test(descriptor)
|
|
|
|
|
|| /^(引用来源|搜索网页)\s*\d*/.test(text)
|
|
|
|
|
) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
current = current.parentElement;
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const loginSignals = [
|
|
|
|
|
"微信扫码登录",
|
|
|
|
|
"手机号快捷登录",
|
|
|
|
@@ -1143,19 +1575,84 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
return order;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const decodeUrlCandidate = (value: string): string => {
|
|
|
|
|
let decoded = value.trim();
|
|
|
|
|
for (let index = 0; index < 3; index += 1) {
|
|
|
|
|
try {
|
|
|
|
|
const next = decodeURIComponent(decoded);
|
|
|
|
|
if (next === decoded) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
decoded = next;
|
|
|
|
|
} catch {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return decoded;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const coercePotentialUrl = (value: string): string => {
|
|
|
|
|
const trimmed = decodeUrlCandidate(value);
|
|
|
|
|
if (/^\/\//.test(trimmed)) {
|
|
|
|
|
return `https:${trimmed}`;
|
|
|
|
|
}
|
|
|
|
|
if (/^[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:[/?#].*)?$/i.test(trimmed)) {
|
|
|
|
|
return `https://${trimmed}`;
|
|
|
|
|
}
|
|
|
|
|
return trimmed;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const normalizeAbsoluteHref = (value: string): string | null => {
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(coercePotentialUrl(value), window.location.href);
|
|
|
|
|
url.hash = "";
|
|
|
|
|
return url.toString();
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const extractRedirectTargetHref = (value: string): string | null => {
|
|
|
|
|
const direct = normalizeAbsoluteHref(value);
|
|
|
|
|
if (!direct) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(direct);
|
|
|
|
|
for (const key of redirectQueryKeys) {
|
|
|
|
|
const raw = url.searchParams.get(key);
|
|
|
|
|
if (!raw) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
const normalized = normalizeAbsoluteHref(raw);
|
|
|
|
|
if (normalized) {
|
|
|
|
|
return normalized;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const encodedMatch = direct.match(/https?%3A%2F%2F[^&\s"'<>]+/i);
|
|
|
|
|
if (encodedMatch) {
|
|
|
|
|
return normalizeAbsoluteHref(encodedMatch[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const normalizeHref = (value: string | null): string | null => {
|
|
|
|
|
const href = normalizeInline(value);
|
|
|
|
|
if (!href) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const url = new URL(href, window.location.href);
|
|
|
|
|
url.hash = "";
|
|
|
|
|
return url.toString();
|
|
|
|
|
} catch {
|
|
|
|
|
if (/^(?:javascript|data|blob|mailto|tel):/i.test(href)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return extractRedirectTargetHref(href) ?? normalizeAbsoluteHref(href);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const buildSyntheticSourceUrl = (siteName: string | null): string | null => {
|
|
|
|
@@ -1287,6 +1784,121 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const firstUrlFromText = (value: string | null): string | null => {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const httpMatch = value.match(/https?:\/\/[^\s"'<>,。;、]+/i);
|
|
|
|
|
if (httpMatch) {
|
|
|
|
|
return normalizeHref(httpMatch[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const domainMatch = value.match(/\b[a-z0-9-]+(?:\.[a-z0-9-]+)+(?:\/[^\s"'<>,。;、]*)?/i);
|
|
|
|
|
return domainMatch ? normalizeHref(domainMatch[0]) : null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const readElementUrl = (element: Element): string | null => {
|
|
|
|
|
const candidateAttributes = [
|
|
|
|
|
"href",
|
|
|
|
|
"data-href",
|
|
|
|
|
"data-url",
|
|
|
|
|
"data-link",
|
|
|
|
|
"data-target-url",
|
|
|
|
|
"data-source-url",
|
|
|
|
|
"data-origin-url",
|
|
|
|
|
"data-real-url",
|
|
|
|
|
"data-page-url",
|
|
|
|
|
"data-web-url",
|
|
|
|
|
"data-log",
|
|
|
|
|
"data-click",
|
|
|
|
|
"data-extra",
|
|
|
|
|
"data-info",
|
|
|
|
|
"data-value",
|
|
|
|
|
];
|
|
|
|
|
const candidates: string[] = [];
|
|
|
|
|
for (const name of candidateAttributes) {
|
|
|
|
|
const value = element.getAttribute(name);
|
|
|
|
|
if (value) {
|
|
|
|
|
candidates.push(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (element instanceof HTMLElement) {
|
|
|
|
|
candidates.push(...Object.values(element.dataset).filter((value): value is string => Boolean(value)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const candidate of candidates) {
|
|
|
|
|
const href = normalizeHref(candidate) ?? firstUrlFromText(candidate);
|
|
|
|
|
if (isExternalSourceUrl(href)) {
|
|
|
|
|
return href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const text = normalizeBlock(element.textContent ?? "");
|
|
|
|
|
if (text && text.length <= 2_000) {
|
|
|
|
|
const href = firstUrlFromText(text);
|
|
|
|
|
if (isExternalSourceUrl(href)) {
|
|
|
|
|
return href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const extractSearchResultLink = (element: Element): KimiDomLink | null => {
|
|
|
|
|
const anchorLink = extractExternalAnchorLink(element);
|
|
|
|
|
if (anchorLink) {
|
|
|
|
|
return anchorLink;
|
|
|
|
|
}
|
|
|
|
|
if (!(element instanceof HTMLElement) || !isVisible(element)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const href = readElementUrl(element);
|
|
|
|
|
if (!isExternalSourceUrl(href)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const card = element.closest([
|
|
|
|
|
"a",
|
|
|
|
|
"li",
|
|
|
|
|
"article",
|
|
|
|
|
"section",
|
|
|
|
|
"[role=\"listitem\"]",
|
|
|
|
|
"[class*=\"item\"]",
|
|
|
|
|
"[class*=\"Item\"]",
|
|
|
|
|
"[class*=\"card\"]",
|
|
|
|
|
"[class*=\"Card\"]",
|
|
|
|
|
"[class*=\"result\"]",
|
|
|
|
|
"[class*=\"Result\"]",
|
|
|
|
|
"[class*=\"source\"]",
|
|
|
|
|
"[class*=\"Source\"]",
|
|
|
|
|
"[class*=\"web\"]",
|
|
|
|
|
"[class*=\"Web\"]",
|
|
|
|
|
].join(",")) ?? element;
|
|
|
|
|
|
|
|
|
|
const title = normalizeInline(
|
|
|
|
|
card.querySelector("[class*=\"title\"], [class*=\"Title\"], h1, h2, h3, h4")?.textContent
|
|
|
|
|
?? element.getAttribute("title")
|
|
|
|
|
?? element.getAttribute("aria-label")
|
|
|
|
|
?? card.textContent,
|
|
|
|
|
);
|
|
|
|
|
const siteName = sanitizeSiteName(
|
|
|
|
|
card.querySelector("[class*=\"site\"], [class*=\"Site\"], [class*=\"domain\"], [class*=\"Domain\"], [class*=\"source\"], [class*=\"Source\"]")?.textContent
|
|
|
|
|
?? element.getAttribute("data-site-name")
|
|
|
|
|
?? element.dataset.siteName
|
|
|
|
|
?? element.getAttribute("aria-label"),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
url: href as string,
|
|
|
|
|
title: title ?? siteName,
|
|
|
|
|
text: title ?? siteName,
|
|
|
|
|
siteName,
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const extractCitationPanelLink = (element: Element): KimiDomLink | null => {
|
|
|
|
|
if (!(element instanceof HTMLAnchorElement)) {
|
|
|
|
|
return null;
|
|
|
|
@@ -1344,6 +1956,31 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
return results;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const collectUniqueRootLinks = (
|
|
|
|
|
roots: HTMLElement[],
|
|
|
|
|
selectors: string,
|
|
|
|
|
extractor: (element: Element) => KimiDomLink | null,
|
|
|
|
|
): KimiDomLink[] => {
|
|
|
|
|
const results: KimiDomLink[] = [];
|
|
|
|
|
const seen = new Set<string>();
|
|
|
|
|
for (const root of roots) {
|
|
|
|
|
const sourceNodes = [
|
|
|
|
|
root,
|
|
|
|
|
...Array.from(root.querySelectorAll(selectors)),
|
|
|
|
|
];
|
|
|
|
|
for (const sourceNode of sourceNodes) {
|
|
|
|
|
const link = extractor(sourceNode);
|
|
|
|
|
if (!link || shouldIgnoreLink(link) || seen.has(link.url)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seen.add(link.url);
|
|
|
|
|
results.push(link);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return results;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const formatMarkdownLink = (label: string | null, url: string | null): string => {
|
|
|
|
|
const normalizedLabel = normalizeInline(label);
|
|
|
|
|
if (!url) {
|
|
|
|
@@ -1614,6 +2251,9 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
if (!(node instanceof HTMLElement) || !isVisible(node)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (isAuxiliaryPanelElement(node)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const text = normalizeBlock(node.innerText);
|
|
|
|
|
if (!text) {
|
|
|
|
@@ -1726,6 +2366,9 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
if (!(element instanceof HTMLElement) || seen.has(element) || !isVisible(element)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (isAuxiliaryPanelElement(element)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
seen.add(element);
|
|
|
|
|
const order = getElementOrder(element);
|
|
|
|
|
|
|
|
|
@@ -1887,30 +2530,61 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
.filter((value): value is HTMLElement => value instanceof HTMLElement && isVisible(value))
|
|
|
|
|
.map((element) => {
|
|
|
|
|
const text = normalizeBlock(element.innerText) ?? "";
|
|
|
|
|
const externalAnchorCount = Array.from(element.querySelectorAll("a[href]"))
|
|
|
|
|
.filter((node) => extractExternalAnchorLink(node) !== null)
|
|
|
|
|
.length;
|
|
|
|
|
const searchLinkCount = collectUniqueRootLinks(
|
|
|
|
|
[element],
|
|
|
|
|
[
|
|
|
|
|
"a[href]",
|
|
|
|
|
"[data-href]",
|
|
|
|
|
"[data-url]",
|
|
|
|
|
"[data-link]",
|
|
|
|
|
"[data-target-url]",
|
|
|
|
|
"[data-source-url]",
|
|
|
|
|
"[data-origin-url]",
|
|
|
|
|
"[data-real-url]",
|
|
|
|
|
"[data-page-url]",
|
|
|
|
|
"[data-web-url]",
|
|
|
|
|
"[data-log]",
|
|
|
|
|
"[data-click]",
|
|
|
|
|
"[data-extra]",
|
|
|
|
|
"[data-info]",
|
|
|
|
|
"li",
|
|
|
|
|
"article",
|
|
|
|
|
"[role=\"listitem\"]",
|
|
|
|
|
"[class*=\"item\"]",
|
|
|
|
|
"[class*=\"Item\"]",
|
|
|
|
|
"[class*=\"card\"]",
|
|
|
|
|
"[class*=\"Card\"]",
|
|
|
|
|
"[class*=\"result\"]",
|
|
|
|
|
"[class*=\"Result\"]",
|
|
|
|
|
].join(","),
|
|
|
|
|
extractSearchResultLink,
|
|
|
|
|
).length;
|
|
|
|
|
const rect = element.getBoundingClientRect();
|
|
|
|
|
const area = Math.round(Math.max(rect.width, 0) * Math.max(rect.height, 0));
|
|
|
|
|
const className = normalizeInline(element.getAttribute("class"))?.toLowerCase() ?? "";
|
|
|
|
|
return {
|
|
|
|
|
element,
|
|
|
|
|
text,
|
|
|
|
|
externalAnchorCount,
|
|
|
|
|
searchLinkCount,
|
|
|
|
|
area,
|
|
|
|
|
className,
|
|
|
|
|
};
|
|
|
|
|
})
|
|
|
|
|
.filter((candidate) =>
|
|
|
|
|
candidate.externalAnchorCount >= 3
|
|
|
|
|
candidate.searchLinkCount >= 1
|
|
|
|
|
&& (
|
|
|
|
|
/^搜索网页(?:\s+\d+)?/m.test(candidate.text)
|
|
|
|
|
|| (
|
|
|
|
|
candidate.text.includes("搜索网页")
|
|
|
|
|
&& (candidate.className.includes("ole") || candidate.className.includes("search"))
|
|
|
|
|
&& (
|
|
|
|
|
candidate.text.includes("结果")
|
|
|
|
|
|| candidate.className.includes("ole")
|
|
|
|
|
|| candidate.className.includes("search")
|
|
|
|
|
|| candidate.className.includes("web")
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
))
|
|
|
|
|
.sort((left, right) => left.area - right.area || right.externalAnchorCount - left.externalAnchorCount);
|
|
|
|
|
.sort((left, right) => left.area - right.area || right.searchLinkCount - left.searchLinkCount);
|
|
|
|
|
|
|
|
|
|
const searchPanelRoots: HTMLElement[] = [];
|
|
|
|
|
for (const candidate of searchPanelCandidates) {
|
|
|
|
@@ -1922,8 +2596,32 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
|
|
|
|
|
const searchResultLinks = collectUniqueLinks(
|
|
|
|
|
searchPanelRoots,
|
|
|
|
|
"a[href]",
|
|
|
|
|
extractExternalAnchorLink,
|
|
|
|
|
[
|
|
|
|
|
"a[href]",
|
|
|
|
|
"[data-href]",
|
|
|
|
|
"[data-url]",
|
|
|
|
|
"[data-link]",
|
|
|
|
|
"[data-target-url]",
|
|
|
|
|
"[data-source-url]",
|
|
|
|
|
"[data-origin-url]",
|
|
|
|
|
"[data-real-url]",
|
|
|
|
|
"[data-page-url]",
|
|
|
|
|
"[data-web-url]",
|
|
|
|
|
"[data-log]",
|
|
|
|
|
"[data-click]",
|
|
|
|
|
"[data-extra]",
|
|
|
|
|
"[data-info]",
|
|
|
|
|
"li",
|
|
|
|
|
"article",
|
|
|
|
|
"[role=\"listitem\"]",
|
|
|
|
|
"[class*=\"item\"]",
|
|
|
|
|
"[class*=\"Item\"]",
|
|
|
|
|
"[class*=\"card\"]",
|
|
|
|
|
"[class*=\"Card\"]",
|
|
|
|
|
"[class*=\"result\"]",
|
|
|
|
|
"[class*=\"Result\"]",
|
|
|
|
|
].join(","),
|
|
|
|
|
extractSearchResultLink,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const bodyText = normalizeBlock(document.body?.innerText) ?? "";
|
|
|
|
@@ -1996,6 +2694,7 @@ async function readKimiPageSnapshot(page: PlaywrightPage, questionText: string):
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
question: questionText,
|
|
|
|
|
redirectQueryKeys: KIMI_REDIRECT_QUERY_KEYS,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
@@ -2065,8 +2764,18 @@ async function waitForKimiAnswer(
|
|
|
|
|
&& stablePollCount >= KIMI_STABLE_POLLS_REQUIRED
|
|
|
|
|
&& shouldReturnStableKimiAnswer(snapshot, firstChangedContentAt, now)
|
|
|
|
|
) {
|
|
|
|
|
const contentSnapshot = pickBetterSnapshot(bestSnapshot, snapshot);
|
|
|
|
|
const revealedSearchLinks = await collectKimiSearchPanelLinks(page, questionText, signal).catch(() => []);
|
|
|
|
|
let sourceSnapshot = await readKimiPageSnapshot(page, questionText);
|
|
|
|
|
if (revealedSearchLinks.length) {
|
|
|
|
|
sourceSnapshot = mergeKimiSnapshots(sourceSnapshot, {
|
|
|
|
|
...sourceSnapshot,
|
|
|
|
|
searchResultLinks: revealedSearchLinks,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await ensureKimiCitationPanelOpen(page, signal);
|
|
|
|
|
let finalSnapshot = pickBetterSnapshot(bestSnapshot, await readKimiPageSnapshot(page, questionText));
|
|
|
|
|
sourceSnapshot = mergeKimiSnapshots(sourceSnapshot, await readKimiPageSnapshot(page, questionText));
|
|
|
|
|
let finalSnapshot = mergeKimiSourceLinksIntoContentSnapshot(contentSnapshot, sourceSnapshot);
|
|
|
|
|
if (!hasKimiObservedContent(finalSnapshot) && bestContentSnapshot) {
|
|
|
|
|
finalSnapshot = pickBetterSnapshot(finalSnapshot, bestContentSnapshot);
|
|
|
|
|
}
|
|
|
|
@@ -2082,8 +2791,18 @@ async function waitForKimiAnswer(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await pinKimiConversationToBottom(page);
|
|
|
|
|
const contentSnapshot = bestContentSnapshot ?? bestSnapshot;
|
|
|
|
|
const revealedSearchLinks = await collectKimiSearchPanelLinks(page, questionText, signal).catch(() => []);
|
|
|
|
|
let sourceSnapshot = await readKimiPageSnapshot(page, questionText);
|
|
|
|
|
if (revealedSearchLinks.length) {
|
|
|
|
|
sourceSnapshot = mergeKimiSnapshots(sourceSnapshot, {
|
|
|
|
|
...sourceSnapshot,
|
|
|
|
|
searchResultLinks: revealedSearchLinks,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await ensureKimiCitationPanelOpen(page, signal);
|
|
|
|
|
let finalSnapshot = pickBetterSnapshot(bestSnapshot, await readKimiPageSnapshot(page, questionText));
|
|
|
|
|
sourceSnapshot = mergeKimiSnapshots(sourceSnapshot, await readKimiPageSnapshot(page, questionText));
|
|
|
|
|
let finalSnapshot = mergeKimiSourceLinksIntoContentSnapshot(contentSnapshot, sourceSnapshot);
|
|
|
|
|
if (!hasKimiObservedContent(finalSnapshot) && bestContentSnapshot) {
|
|
|
|
|
finalSnapshot = pickBetterSnapshot(finalSnapshot, bestContentSnapshot);
|
|
|
|
|
}
|
|
|
|
@@ -2173,16 +2892,11 @@ export const kimiAdapter: MonitorAdapter = {
|
|
|
|
|
context.reportProgress("kimi.wait_answer");
|
|
|
|
|
const queryResult = await waitForKimiAnswer(page, questionText, context.signal);
|
|
|
|
|
const finalSnapshot = queryResult.finalSnapshot;
|
|
|
|
|
const citations = dedupeSourceItems(
|
|
|
|
|
finalSnapshot.citationLinks
|
|
|
|
|
.map((item) => buildSourceItem(item))
|
|
|
|
|
.filter((item): item is MonitoringSourceItem => item !== null),
|
|
|
|
|
);
|
|
|
|
|
const searchResults = dedupeSourceItems(
|
|
|
|
|
finalSnapshot.searchResultLinks
|
|
|
|
|
.map((item) => buildSourceItem(item))
|
|
|
|
|
.filter((item): item is MonitoringSourceItem => item !== null),
|
|
|
|
|
);
|
|
|
|
|
const classifiedSources = classifyKimiSources(finalSnapshot);
|
|
|
|
|
const citations = classifiedSources.citations;
|
|
|
|
|
const searchResults = classifiedSources.searchResults;
|
|
|
|
|
const inlineCitationCandidates = classifiedSources.inlineCitationCandidates;
|
|
|
|
|
const panelCitations = classifiedSources.panelCitations;
|
|
|
|
|
const answer = normalizeText(finalSnapshot.answerText);
|
|
|
|
|
const reasoning = normalizeText(finalSnapshot.reasoningText);
|
|
|
|
|
const answerComplete = isKimiAnswerComplete(finalSnapshot);
|
|
|
|
@@ -2284,14 +2998,12 @@ export const kimiAdapter: MonitorAdapter = {
|
|
|
|
|
reasoning_blocks: finalSnapshot.reasoningBlocks,
|
|
|
|
|
citation_count: citations.length,
|
|
|
|
|
citations: toJsonSources(citations),
|
|
|
|
|
panel_citation_count: finalSnapshot.panelCitationLinks.length,
|
|
|
|
|
panel_citations: finalSnapshot.panelCitationLinks.map((item) => ({
|
|
|
|
|
url: item.url,
|
|
|
|
|
title: item.title ?? item.text ?? null,
|
|
|
|
|
site_name: item.siteName ?? null,
|
|
|
|
|
})),
|
|
|
|
|
inline_citation_candidate_count: finalSnapshot.inlineCitationCandidateLinks.length,
|
|
|
|
|
inline_citation_candidates: finalSnapshot.inlineCitationCandidateLinks.map((item) => ({
|
|
|
|
|
panel_citation_count: panelCitations.length,
|
|
|
|
|
panel_citations: toJsonSources(panelCitations),
|
|
|
|
|
inline_citation_candidate_count: inlineCitationCandidates.length,
|
|
|
|
|
inline_citation_candidates: toJsonSources(inlineCitationCandidates),
|
|
|
|
|
inline_answer_citation_candidate_count: finalSnapshot.inlineCitationCandidateLinks.length,
|
|
|
|
|
inline_answer_citation_candidates: finalSnapshot.inlineCitationCandidateLinks.map((item) => ({
|
|
|
|
|
url: item.url,
|
|
|
|
|
title: item.title ?? item.text ?? null,
|
|
|
|
|
site_name: item.siteName ?? null,
|
|
|
|
@@ -2312,3 +3024,11 @@ export const kimiAdapter: MonitorAdapter = {
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const __kimiTestUtils = {
|
|
|
|
|
buildSourceItem,
|
|
|
|
|
classifyKimiSources,
|
|
|
|
|
dedupeSourceItems,
|
|
|
|
|
mergeKimiSourceLinksIntoContentSnapshot,
|
|
|
|
|
normalizeUrl,
|
|
|
|
|
};
|
|
|
|
|