style: format web apps with prettier and sort imports

Apply repo-wide Prettier/lint normalization across admin-web,
desktop-client and ops-web: single quotes, no semicolons, trailing
commas, consistent line wrapping, and import ordering. Also drop an
unused brand-logo import in DesktopShell.vue.

No behavior changes — formatting only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:56:18 +08:00
parent a44ed21967
commit aa96143754
81 changed files with 2929 additions and 2252 deletions
+29 -15
View File
@@ -103,9 +103,11 @@ export function mergeAbortSignals(
if (!activeSignals.length) {
return undefined
}
const anyAbortSignal = (AbortSignal as typeof AbortSignal & {
any?: (signals: AbortSignal[]) => AbortSignal
}).any
const anyAbortSignal = (
AbortSignal as typeof AbortSignal & {
any?: (signals: AbortSignal[]) => AbortSignal
}
).any
if (typeof anyAbortSignal === 'function') {
return anyAbortSignal(activeSignals)
}
@@ -127,9 +129,11 @@ export function mergeAbortSignals(
}
export function timeoutSignal(timeoutMs = defaultFetchTimeoutMs): AbortSignal {
const timeoutAbortSignal = (AbortSignal as typeof AbortSignal & {
timeout?: (timeoutMs: number) => AbortSignal
}).timeout
const timeoutAbortSignal = (
AbortSignal as typeof AbortSignal & {
timeout?: (timeoutMs: number) => AbortSignal
}
).timeout
if (typeof timeoutAbortSignal === 'function') {
return timeoutAbortSignal(timeoutMs)
}
@@ -146,7 +150,10 @@ export function buildAdapterAbortSignal(options: AdapterFetchOptions = {}): Abor
]) as AbortSignal
}
export function withRequestTimeout(init: RequestInit = {}, options: AdapterFetchOptions = {}): RequestInit {
export function withRequestTimeout(
init: RequestInit = {},
options: AdapterFetchOptions = {},
): RequestInit {
return {
...init,
signal: mergeAbortSignals([
@@ -219,10 +226,14 @@ export async function fetchImageBlob(
return null
}
const response = await adapterFetch(normalizedUrl, {}, {
timeoutMs: options.timeoutMs ?? defaultImageFetchTimeoutMs,
signal: options.signal,
}).catch(() => null)
const response = await adapterFetch(
normalizedUrl,
{},
{
timeoutMs: options.timeoutMs ?? defaultImageFetchTimeoutMs,
signal: options.signal,
},
).catch(() => null)
if (!response?.ok) {
return null
}
@@ -481,10 +492,13 @@ export async function pageFetchJson<T>(
let serialized: string
try {
serialized = (await raceWithAbort(webContents.executeJavaScript(script, true) as Promise<string>, {
signal: options.signal,
timeoutMs: options.timeoutMs,
})) as string
serialized = (await raceWithAbort(
webContents.executeJavaScript(script, true) as Promise<string>,
{
signal: options.signal,
timeoutMs: options.timeoutMs,
},
)) as string
} catch {
return null
}
@@ -1,9 +1,9 @@
import { describe, expect, it, vi } from "vitest";
import { describe, expect, it, vi } from 'vitest'
vi.mock("electron/main", () => ({
vi.mock('electron/main', () => ({
app: {
getPath: () => "/tmp/geo-rankly-deepseek-test",
getVersion: () => "0.0.0-test",
getPath: () => '/tmp/geo-rankly-deepseek-test',
getVersion: () => '0.0.0-test',
isPackaged: false,
on: vi.fn(),
whenReady: () => Promise.resolve(),
@@ -42,10 +42,10 @@ vi.mock("electron/main", () => ({
},
})),
},
}));
}))
import { __deepseekTestUtils } from "./deepseek";
import { getMonitorAdapter } from "./index";
import { __deepseekTestUtils } from './deepseek'
import { getMonitorAdapter } from './index'
const {
extractQuestionText,
@@ -60,337 +60,347 @@ const {
isDeepSeekToggleSelected,
isDeepSeekWebpagesTriggerText,
isDeepSeekReasoningSearchTriggerText,
} = __deepseekTestUtils;
} = __deepseekTestUtils
describe("deepseek adapter helpers", () => {
it("extracts question text from monitoring payload", () => {
expect(extractQuestionText({ question_text: "DeepSeek 怎么看 GEO" })).toBe("DeepSeek 怎么看 GEO");
});
describe('deepseek adapter helpers', () => {
it('extracts question text from monitoring payload', () => {
expect(extractQuestionText({ question_text: 'DeepSeek 怎么看 GEO' })).toBe(
'DeepSeek 怎么看 GEO',
)
})
it("dedupes sources by normalized url without hash fragments", () => {
it('dedupes sources by normalized url without hash fragments', () => {
const items = dedupeSourceItems([
{
url: "https://example.com/a#top",
title: "A",
normalized_url: "https://example.com/a#top",
url: 'https://example.com/a#top',
title: 'A',
normalized_url: 'https://example.com/a#top',
},
{
url: "https://example.com/a#bottom",
title: "A later",
normalized_url: "https://example.com/a#bottom",
url: 'https://example.com/a#bottom',
title: 'A later',
normalized_url: 'https://example.com/a#bottom',
},
]);
])
expect(items).toHaveLength(1);
expect(items[0]?.normalized_url).toBe("https://example.com/a");
expect(items[0]?.title).toBe("A");
});
expect(items).toHaveLength(1)
expect(items[0]?.normalized_url).toBe('https://example.com/a')
expect(items[0]?.title).toBe('A')
})
it("treats revealed search cards as citation sources and inline links as content citation candidates", () => {
it('treats revealed search cards as citation sources and inline links as content citation candidates', () => {
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: "https://example.com/citation",
title: "Citation",
normalized_url: "https://example.com/citation",
url: 'https://example.com/citation',
title: 'Citation',
normalized_url: 'https://example.com/citation',
},
],
sourcePanelLinks: [],
searchPanelLinks: [
{
url: "https://example.com/search",
title: "Search",
normalized_url: "https://example.com/search",
url: 'https://example.com/search',
title: 'Search',
normalized_url: 'https://example.com/search',
},
],
});
})
expect(classified.citations).toHaveLength(1);
expect(classified.inlineCitationCandidates).toHaveLength(1);
expect(classified.searchResults).toHaveLength(1);
expect(classified.citations[0]?.url).toBe("https://example.com/search");
expect(classified.inlineCitationCandidates[0]?.url).toBe("https://example.com/citation");
expect(classified.searchResults[0]?.url).toBe("https://example.com/search");
});
expect(classified.citations).toHaveLength(1)
expect(classified.inlineCitationCandidates).toHaveLength(1)
expect(classified.searchResults).toHaveLength(1)
expect(classified.citations[0]?.url).toBe('https://example.com/search')
expect(classified.inlineCitationCandidates[0]?.url).toBe('https://example.com/citation')
expect(classified.searchResults[0]?.url).toBe('https://example.com/search')
})
it("uses revealed xx webpages panel links instead of stale snapshot links for citation sources", () => {
it('uses revealed xx webpages panel links instead of stale snapshot links for citation sources', () => {
const citationPanelLinks = resolveDeepSeekCitationPanelLinks(
[
{
url: "https://stale.example/from-snapshot",
title: "Stale snapshot result",
text: "Wrong result from the page snapshot",
siteName: "Stale",
kind: "search",
url: 'https://stale.example/from-snapshot',
title: 'Stale snapshot result',
text: 'Wrong result from the page snapshot',
siteName: 'Stale',
kind: 'search',
},
],
[
{
url: "https://right.example/from-xx-webpages",
title: "Right side panel result",
text: "Correct result from DeepSeek search results",
siteName: "Right",
kind: "search",
url: 'https://right.example/from-xx-webpages',
title: 'Right side panel result',
text: 'Correct result from DeepSeek search results',
siteName: 'Right',
kind: 'search',
},
],
);
)
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: "https://inline.example/body-link",
title: "Inline body link",
text: "[1]",
siteName: "Inline",
kind: "inline",
url: 'https://inline.example/body-link',
title: 'Inline body link',
text: '[1]',
siteName: 'Inline',
kind: 'inline',
},
],
sourcePanelLinks: [],
searchPanelLinks: citationPanelLinks,
});
})
expect(citationPanelLinks.map((item) => item.url)).toEqual([
"https://right.example/from-xx-webpages",
]);
'https://right.example/from-xx-webpages',
])
expect(classified.citations.map((item) => item.url)).toEqual([
"https://right.example/from-xx-webpages",
]);
'https://right.example/from-xx-webpages',
])
expect(classified.searchResults.map((item) => item.url)).toEqual([
"https://right.example/from-xx-webpages",
]);
'https://right.example/from-xx-webpages',
])
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
"https://inline.example/body-link",
]);
});
'https://inline.example/body-link',
])
})
it("falls back to snapshot search links when the revealed xx webpages panel is unavailable", () => {
it('falls back to snapshot search links when the revealed xx webpages panel is unavailable', () => {
const citationPanelLinks = resolveDeepSeekCitationPanelLinks(
[
{
url: "https://snapshot.example/result",
title: "Snapshot result",
text: "Fallback result",
siteName: "Snapshot",
kind: "search",
url: 'https://snapshot.example/result',
title: 'Snapshot result',
text: 'Fallback result',
siteName: 'Snapshot',
kind: 'search',
},
],
[],
);
)
expect(citationPanelLinks.map((item) => item.url)).toEqual([
"https://snapshot.example/result",
]);
});
expect(citationPanelLinks.map((item) => item.url)).toEqual(['https://snapshot.example/result'])
})
it("enriches numeric inline citations with source metadata from revealed search cards", () => {
it('enriches numeric inline citations with source metadata from revealed search cards', () => {
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: "https://example.com/citation",
url: 'https://example.com/citation',
title: null,
text: "-1",
text: '-1',
siteName: null,
kind: "inline",
kind: 'inline',
},
],
sourcePanelLinks: [],
searchPanelLinks: [
{
url: "https://example.com/citation",
title: "DeepSeek source article",
text: "Snippet",
siteName: "Example",
kind: "search",
url: 'https://example.com/citation',
title: 'DeepSeek source article',
text: 'Snippet',
siteName: 'Example',
kind: 'search',
},
],
});
})
expect(classified.inlineCitationCandidates).toHaveLength(1);
expect(classified.inlineCitationCandidates[0]?.title).toBe("DeepSeek source article");
expect(classified.inlineCitationCandidates[0]?.site_name).toBe("Example");
expect(classified.citations).toHaveLength(1);
expect(classified.searchResults).toHaveLength(1);
});
expect(classified.inlineCitationCandidates).toHaveLength(1)
expect(classified.inlineCitationCandidates[0]?.title).toBe('DeepSeek source article')
expect(classified.inlineCitationCandidates[0]?.site_name).toBe('Example')
expect(classified.citations).toHaveLength(1)
expect(classified.searchResults).toHaveLength(1)
})
it("keeps reasoning search pages as citation sources separate from answer content citations", () => {
it('keeps reasoning search pages as citation sources separate from answer content citations', () => {
const classified = classifyDeepseekSources({
inlineLinks: [
{
url: "https://example.com/body-citation",
title: "Answer body citation",
text: "[15]",
siteName: "Body Site",
kind: "inline",
url: 'https://example.com/body-citation',
title: 'Answer body citation',
text: '[15]',
siteName: 'Body Site',
kind: 'inline',
},
],
sourcePanelLinks: [],
searchPanelLinks: [
{
url: "https://example.com/reasoning-search",
title: "Reasoning searched page",
text: "浏览页面",
siteName: "Search Site",
kind: "search",
url: 'https://example.com/reasoning-search',
title: 'Reasoning searched page',
text: '浏览页面',
siteName: 'Search Site',
kind: 'search',
},
],
});
})
expect(classified.inlineCitationCandidates.map((item) => item.url)).toEqual([
"https://example.com/body-citation",
]);
'https://example.com/body-citation',
])
expect(classified.citations.map((item) => item.url)).toEqual([
"https://example.com/reasoning-search",
]);
'https://example.com/reasoning-search',
])
expect(classified.searchResults.map((item) => item.url)).toEqual([
"https://example.com/reasoning-search",
]);
});
'https://example.com/reasoning-search',
])
})
it("retains all revealed DeepSeek search results instead of truncating after 32 items", () => {
it('retains all revealed DeepSeek search results instead of truncating after 32 items', () => {
const searchPanelLinks = Array.from({ length: 78 }, (_, index) => ({
url: `https://example.com/search-${index + 1}`,
title: `Search ${index + 1}`,
text: `Snippet ${index + 1}`,
siteName: "Example",
kind: "search" as const,
}));
siteName: 'Example',
kind: 'search' as const,
}))
const classified = classifyDeepseekSources({
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks,
});
})
expect(classified.citations).toHaveLength(78);
expect(classified.searchResults).toHaveLength(78);
});
expect(classified.citations).toHaveLength(78)
expect(classified.searchResults).toHaveLength(78)
})
it("merges revealed sidebar search links with the snapshot search links by normalized url", () => {
it('merges revealed sidebar search links with the snapshot search links by normalized url', () => {
const merged = mergeDeepSeekRawSourceLinks(
[
{
url: "https://example.com/report#top",
url: 'https://example.com/report#top',
title: null,
text: "Visible title",
text: 'Visible title',
siteName: null,
kind: "search",
kind: 'search',
},
],
[
{
url: "https://example.com/report#bottom",
title: "Merged title",
text: "Merged snippet",
siteName: "Example",
kind: "search",
url: 'https://example.com/report#bottom',
title: 'Merged title',
text: 'Merged snippet',
siteName: 'Example',
kind: 'search',
},
{
url: "https://example.com/extra",
title: "Extra result",
text: "Extra snippet",
siteName: "Example",
kind: "search",
url: 'https://example.com/extra',
title: 'Extra result',
text: 'Extra snippet',
siteName: 'Example',
kind: 'search',
},
],
);
)
expect(merged).toHaveLength(2);
expect(merged[0]).toEqual(expect.objectContaining({
url: "https://example.com/report",
title: "Merged title",
text: "Visible title",
siteName: "Example",
}));
expect(merged[1]?.url).toBe("https://example.com/extra");
});
expect(merged).toHaveLength(2)
expect(merged[0]).toEqual(
expect.objectContaining({
url: 'https://example.com/report',
title: 'Merged title',
text: 'Visible title',
siteName: 'Example',
}),
)
expect(merged[1]?.url).toBe('https://example.com/extra')
})
it("normalizes deepseek raw source links into monitoring source items", () => {
it('normalizes deepseek raw source links into monitoring source items', () => {
const item = buildSourceItem({
url: "https://example.com/report#cite-1",
url: 'https://example.com/report#cite-1',
title: null,
text: "DeepSeek source title",
siteName: "Example",
kind: "source",
});
text: 'DeepSeek source title',
siteName: 'Example',
kind: 'source',
})
expect(item).toEqual(expect.objectContaining({
url: "https://example.com/report",
normalized_url: "https://example.com/report",
title: "DeepSeek source title",
site_name: "Example",
host: "example.com",
}));
});
expect(item).toEqual(
expect.objectContaining({
url: 'https://example.com/report',
normalized_url: 'https://example.com/report',
title: 'DeepSeek source title',
site_name: 'Example',
host: 'example.com',
}),
)
})
it("unwraps DeepSeek redirect URLs before storing source items", () => {
it('unwraps DeepSeek redirect URLs before storing source items', () => {
const item = buildSourceItem({
url: "https://chat.deepseek.com/api/redirect?url=https%3A%2F%2Fexample.com%2Fsource%23ref",
title: "Redirected source",
url: 'https://chat.deepseek.com/api/redirect?url=https%3A%2F%2Fexample.com%2Fsource%23ref',
title: 'Redirected source',
text: null,
siteName: null,
kind: "search",
});
kind: 'search',
})
expect(item).toEqual(expect.objectContaining({
url: "https://example.com/source",
normalized_url: "https://example.com/source",
host: "example.com",
}));
});
expect(item).toEqual(
expect.objectContaining({
url: 'https://example.com/source',
normalized_url: 'https://example.com/source',
host: 'example.com',
}),
)
})
it("does not use numeric citation markers as source titles", () => {
it('does not use numeric citation markers as source titles', () => {
const item = buildSourceItem({
url: "https://example.com/report#cite-1",
url: 'https://example.com/report#cite-1',
title: null,
text: "-1",
text: '-1',
siteName: null,
kind: "inline",
});
kind: 'inline',
})
expect(item?.title).toBeNull();
expect(item?.site_name).toBe("example.com");
});
expect(item?.title).toBeNull()
expect(item?.site_name).toBe('example.com')
})
it("treats a stable answer as complete even without citations", () => {
expect(isObservationComplete({
answer: "这是最终答案",
busy: false,
loginRequired: false,
challengeRequired: false,
signature: "sig-1",
})).toBe(true);
});
it('treats a stable answer as complete even without citations', () => {
expect(
isObservationComplete({
answer: '这是最终答案',
busy: false,
loginRequired: false,
challengeRequired: false,
signature: 'sig-1',
}),
).toBe(true)
})
it("ignores source-logo loading placeholders when computing busy signals", () => {
expect(detectDeepSeekBusySignals({
bodyText: "已阅读 10 个网页",
descriptors: [
"site_logo_loading site_logo_fallback",
"site_logo_loading site_logo_fallback",
],
})).toEqual([]);
});
it('ignores source-logo loading placeholders when computing busy signals', () => {
expect(
detectDeepSeekBusySignals({
bodyText: '已阅读 10 个网页',
descriptors: [
'site_logo_loading site_logo_fallback',
'site_logo_loading site_logo_fallback',
],
}),
).toEqual([])
})
it("recognizes deepseek toggle selected state from class names", () => {
expect(isDeepSeekToggleSelected("ds-toggle-button ds-toggle-button--selected")).toBe(true);
expect(isDeepSeekToggleSelected("ds-toggle-button")).toBe(false);
});
it('recognizes deepseek toggle selected state from class names', () => {
expect(isDeepSeekToggleSelected('ds-toggle-button ds-toggle-button--selected')).toBe(true)
expect(isDeepSeekToggleSelected('ds-toggle-button')).toBe(false)
})
it("only treats the bottom 网页 pill as the DeepSeek source trigger", () => {
expect(isDeepSeekWebpagesTriggerText("57 个网页")).toBe(true);
expect(isDeepSeekWebpagesTriggerText("已阅读 57 个网页")).toBe(true);
expect(isDeepSeekWebpagesTriggerText("搜索到 39 个网页")).toBe(false);
});
it('only treats the bottom 网页 pill as the DeepSeek source trigger', () => {
expect(isDeepSeekWebpagesTriggerText('57 个网页')).toBe(true)
expect(isDeepSeekWebpagesTriggerText('已阅读 57 个网页')).toBe(true)
expect(isDeepSeekWebpagesTriggerText('搜索到 39 个网页')).toBe(false)
})
it("treats reasoning 搜索到 xx 个网页 as a separate citation-source fallback trigger", () => {
expect(isDeepSeekReasoningSearchTriggerText("搜索到 30 个网页")).toBe(true);
expect(isDeepSeekReasoningSearchTriggerText("57 个网页")).toBe(false);
expect(isDeepSeekReasoningSearchTriggerText("已阅读 57 个网页")).toBe(false);
});
it('treats reasoning 搜索到 xx 个网页 as a separate citation-source fallback trigger', () => {
expect(isDeepSeekReasoningSearchTriggerText('搜索到 30 个网页')).toBe(true)
expect(isDeepSeekReasoningSearchTriggerText('57 个网页')).toBe(false)
expect(isDeepSeekReasoningSearchTriggerText('已阅读 57 个网页')).toBe(false)
})
it("normalizes DeepSeek model labels without mistaking search toggles for models", () => {
it('normalizes DeepSeek model labels without mistaking search toggles for models', () => {
const snapshot = {
url: "https://chat.deepseek.com/",
title: "DeepSeek",
url: 'https://chat.deepseek.com/',
title: 'DeepSeek',
loginRequired: false,
loginReason: null,
challengeRequired: false,
@@ -399,74 +409,84 @@ describe("deepseek adapter helpers", () => {
busySignals: [],
composerValue: null,
sendDisabled: false,
answer: "Final answer",
answerText: "Final answer",
answer: 'Final answer',
answerText: 'Final answer',
reasoning: null,
signature: "sig-1",
currentModelLabel: "深度思考 (R1)",
signature: 'sig-1',
currentModelLabel: '深度思考 (R1)',
providerRequestID: null,
requestID: null,
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks: [],
};
}
const sources = {
citations: [],
inlineCitationCandidates: [],
searchResults: [],
};
}
const r1Payload = buildDeepSeekPayload(snapshot, sources, "Final answer");
const searchPayload = buildDeepSeekPayload({
...snapshot,
currentModelLabel: "联网搜索",
}, sources, "Final answer");
const r1Payload = buildDeepSeekPayload(snapshot, sources, 'Final answer')
const searchPayload = buildDeepSeekPayload(
{
...snapshot,
currentModelLabel: '联网搜索',
},
sources,
'Final answer',
)
expect(r1Payload.provider_model).toBe("DeepSeek");
expect(searchPayload.provider_model).toBeNull();
});
expect(r1Payload.provider_model).toBe('DeepSeek')
expect(searchPayload.provider_model).toBeNull()
})
it("builds json-safe payloads with null placeholders instead of undefined", () => {
const payload = buildDeepSeekPayload({
url: "https://chat.deepseek.com/",
title: "DeepSeek",
loginRequired: false,
loginReason: null,
challengeRequired: false,
challengeReason: null,
busy: false,
busySignals: [],
composerValue: null,
sendDisabled: false,
answer: "Final answer",
answerText: "Final answer",
reasoning: null,
signature: "sig-1",
currentModelLabel: null,
providerRequestID: null,
requestID: null,
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks: [],
}, {
citations: [],
inlineCitationCandidates: [],
searchResults: [],
}, "Final answer");
it('builds json-safe payloads with null placeholders instead of undefined', () => {
const payload = buildDeepSeekPayload(
{
url: 'https://chat.deepseek.com/',
title: 'DeepSeek',
loginRequired: false,
loginReason: null,
challengeRequired: false,
challengeReason: null,
busy: false,
busySignals: [],
composerValue: null,
sendDisabled: false,
answer: 'Final answer',
answerText: 'Final answer',
reasoning: null,
signature: 'sig-1',
currentModelLabel: null,
providerRequestID: null,
requestID: null,
inlineLinks: [],
sourcePanelLinks: [],
searchPanelLinks: [],
},
{
citations: [],
inlineCitationCandidates: [],
searchResults: [],
},
'Final answer',
)
expect(payload.provider_model).toBeNull();
expect(payload.provider_request_id).toBeNull();
expect(payload.request_id).toBeNull();
expect((payload.raw_response_json as Record<string, unknown>).provider_model).toBeNull();
expect((payload.raw_response_json as Record<string, unknown>).provider_request_id).toBeNull();
expect((payload.raw_response_json as Record<string, unknown>).request_id).toBeNull();
expect((payload.raw_response_json as Record<string, unknown>).signature).toBe("sig-1");
expect((payload.raw_response_json as Record<string, unknown>).inline_citation_candidates).toEqual([]);
});
});
expect(payload.provider_model).toBeNull()
expect(payload.provider_request_id).toBeNull()
expect(payload.request_id).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).provider_model).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).provider_request_id).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).request_id).toBeNull()
expect((payload.raw_response_json as Record<string, unknown>).signature).toBe('sig-1')
expect(
(payload.raw_response_json as Record<string, unknown>).inline_citation_candidates,
).toEqual([])
})
})
describe("monitor adapter registry", () => {
it("registers deepseek as a monitor adapter", () => {
expect(getMonitorAdapter("deepseek")?.provider).toBe("deepseek");
});
});
describe('monitor adapter registry', () => {
it('registers deepseek as a monitor adapter', () => {
expect(getMonitorAdapter('deepseek')?.provider).toBe('deepseek')
})
})
@@ -1181,15 +1181,13 @@ async function maybeRevealDeepSeekReasoningSearchSources(page: PlaywrightPage):
const text = normalize(value)
return Boolean(
text &&
(/^搜索到\s*\d+\s*个网页$/i.test(text) ||
/^(?:found|searched)\s*\d+\s*(?:web\s*pages?|webpages?)$/i.test(text)),
(/^搜索到\s*\d+\s*个网页$/i.test(text) ||
/^(?:found|searched)\s*\d+\s*(?:web\s*pages?|webpages?)$/i.test(text)),
)
}
const isReasoningNode = (node: HTMLElement): boolean => {
if (
node.closest(
".ds-think-content, [class*='think-content'], [class*='reasoning-content']",
)
node.closest(".ds-think-content, [class*='think-content'], [class*='reasoning-content']")
) {
return true
}
@@ -2832,9 +2830,7 @@ export const deepseekAdapter: MonitorAdapter = {
)
if (revealedSearchPanelLinks.length === 0) {
await maybeRevealDeepSeekReasoningSearchSources(page).catch(() => undefined)
revealedSearchPanelLinks = await collectDeepSeekSearchResultsPanelLinks(page).catch(
() => [],
)
revealedSearchPanelLinks = await collectDeepSeekSearchResultsPanelLinks(page).catch(() => [])
}
const finalSnapshot = await readDeepSeekPageSnapshot(page, questionText).catch(
() => waitResult.finalSnapshot,
@@ -26,9 +26,7 @@ function pickString(value: unknown): string | null {
// 微信: { biz_data: { id_profile: { provider: 'WECHAT', name: '阿白', picture: '...' }, ... } }
// 手机: { biz_data: { id_profile: null, mobile_number: '177******08', email: '', ... } }
// Empty-string fields (email = "") are common, so we use truthy checks rather than ??.
export function extractDeepseekIdentityFromPayload(
payload: unknown,
): DeepseekAccountIdentityHints {
export function extractDeepseekIdentityFromPayload(payload: unknown): DeepseekAccountIdentityHints {
if (!isRecord(payload)) {
return { displayName: null, avatarUrl: null }
}
@@ -42,9 +40,7 @@ export function extractDeepseekIdentityFromPayload(
const profile = isRecord(bizData.id_profile) ? bizData.id_profile : null
const displayName =
pickString(profile?.name) ??
pickString(bizData.mobile_number) ??
pickString(bizData.email)
pickString(profile?.name) ?? pickString(bizData.mobile_number) ?? pickString(bizData.email)
const avatarUrl = pickString(profile?.picture)
return {
@@ -178,8 +178,7 @@ describe('doubao adapter helpers', () => {
)
expect(summary.answer).not.toMatch(/[åæèéäãï][\u0080-\u00ff\u02c0-\u02ff\u2000-\u20ff]/)
expect(resolveDoubaoSubmissionAnswer({ answer: summary.answer })).toEqual({
answer:
'需要合肥地区全屋定制推荐,综合考量性价比、口碑服务等因素。2000㎡展厅,核验批号。',
answer: '需要合肥地区全屋定制推荐,综合考量性价比、口碑服务等因素。2000㎡展厅,核验批号。',
source: 'sse',
})
})
+19 -19
View File
@@ -231,10 +231,12 @@ function isDoubaoMojibakeRepairableCharacter(value: string): boolean {
}
function containsDoubaoMojibakeSignal(value: string): boolean {
return /[\u0080-\u009f]/.test(value) ||
return (
/[\u0080-\u009f]/.test(value) ||
/(?:Ã.|Â[\u0080-\u017f]|â[\u0080-\u017f\u2000-\u20ff]|[åæèéäãï][\u0080-\u017f\u02c0-\u02ff\u2000-\u20ff]|ã[\u0080-\u017f\u02c0-\u02ff\u2000-\u20ff])/.test(
value,
)
)
}
function repairDoubaoMojibakeSegment(value: string): string {
@@ -467,9 +469,10 @@ function selectBestDoubaoText(primary: string | null, secondary: string | null):
return secondaryScore > primaryScore ? secondaryText : primaryText
}
function resolveDoubaoSubmissionAnswer(input: {
function resolveDoubaoSubmissionAnswer(input: { answer: string | null }): {
answer: string | null
}): { answer: string | null; source: 'sse' | null } {
source: 'sse' | null
} {
const answer = normalizeOptionalString(input.answer)
if (answer && !containsDoubaoMojibakeSignal(answer)) {
return { answer, source: 'sse' }
@@ -867,12 +870,11 @@ function collectAnswerFragments(
return result
}
function appendDoubaoChunkDeltaAnswer(
summary: DoubaoStreamSummary,
payload: unknown,
): void {
function appendDoubaoChunkDeltaAnswer(summary: DoubaoStreamSummary, payload: unknown): void {
const text =
isRecord(payload) && typeof payload.text === 'string' ? normalizeOptionalString(payload.text) : null
isRecord(payload) && typeof payload.text === 'string'
? normalizeOptionalString(payload.text)
: null
if (text) {
summary.answer = mergeAnswerText(summary.answer, text)
}
@@ -994,7 +996,10 @@ function isDoubaoRecoverablePageError(error: string): boolean {
return error === 'doubao_query_timeout' || error === 'doubao_page_navigation_interrupted'
}
function buildDoubaoPageNavigationResult(page: PlaywrightPage, detail: string): DoubaoPageQueryFailureResult {
function buildDoubaoPageNavigationResult(
page: PlaywrightPage,
detail: string,
): DoubaoPageQueryFailureResult {
return {
ok: false,
error: 'doubao_page_navigation_interrupted',
@@ -2852,8 +2857,7 @@ const doubaoQueryInPage = async (
title: normalize(document.title),
modelLabel: resolveModelLabel(),
modeLabel,
thinkingModeApplied:
modeKind === 'thinking' ? true : (modeSnapshot.applied ?? null),
thinkingModeApplied: modeKind === 'thinking' ? true : (modeSnapshot.applied ?? null),
deepThinkEnabled: toggleState(deepThinkButton),
domAnswer: snapshot.answerText,
domReasoning: snapshot.reasoningText,
@@ -2869,11 +2873,7 @@ const doubaoQueryInPage = async (
}
if (hasLoginGate()) {
return fail(
'doubao_login_required',
bodyText(),
composer?.getBoundingClientRect().top ?? null,
)
return fail('doubao_login_required', bodyText(), composer?.getBoundingClientRect().top ?? null)
}
if (!composer) {
@@ -3036,9 +3036,9 @@ export const doubaoAdapter: MonitorAdapter = {
if (!isDoubaoExecutionContextNavigationError(error)) {
throw error
}
await page.waitForLoadState('domcontentloaded', { timeout: DOUBAO_PAGE_READY_TIMEOUT_MS }).catch(
() => undefined,
)
await page
.waitForLoadState('domcontentloaded', { timeout: DOUBAO_PAGE_READY_TIMEOUT_MS })
.catch(() => undefined)
await page.waitForLoadState('networkidle', { timeout: 3_000 }).catch(() => undefined)
pageResult = buildDoubaoPageNavigationResult(page, formatUnknownError(error))
}
@@ -1,8 +1,8 @@
import type { Cookie, Session, WebContents } from 'electron/main'
import { normalizeRemoteUrl } from '../common'
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
import type { GenericAIPageState } from '../../generic-ai-auth'
import { normalizeRemoteUrl } from '../common'
// Doubao binding rules are deliberately isolated here.
// Doubao can chat anonymously, while Geo Rankly requires a real logged-in Doubao
@@ -118,8 +118,8 @@ export function doubaoHasBoundAccountPageSignal(
): boolean {
return Boolean(
pageState &&
pageState.strongAuthSignalCount >= 2 &&
(pageState.displayName || pageState.avatarUrl),
pageState.strongAuthSignalCount >= 2 &&
(pageState.displayName || pageState.avatarUrl),
)
}
@@ -45,8 +45,9 @@ const KIMI_PROMOTIONAL_ANSWER_NOISE_PATTERNS = [
/OpenClaw/i,
/24\s*小时为你干活/i,
]
const KIMI_PROMOTIONAL_ANSWER_NOISE_PATTERN_SOURCES =
KIMI_PROMOTIONAL_ANSWER_NOISE_PATTERNS.map((pattern) => pattern.source)
const KIMI_PROMOTIONAL_ANSWER_NOISE_PATTERN_SOURCES = KIMI_PROMOTIONAL_ANSWER_NOISE_PATTERNS.map(
(pattern) => pattern.source,
)
type KimiDomLink = {
url: string
@@ -1,8 +1,8 @@
import type { Cookie, Session, WebContents } from 'electron/main'
import { normalizeRemoteUrl } from '../common'
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
import type { GenericAIPageState } from '../../generic-ai-auth'
import { normalizeRemoteUrl } from '../common'
// Kimi binding rules are deliberately isolated here.
// Kimi creates anonymous tokens/cookies before login (`kimi-auth`,
@@ -98,9 +98,9 @@ export function kimiHasBoundAccountPageSignal(
): boolean {
return Boolean(
pageState &&
pageState.strongAuthSignalCount >= 2 &&
pageState.displayName &&
pageState.credentialFingerprint,
pageState.strongAuthSignalCount >= 2 &&
pageState.displayName &&
pageState.credentialFingerprint,
)
}
@@ -106,12 +106,13 @@ export async function fetchImageAssetBlob(
const response = await (
shouldUseDesktopAssetFetch(candidate)
? fetchDesktopApiURL(candidate, {
signal: mergeAbortSignals([
options.signal,
timeoutSignal(options.timeoutMs ?? 10_000),
]),
signal: mergeAbortSignals([options.signal, timeoutSignal(options.timeoutMs ?? 10_000)]),
})
: adapterFetch(candidate, {}, { timeoutMs: options.timeoutMs ?? 10_000, signal: options.signal })
: adapterFetch(
candidate,
{},
{ timeoutMs: options.timeoutMs ?? 10_000, signal: options.signal },
)
).catch(() => null)
if (!response?.ok) {
continue
@@ -1,8 +1,8 @@
import type { Cookie, Session, WebContents } from 'electron/main'
import { normalizeRemoteUrl } from '../common'
import type { AccountHealthProfile, AuthProbeResult } from '../../auth-types'
import type { GenericAIPageState } from '../../generic-ai-auth'
import { normalizeRemoteUrl } from '../common'
// Qwen binding rules are deliberately isolated here.
// Qwen can keep guest chats and cached sidebar sessions before/without a real
@@ -198,9 +198,7 @@ export function qwenHasBoundAccountPageSignal(
pageState: GenericAIPageState | null | undefined,
): boolean {
return Boolean(
pageState &&
pageState.strongAuthSignalCount >= 2 &&
pageState.credentialFingerprint,
pageState && pageState.strongAuthSignalCount >= 2 && pageState.credentialFingerprint,
)
}
@@ -22,8 +22,8 @@ vi.mock('./media-image', () => ({
}))
import { publishToutiaoArticle } from '../../../../../packages/publisher-platforms/src/toutiao'
import { toutiaoAdapter } from './toutiao'
import { fetchImageAssetBlob } from './media-image'
import { toutiaoAdapter } from './toutiao'
describe('toutiao adapter', () => {
it('fetches relative public assets through the desktop asset pipeline before platform upload', async () => {
@@ -60,7 +60,13 @@ describe('toutiao adapter', () => {
expect(bodyBlob?.type).toBe('image/png')
expect(coverBlob?.type).toBe('image/png')
expect(fetchImageAssetBlob).toHaveBeenCalledWith('/api/public/assets/body-token', expect.anything())
expect(fetchImageAssetBlob).toHaveBeenCalledWith('/api/public/assets/cover-token', expect.anything())
expect(fetchImageAssetBlob).toHaveBeenCalledWith(
'/api/public/assets/body-token',
expect.anything(),
)
expect(fetchImageAssetBlob).toHaveBeenCalledWith(
'/api/public/assets/cover-token',
expect.anything(),
)
})
})
@@ -134,9 +134,7 @@ describe('yuanbao adapter helpers', () => {
// The new yuanbao model lowers <div data-idx-list="1,2,10"> placeholders to "[1][2][10]";
// older SSE frames may still emit "[citation:N]". Both must extract the same indexes.
expect(extractCitationMarkerIndexes('合肥老牌选 [1][2] 性价比 [10]。')).toEqual([1, 2, 10])
expect(
extractCitationMarkerIndexes('混合两种格式 [citation:3] 还有 [4]'),
).toEqual([3, 4])
expect(extractCitationMarkerIndexes('混合两种格式 [citation:3] 还有 [4]')).toEqual([3, 4])
const sources = [
{ url: 'https://example.com/1', normalized_url: 'https://example.com/1' },
@@ -148,9 +146,7 @@ describe('yuanbao adapter helpers', () => {
})
it('rewrites legacy [citation:N] markers to bare [N] for SaaS rendering', () => {
expect(normalizeCitationMarkers('正文 [citation:1] 和 [citation:10]')).toBe(
'正文 [1] 和 [10]',
)
expect(normalizeCitationMarkers('正文 [citation:1] 和 [citation:10]')).toBe('正文 [1] 和 [10]')
// Already-normalised text passes through.
expect(normalizeCitationMarkers('已经是 [1][2] 格式')).toBe('已经是 [1][2] 格式')
// Null/empty pass through.
@@ -158,7 +154,7 @@ describe('yuanbao adapter helpers', () => {
expect(normalizeCitationMarkers('')).toBe('')
})
it("does not invent citations for fast-cache answers (no searchGuid frame, no [N] markers)", () => {
it('does not invent citations for fast-cache answers (no searchGuid frame, no [N] markers)', () => {
// hunyuan_t1 sometimes returns a cached / non-search answer ("已深度思考(用时1秒)"
// with no "找到了 N 篇相关资料" panel). The SSE then carries only `text`
// frames and no `searchGuid`. The adapter must still surface that answer to
@@ -190,7 +186,7 @@ describe('yuanbao adapter helpers', () => {
expect(findUnresolvedCitationIndexes(summary.answer, summary.citations)).toEqual([])
})
it("aligns citations[index-1] with answer markers when SSE places refs in searchGuid.docs (new hunyuan_t1 protocol)", () => {
it('aligns citations[index-1] with answer markers when SSE places refs in searchGuid.docs (new hunyuan_t1 protocol)', () => {
// Captured against the live yuanbao endpoint: deep-search now ships every
// reference inside `searchGuid.docs[]` with a 1-based `index`, and the
// legacy `citations` field is always null in this frame. Whatever the
+1 -5
View File
@@ -80,11 +80,7 @@ function readPersistedFile(): {
function persistFile(): void {
try {
writeFileSync(
settingsPath(),
JSON.stringify({ ...cachedSettings, ...cachedFlags }),
'utf8',
)
writeFileSync(settingsPath(), JSON.stringify({ ...cachedSettings, ...cachedFlags }), 'utf8')
} catch (error) {
console.warn('[desktop-settings] persist failed', error)
}
+1 -1
View File
@@ -1,8 +1,8 @@
import { execFileSync } from 'node:child_process'
import { createHash, randomUUID } from 'node:crypto'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { hostname, networkInterfaces } from 'node:os'
import type { NetworkInterfaceInfo } from 'node:os'
import { hostname, networkInterfaces } from 'node:os'
import { join } from 'node:path'
import { app } from 'electron/main'
+20 -24
View File
@@ -95,8 +95,7 @@ function scheduleBlankPageWatchdog(window: BrowserWindow, state: WindowNavigatio
}
const looksBlank =
shape.nodeCount < BLANK_PAGE_NODE_THRESHOLD &&
shape.textLength < BLANK_PAGE_TEXT_THRESHOLD
shape.nodeCount < BLANK_PAGE_NODE_THRESHOLD && shape.textLength < BLANK_PAGE_TEXT_THRESHOLD
if (!looksBlank) {
return
@@ -557,31 +556,28 @@ export function attachWindowDiagnostics(window: BrowserWindow, context: string):
clearBlankPageWatchdog(state)
})
window.webContents.on(
'did-start-navigation',
(_event, url, isInPlace, isMainFrame) => {
if (!isMainFrame || isInPlace || !/^https?:\/\//i.test(url)) {
return
}
window.webContents.on('did-start-navigation', (_event, url, isInPlace, isMainFrame) => {
if (!isMainFrame || isInPlace || !/^https?:\/\//i.test(url)) {
return
}
const state = navigationStateRegistry.get(window)
if (!state) {
return
}
const state = navigationStateRegistry.get(window)
if (!state) {
return
}
const fromLocalPage = isLocalPageURL(window.webContents.getURL())
state.readyForDetection = false
state.activeTargetURL = url
if (fromLocalPage) {
state.localPageKind = 'loading'
}
const fromLocalPage = isLocalPageURL(window.webContents.getURL())
state.readyForDetection = false
state.activeTargetURL = url
if (fromLocalPage) {
state.localPageKind = 'loading'
}
if (fromLocalPage || !state.loadTimeoutWatchdog) {
state.activeLoadToken += 1
scheduleRemoteLoadTimeout(window, state, url, context, state.activeLoadToken)
}
},
)
if (fromLocalPage || !state.loadTimeoutWatchdog) {
state.activeLoadToken += 1
scheduleRemoteLoadTimeout(window, state, url, context, state.activeLoadToken)
}
})
window.webContents.on(
'did-fail-load',
@@ -101,10 +101,7 @@ export function isLikelyPlatformAICredentialCookie(
return false
}
if (
genericAIPlatformRequiresVisibleCredentialSignal(platformId) &&
!hasVisibleAccountSignal
) {
if (genericAIPlatformRequiresVisibleCredentialSignal(platformId) && !hasVisibleAccountSignal) {
return false
}
@@ -50,7 +50,9 @@ export function getSavedLoginCredentials(): DesktopLoginCredentials | null {
}
}
export function saveLoginCredentials(credentials: DesktopLoginCredentials): DesktopLoginCredentials {
export function saveLoginCredentials(
credentials: DesktopLoginCredentials,
): DesktopLoginCredentials {
const identifier = credentials.identifier.trim()
if (!identifier || !credentials.password) {
clearSavedLoginCredentials()
@@ -396,12 +396,12 @@ const adapterRegistry = new Map<string, PlatformAdapter>(
: platform === 'weixin_gzh'
? weixinGzhAdapter
: platform === 'zol'
? zolAdapter
: platform === 'dongchedi'
? dongchediAdapter
: aiPlatformCatalog.some((item) => item.id === platform)
? aiAdapter
: genericAdapter,
? zolAdapter
: platform === 'dongchedi'
? dongchediAdapter
: aiPlatformCatalog.some((item) => item.id === platform)
? aiAdapter
: genericAdapter,
]),
)
@@ -1,9 +1,5 @@
import type {
ContextMenuParams,
MenuItemConstructorOptions,
WebContents,
} from 'electron/main'
import { clipboard, shell } from 'electron'
import type { ContextMenuParams, MenuItemConstructorOptions, WebContents } from 'electron/main'
import { Menu } from 'electron/main'
type WebContextMenuOptions = {