kiku-agent 1.3.2 → 1.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -12
- package/package.json +2 -1
- package/server/agent/agentCore.js +319 -46
- package/server/agent/appPaths.js +31 -0
- package/server/agent/audioPlayer.js +339 -12
- package/server/agent/autoContext.js +193 -0
- package/server/agent/catalogEntityResolver.js +170 -28
- package/server/agent/clarifyingQuestions.js +137 -35
- package/server/agent/cli.js +177 -48
- package/server/agent/cloudAdapters.js +116 -0
- package/server/agent/commandRegistry.js +409 -80
- package/server/agent/commands.js +1056 -125
- package/server/agent/config.js +1088 -690
- package/server/agent/contextCompactor.js +213 -21
- package/server/agent/cpuMetrics.js +199 -0
- package/server/agent/engineService.js +591 -0
- package/server/agent/eval/modules/evaluation/runner.js +4 -14
- package/server/agent/eval/modules/evaluation/scoring.js +15 -3
- package/server/agent/eval/modules/server/evalApi.js +5 -5
- package/server/agent/eval/modules/ui/evalUi.js +7 -4
- package/server/agent/eventBus.js +1 -0
- package/server/agent/ink/main.jsx +196 -191
- package/server/agent/jobManager.js +11 -1
- package/server/agent/lanDiscovery.js +721 -23
- package/server/agent/llmClient.js +317 -71
- package/server/agent/lmStudioWakeup.js +517 -65
- package/server/agent/locationService.js +797 -0
- package/server/agent/mcpQuizData.js +509 -0
- package/server/agent/memory/contextAllocator.js +19 -7
- package/server/agent/memory.js +3 -1
- package/server/agent/modelCompatibility.js +352 -0
- package/server/agent/modelDefaults.js +1 -0
- package/server/agent/modelManager.js +46 -2
- package/server/agent/newsWatch.js +9 -3
- package/server/agent/processManager.js +229 -0
- package/server/agent/prompts.js +174 -8
- package/server/agent/routingInterceptor.js +9 -1
- package/server/agent/sanitizer.js +41 -6
- package/server/agent/searchQueryPlanner.js +202 -0
- package/server/agent/session.js +59 -4
- package/server/agent/tests/agentFollowUps.test.js +128 -2
- package/server/agent/tests/agentMusicControl.test.js +245 -0
- package/server/agent/tests/apiRouter.test.js +1 -1
- package/server/agent/tests/autoContext.test.js +176 -0
- package/server/agent/tests/bilibiliEmbed.test.js +45 -0
- package/server/agent/tests/chatHardening.test.js +121 -0
- package/server/agent/tests/chineseWebSearchEscalation.test.js +62 -0
- package/server/agent/tests/clarifyingQuestions.test.js +33 -0
- package/server/agent/tests/cliAutomation.test.js +38 -1
- package/server/agent/tests/cloudProviders.test.js +91 -3
- package/server/agent/tests/commands.test.js +92 -3
- package/server/agent/tests/contextIntelligence.test.js +50 -3
- package/server/agent/tests/cpuMetrics.test.js +101 -0
- package/server/agent/tests/decisionAndSearchEnhancements.test.js +32 -0
- package/server/agent/tests/edgeCasesScenarios.test.js +82 -0
- package/server/agent/tests/engineService.test.js +436 -0
- package/server/agent/tests/engineUnload.test.js +94 -0
- package/server/agent/tests/envCommand.test.js +69 -0
- package/server/agent/tests/evalMetrics.test.js +8 -0
- package/server/agent/tests/fetchApi.test.js +99 -0
- package/server/agent/tests/hybridWebSearch.test.js +2 -0
- package/server/agent/tests/lanDiscovery.test.js +5 -0
- package/server/agent/tests/lanInterfaces.test.js +510 -0
- package/server/agent/tests/lmStudioWakeup.test.js +278 -0
- package/server/agent/tests/locationTool.test.js +278 -0
- package/server/agent/tests/memoryRoute.test.js +173 -0
- package/server/agent/tests/memoryUiNormalization.test.js +75 -0
- package/server/agent/tests/modelCompatibility.test.js +148 -0
- package/server/agent/tests/modelManager.test.js +32 -0
- package/server/agent/tests/musicControlUiBridge.test.js +113 -0
- package/server/agent/tests/nextGenArchitecture.test.js +4 -2
- package/server/agent/tests/openDomainPrompt.test.js +197 -0
- package/server/agent/tests/openDomainSearch.test.js +121 -0
- package/server/agent/tests/processManager.test.js +144 -0
- package/server/agent/tests/providerPersistence.test.js +10 -5
- package/server/agent/tests/quickActionsAndModals.test.js +180 -0
- package/server/agent/tests/searchQueryPlanner.test.js +108 -0
- package/server/agent/tests/securityAuditRemediation.test.js +16 -9
- package/server/agent/tests/securityAuditRemediations2.test.js +154 -0
- package/server/agent/tests/securityHardening.test.js +4 -4
- package/server/agent/tests/sessionAndCopy.test.js +39 -4
- package/server/agent/tests/sidebarControls.test.js +34 -0
- package/server/agent/tests/statefulMemory.test.js +8 -1
- package/server/agent/tests/textProtocolLoop.test.js +71 -0
- package/server/agent/tests/textToolParsing.test.js +48 -0
- package/server/agent/tests/toolRoutingRegressions.test.js +41 -0
- package/server/agent/tests/unifiedEndpoints.test.js +13 -6
- package/server/agent/tests/v110Features.test.js +1 -1
- package/server/agent/tests/vramEstimator.test.js +75 -0
- package/server/agent/tests/webChatDocs.test.js +199 -0
- package/server/agent/tests/webChatInferenceControl.test.js +298 -0
- package/server/agent/tests/webChatInspection.test.js +256 -5
- package/server/agent/tests/webChatMcp.test.js +94 -0
- package/server/agent/tests/webExtract.test.js +23 -0
- package/server/agent/tests/webSources.test.js +46 -12
- package/server/agent/textToolProtocol.js +173 -46
- package/server/agent/toolRouter.js +318 -44
- package/server/agent/tools.js +877 -96
- package/server/agent/ui.js +39 -34
- package/server/agent/vramService.js +35 -54
- package/server/agent/webSearch.js +441 -162
- package/server/agent/websearch/orchestrator.js +15 -5
- package/server/agent/websearch/resilience/cache.js +9 -2
- package/server/agent/websearch/security/ssrf.js +41 -10
- package/server/agent/youtubeAuth.js +5 -20
- package/server/apiRouter.js +5558 -4422
- package/server/mcp/data/discography.js +2 -2
- package/server/mcp/examples/antigravity.example.json +17 -0
- package/server/mcp/handler.js +8 -4
- package/server/mcp/security.js +220 -21
- package/server/mcp/server.js +2 -1
- package/server/mcp/tests/connection.test.js +14 -8
- package/server/mcp/tests/mcpKeyAuth.test.js +156 -0
- package/server/mcp/tools.js +7 -1
- package/server/supabase_migration_prefix.sql +102 -0
- package/server/supabase_schema.sql +18 -18
- package/server/web-chat/core/connection.js +179 -218
- package/server/web-chat/core/conversationContext.js +9 -2
- package/server/web-chat/core/directCatalogEngine.js +52 -28
- package/server/web-chat/core/dramaKnowledge.js +33 -22
- package/server/web-chat/core/followUpEngine.js +735 -81
- package/server/web-chat/core/futureEventGuard.js +5 -1
- package/server/web-chat/core/musicControlAction.js +70 -0
- package/server/web-chat/core/security.js +32 -21
- package/server/web-chat/core/toolInspector.js +6 -6
- package/server/web-chat/core/urlGuard.js +45 -0
- package/server/web-chat/router.js +204 -157
- package/server/web-chat/routes/chatRoute.js +885 -488
- package/server/web-chat/routes/engineRoute.js +270 -0
- package/server/web-chat/routes/exportRoute.js +1 -1
- package/server/web-chat/routes/lanDiscoveryRoute.js +17 -2
- package/server/web-chat/routes/lmStudioLoadRoute.js +172 -0
- package/server/web-chat/routes/mcpRoute.js +386 -0
- package/server/web-chat/routes/memoryRoute.js +253 -0
- package/server/web-chat/routes/providerRoute.js +49 -16
- package/server/web-chat/routes/staticRoute.js +306 -17
- package/server/web-chat/routes/statusRoute.js +194 -73
- package/server/web-chat/routes/suggestRoute.js +11 -3
- package/server/web-chat/routes/verifyRoute.js +26 -4
- package/server/web-chat/server.js +7 -6
- package/server/web-chat/ui/docsBuilder.js +3558 -0
- package/server/web-chat/ui/htmlBuilder.js +12 -0
- package/server/web-chat/ui/scripts/client.js +3401 -1302
- package/server/web-chat/ui/scripts/webchatI18n.js +155 -153
- package/server/web-chat/ui/settingsBuilder.js +7870 -0
- package/server/web-chat/ui/styles/lan-card.css +165 -0
- package/server/web-chat/ui/styles/main.css +4289 -447
- package/server/web-chat/ui/sw.js +64 -0
- package/server/web-chat/ui/templates/body.html +407 -281
- package/src/data/awardsData.js +146 -146
- package/src/data/musicVideoData.js +6 -4
- package/src/lib/vram/estimator.js +195 -19
|
@@ -3,17 +3,20 @@ import { execFile } from 'node:child_process';
|
|
|
3
3
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
+
import { resolveDynamicWindowsAppPaths } from './appPaths.js';
|
|
6
7
|
import http from 'node:http';
|
|
7
8
|
import https from 'node:https';
|
|
8
9
|
import { lookup as dnsLookup } from 'node:dns/promises';
|
|
9
10
|
import { assertSafeUrlHostname, sanitizeUrl, isPrivateIPv4, isPrivateIPv6, isIpAddress } from './sanitizer.js';
|
|
10
|
-
import { getContextualizedSearchQuery } from './catalogEntityResolver.js';
|
|
11
|
+
import { getContextualizedSearchQuery, isQueryInDomain, THAI_INTENT_MAP } from './catalogEntityResolver.js';
|
|
12
|
+
import { dedupeNewsItems, rankNewsItems } from './newsIntelligence.js';
|
|
11
13
|
import { galleryData } from '../../src/data/galleryData.js';
|
|
12
14
|
import { discographyData } from '../../src/data/discographyData.js';
|
|
13
15
|
import { musicVideoData } from '../../src/data/musicVideoData.js';
|
|
14
16
|
import { profileData } from '../../src/data/profileData.js';
|
|
15
17
|
import { defaultMetaSearch, hybridWebSearch } from './websearch/index.js';
|
|
16
18
|
import { computeSearchCacheKey, TTL_GENERAL_MS } from './websearch/resilience/cache.js';
|
|
19
|
+
import { appendDomainFilters, planSearchRequest, rerankSearchResults } from './searchQueryPlanner.js';
|
|
17
20
|
import {
|
|
18
21
|
decodeHtmlBytes,
|
|
19
22
|
decodeEntities,
|
|
@@ -156,14 +159,17 @@ export async function safeFetch(inputUrl, options = {}, maxRedirects = 5) {
|
|
|
156
159
|
let currentOptions = { ...options };
|
|
157
160
|
|
|
158
161
|
for (let redirects = 0; redirects <= maxRedirects; redirects++) {
|
|
159
|
-
const sanitized = sanitizeUrl(currentUrl);
|
|
162
|
+
const sanitized = sanitizeUrl(currentUrl, { allowLoopback: Boolean(currentOptions.allowLoopback) });
|
|
160
163
|
if (!sanitized.safe) throw new Error(`URL rejected by SSRF guard: ${sanitized.reason}`);
|
|
161
164
|
|
|
162
165
|
const parsedUrl = sanitized.url;
|
|
163
166
|
const hostname = sanitized.hostname;
|
|
167
|
+
const isLoopback = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
|
|
164
168
|
|
|
165
169
|
let targetIp = hostname;
|
|
166
|
-
if (
|
|
170
|
+
if (currentOptions.allowLoopback && isLoopback) {
|
|
171
|
+
targetIp = hostname;
|
|
172
|
+
} else if (!isIpAddress(hostname)) {
|
|
167
173
|
const addresses = await safeFetchDnsLookup(hostname);
|
|
168
174
|
if (!addresses || addresses.length === 0) {
|
|
169
175
|
throw new Error(`Unable to resolve hostname ${hostname}`);
|
|
@@ -603,14 +609,47 @@ async function searchBing(query, maxResults, { language } = {}) {
|
|
|
603
609
|
return results;
|
|
604
610
|
}
|
|
605
611
|
|
|
612
|
+
function prepareSearchQuery(rawQuery) {
|
|
613
|
+
let q = String(rawQuery || '').trim();
|
|
614
|
+
if (!q) return '';
|
|
615
|
+
for (const [regex, replacement] of THAI_INTENT_MAP) {
|
|
616
|
+
if (regex.test(q)) {
|
|
617
|
+
q = `${q} ${replacement}`;
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return q;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function buildQueryVariants(rawQuery) {
|
|
625
|
+
const q = String(rawQuery || '').trim();
|
|
626
|
+
const variants = [];
|
|
627
|
+
const noQuotes = q.replace(/["'«»“”„]/g, ' ').replace(/\s+/g, ' ').trim();
|
|
628
|
+
if (noQuotes && noQuotes !== q) variants.push(noQuotes);
|
|
629
|
+
|
|
630
|
+
const cleanStopwords = noQuotes.replace(/\b(what|where|who|is|are|tell me|about|details|news|ขอข้อมูล|เกี่ยวกับ|คือใคร|มีอะไรบ้าง|ไหม|มั้ย)\b/gi, ' ').replace(/\s+/g, ' ').trim();
|
|
631
|
+
if (cleanStopwords && cleanStopwords !== noQuotes && cleanStopwords.length > 2) {
|
|
632
|
+
variants.push(cleanStopwords);
|
|
633
|
+
}
|
|
634
|
+
return variants;
|
|
635
|
+
}
|
|
636
|
+
|
|
606
637
|
export async function searchWeb(query, maxResults = 8, options = {}) {
|
|
607
638
|
if (!query || typeof query !== 'string') {
|
|
608
639
|
return { status: 'error', message: 'Query string is required for web search.' };
|
|
609
640
|
}
|
|
610
641
|
|
|
611
|
-
const
|
|
642
|
+
const ownedPlan = !options.queryPlan;
|
|
643
|
+
const queryPlan = options.queryPlan || planSearchRequest({
|
|
644
|
+
keywords: query,
|
|
645
|
+
target_language: options.targetLanguage,
|
|
646
|
+
domain_filter: options.allowedDomains,
|
|
647
|
+
}, { kind: 'web' });
|
|
648
|
+
const originalQuery = options.originalQuery || queryPlan.originalQuery || query;
|
|
649
|
+
const fetchBase = ownedPlan ? (queryPlan.keywords || query) : query;
|
|
650
|
+
const lang = queryPlan.targetLanguage || detectLanguage(fetchBase);
|
|
612
651
|
const capped = Math.min(Math.max(maxResults, 1), 25);
|
|
613
|
-
const cacheKey = computeSearchCacheKey('searchWeb', query, capped);
|
|
652
|
+
const cacheKey = computeSearchCacheKey('searchWeb', `${query}|${queryPlan.domains.join(',')}`, capped);
|
|
614
653
|
|
|
615
654
|
if (!options.noCache) {
|
|
616
655
|
const cached = defaultMetaSearch.cache.get(cacheKey);
|
|
@@ -619,68 +658,91 @@ export async function searchWeb(query, maxResults = 8, options = {}) {
|
|
|
619
658
|
}
|
|
620
659
|
}
|
|
621
660
|
|
|
622
|
-
|
|
623
|
-
const [metaResSettled, imgResSettled] = await Promise.allSettled([
|
|
624
|
-
defaultMetaSearch.executeSearch(query, {
|
|
625
|
-
limit: capped,
|
|
626
|
-
language: lang,
|
|
627
|
-
...options
|
|
628
|
-
}),
|
|
629
|
-
searchWebImages(query, 6).catch(() => null)
|
|
630
|
-
]);
|
|
661
|
+
const preparedQuery = appendDomainFilters(options.skipPrep ? fetchBase : prepareSearchQuery(fetchBase), queryPlan.domains);
|
|
631
662
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
663
|
+
const runAttempt = async (targetQuery) => {
|
|
664
|
+
try {
|
|
665
|
+
const [metaResSettled, imgResSettled] = await Promise.allSettled([
|
|
666
|
+
defaultMetaSearch.executeSearch(targetQuery, {
|
|
667
|
+
limit: capped,
|
|
668
|
+
language: lang,
|
|
669
|
+
...options
|
|
670
|
+
}),
|
|
671
|
+
searchWebImages(targetQuery, 6, { queryPlan }).catch(() => null)
|
|
672
|
+
]);
|
|
635
673
|
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
count: metaRes.results.length,
|
|
643
|
-
sources,
|
|
644
|
-
results: metaRes.results.map(r => ({
|
|
674
|
+
const metaRes = metaResSettled.status === 'fulfilled' ? metaResSettled.value : null;
|
|
675
|
+
const imgRes = imgResSettled.status === 'fulfilled' ? imgResSettled.value : null;
|
|
676
|
+
const images = Array.isArray(imgRes?.results) ? imgRes.results.slice(0, 6) : [];
|
|
677
|
+
|
|
678
|
+
if (metaRes && metaRes.results && metaRes.results.length > 0) {
|
|
679
|
+
const rankedResults = rerankSearchResults(metaRes.results.map(r => ({
|
|
645
680
|
title: r.title,
|
|
646
681
|
url: r.link,
|
|
647
682
|
snippet: r.snippet,
|
|
648
683
|
source: r.engine || 'web'
|
|
649
|
-
})),
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
684
|
+
})), queryPlan, { limit: capped });
|
|
685
|
+
const sources = [...new Set(rankedResults.map(r => r.source || 'web'))];
|
|
686
|
+
const resultObj = {
|
|
687
|
+
status: 'success',
|
|
688
|
+
query: originalQuery,
|
|
689
|
+
normalizedQuery: queryPlan.keywords,
|
|
690
|
+
language: lang,
|
|
691
|
+
count: rankedResults.length,
|
|
692
|
+
sources,
|
|
693
|
+
results: rankedResults,
|
|
694
|
+
...(images.length > 0 ? { images } : {}),
|
|
695
|
+
...(metaRes.mode === 'deep_scrape' ? {
|
|
696
|
+
scrapedPages: metaRes.scrapedPages,
|
|
697
|
+
combinedContent: metaRes.combinedContent
|
|
698
|
+
} : {})
|
|
699
|
+
};
|
|
700
|
+
if (!options.noCache) {
|
|
701
|
+
defaultMetaSearch.cache.set(cacheKey, resultObj, TTL_GENERAL_MS);
|
|
702
|
+
}
|
|
703
|
+
return resultObj;
|
|
658
704
|
}
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
} catch {}
|
|
705
|
+
} catch {}
|
|
662
706
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
707
|
+
try {
|
|
708
|
+
const wikiItems = await searchWikipedia(targetQuery, Math.min(capped, 3), lang);
|
|
709
|
+
if (wikiItems.length > 0) {
|
|
710
|
+
const wikiResponse = {
|
|
711
|
+
status: 'success',
|
|
712
|
+
query: originalQuery,
|
|
713
|
+
normalizedQuery: queryPlan.keywords,
|
|
714
|
+
language: lang,
|
|
715
|
+
count: wikiItems.length,
|
|
716
|
+
sources: ['Wikipedia'],
|
|
717
|
+
results: wikiItems
|
|
718
|
+
};
|
|
719
|
+
if (!options.noCache) {
|
|
720
|
+
defaultMetaSearch.cache.set(cacheKey, wikiResponse, TTL_GENERAL_MS);
|
|
721
|
+
}
|
|
722
|
+
return wikiResponse;
|
|
676
723
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
724
|
+
} catch {}
|
|
725
|
+
return null;
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const primaryResult = await runAttempt(preparedQuery);
|
|
729
|
+
if (primaryResult) return primaryResult;
|
|
730
|
+
|
|
731
|
+
if (preparedQuery !== query) {
|
|
732
|
+
const rawResult = await runAttempt(query);
|
|
733
|
+
if (rawResult) return rawResult;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const variants = buildQueryVariants(query);
|
|
737
|
+
for (const variant of variants) {
|
|
738
|
+
const variantResult = await runAttempt(variant);
|
|
739
|
+
if (variantResult) return variantResult;
|
|
740
|
+
}
|
|
680
741
|
|
|
681
742
|
return {
|
|
682
743
|
status: 'no_results',
|
|
683
|
-
query,
|
|
744
|
+
query: originalQuery,
|
|
745
|
+
normalizedQuery: queryPlan.keywords,
|
|
684
746
|
language: lang,
|
|
685
747
|
count: 0,
|
|
686
748
|
sources: [],
|
|
@@ -691,11 +753,31 @@ export async function searchWeb(query, maxResults = 8, options = {}) {
|
|
|
691
753
|
const PAGE_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
692
754
|
const pageContentCache = new BoundedLruCache(40);
|
|
693
755
|
|
|
694
|
-
function buildPageResult(pageData, { finalUrl, maxLength, query, cached }) {
|
|
756
|
+
export function buildPageResult(pageData, { finalUrl, maxLength, query, cached }) {
|
|
695
757
|
const markdown = pageData.markdown || '';
|
|
758
|
+
const totalChars = markdown.length;
|
|
759
|
+
|
|
760
|
+
if (!markdown.trim()) {
|
|
761
|
+
return {
|
|
762
|
+
status: 'error',
|
|
763
|
+
url: finalUrl,
|
|
764
|
+
contentType: 'html',
|
|
765
|
+
contentLength: 0,
|
|
766
|
+
totalPageChars: 0,
|
|
767
|
+
message: 'No readable text could be extracted from this page (login-walled or JavaScript-rendered site such as Weibo, Douyin or Xiaohongshu). Do NOT present this as the final answer — retry in the same turn through an alternate route: (1) the mobile mirror of the URL (e.g. weibo.com → m.weibo.cn keeping the same path), (2) search_chinese_web with the topic keywords, (3) search_news on the topic, or (4) web_search for corroborating pages. Only tell the user the page is unavailable after an alternate route also returns nothing.',
|
|
768
|
+
meta: pageData.meta || {},
|
|
769
|
+
extraction: {
|
|
770
|
+
engine: pageData.engine || 'cheerio-readability',
|
|
771
|
+
mode: 'no_content',
|
|
772
|
+
cached: Boolean(cached),
|
|
773
|
+
charset: pageData.charset || 'utf-8',
|
|
774
|
+
quality: pageData.quality || null
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
|
|
696
779
|
let content = markdown;
|
|
697
780
|
let contentMode = 'full';
|
|
698
|
-
const totalChars = markdown.length;
|
|
699
781
|
|
|
700
782
|
if (query) {
|
|
701
783
|
const passages = splitIntoPassages(markdown);
|
|
@@ -718,7 +800,7 @@ function buildPageResult(pageData, { finalUrl, maxLength, query, cached }) {
|
|
|
718
800
|
contentType: 'html',
|
|
719
801
|
contentLength: content.length,
|
|
720
802
|
totalPageChars: totalChars,
|
|
721
|
-
content
|
|
803
|
+
content,
|
|
722
804
|
meta: pageData.meta || {},
|
|
723
805
|
extraction: {
|
|
724
806
|
engine: pageData.engine || 'cheerio-readability',
|
|
@@ -964,7 +1046,7 @@ export async function fetchJson(url, options = {}) {
|
|
|
964
1046
|
} catch {
|
|
965
1047
|
return { status: 'error', message: 'Invalid URL format.' };
|
|
966
1048
|
}
|
|
967
|
-
const ssrf = await assertSafeUrlHostname(parsedUrl.hostname);
|
|
1049
|
+
const ssrf = await assertSafeUrlHostname(parsedUrl.hostname, { allowLoopback: Boolean(options.allowLoopback) });
|
|
968
1050
|
if (!ssrf.safe) {
|
|
969
1051
|
return { status: 'error', message: `URL rejected by SSRF guard: ${ssrf.reason}` };
|
|
970
1052
|
}
|
|
@@ -988,7 +1070,8 @@ export async function fetchJson(url, options = {}) {
|
|
|
988
1070
|
'User-Agent': 'KikuAgent/2.0',
|
|
989
1071
|
...headers
|
|
990
1072
|
},
|
|
991
|
-
signal: controller.signal
|
|
1073
|
+
signal: controller.signal,
|
|
1074
|
+
allowLoopback: Boolean(options.allowLoopback)
|
|
992
1075
|
};
|
|
993
1076
|
|
|
994
1077
|
if (body && ['POST', 'PUT', 'PATCH'].includes(fetchOptions.method)) {
|
|
@@ -1056,13 +1139,7 @@ export async function fetchJson(url, options = {}) {
|
|
|
1056
1139
|
function findChromePath() {
|
|
1057
1140
|
const p = process.platform;
|
|
1058
1141
|
if (p === 'win32') {
|
|
1059
|
-
const candidates =
|
|
1060
|
-
(process.env.LOCALAPPDATA || '') + '/Google/Chrome/Application/chrome.exe',
|
|
1061
|
-
(process.env['PROGRAMFILES'] || '') + '/Google/Chrome/Application/chrome.exe',
|
|
1062
|
-
(process.env['PROGRAMFILES(X86)'] || '') + '/Google/Chrome/Application/chrome.exe',
|
|
1063
|
-
'C:/Program Files/Google/Chrome/Application/chrome.exe',
|
|
1064
|
-
'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe'
|
|
1065
|
-
];
|
|
1142
|
+
const candidates = resolveDynamicWindowsAppPaths('Google', 'Chrome', 'Application', 'chrome.exe');
|
|
1066
1143
|
for (const c of candidates) {
|
|
1067
1144
|
if (c && existsSync(c)) return c;
|
|
1068
1145
|
}
|
|
@@ -1303,69 +1380,88 @@ async function fetchYahooNewsRss(query, maxResults) {
|
|
|
1303
1380
|
}
|
|
1304
1381
|
}
|
|
1305
1382
|
|
|
1306
|
-
export async function searchNews(query, maxResults = 8) {
|
|
1383
|
+
export async function searchNews(query, maxResults = 8, options = {}) {
|
|
1307
1384
|
if (!query || typeof query !== 'string') {
|
|
1308
1385
|
return { status: 'error', message: 'Query string is required for news search.' };
|
|
1309
1386
|
}
|
|
1310
1387
|
|
|
1388
|
+
const ownedPlan = !options.queryPlan;
|
|
1389
|
+
const queryPlan = options.queryPlan || planSearchRequest(query, { kind: 'news' });
|
|
1390
|
+
const searchBase = ownedPlan ? (queryPlan.keywords || query) : query;
|
|
1311
1391
|
const capped = Math.min(Math.max(maxResults, 1), 20);
|
|
1312
|
-
const lang = detectLanguage(
|
|
1313
|
-
const results = [];
|
|
1392
|
+
const lang = queryPlan.targetLanguage || detectLanguage(searchBase);
|
|
1314
1393
|
const sources = [];
|
|
1315
|
-
const attempts = [];
|
|
1316
1394
|
const seenUrls = new Set();
|
|
1395
|
+
const rawItems = [];
|
|
1317
1396
|
|
|
1318
1397
|
const add = (item, defaultSource) => {
|
|
1319
1398
|
if (!item?.url || seenUrls.has(item.url)) return;
|
|
1320
1399
|
seenUrls.add(item.url);
|
|
1321
1400
|
const src = item.source || defaultSource;
|
|
1322
|
-
|
|
1401
|
+
rawItems.push({ ...item, source: src });
|
|
1323
1402
|
if (!sources.includes(src)) sources.push(src);
|
|
1324
1403
|
};
|
|
1325
1404
|
|
|
1326
|
-
const
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1405
|
+
const isArtistQuery = /(?:จวีจิ้งอี|จิ้งอี|ju\s*jing\s*yi|kiku|鞠婧祎|小鞠)/i.test(`${query} ${searchBase}`);
|
|
1406
|
+
const cleanLocalQuery = searchBase
|
|
1407
|
+
.replace(/(?:ข่าวล่าสุด|ข่าวใหม่|ข่าวสาร|ข่าว|ล่าสุด|เมื่อเร็วๆ\s*นี้|update|news|when:\d+d)/gi, ' ')
|
|
1408
|
+
.replace(/\s+/g, ' ')
|
|
1409
|
+
.trim() || searchBase;
|
|
1410
|
+
const filteredLocalQuery = appendDomainFilters(cleanLocalQuery, queryPlan.domains);
|
|
1411
|
+
|
|
1412
|
+
const cnQuery = isArtistQuery ? getContextualizedSearchQuery(searchBase, 'chinese_web') : null;
|
|
1413
|
+
|
|
1414
|
+
const tasks = [
|
|
1415
|
+
fetchGoogleNewsRss(filteredLocalQuery, capped, lang),
|
|
1416
|
+
fetchBingNewsRss(filteredLocalQuery, capped, lang),
|
|
1417
|
+
fetchYahooNewsRss(filteredLocalQuery, Math.ceil(capped / 2)),
|
|
1418
|
+
searchDuckDuckGo(filteredLocalQuery, Math.min(capped, 4)),
|
|
1419
|
+
searchWebImages(`${filteredLocalQuery} news`, 6, { queryPlan }).catch(() => null)
|
|
1420
|
+
];
|
|
1421
|
+
|
|
1422
|
+
if (cnQuery && lang !== 'zh') {
|
|
1423
|
+
tasks.push(
|
|
1424
|
+
fetchGoogleNewsRss(cnQuery, capped, 'zh').catch(() => []),
|
|
1425
|
+
fetchBingNewsRss(cnQuery, capped, 'zh').catch(() => [])
|
|
1426
|
+
);
|
|
1338
1427
|
}
|
|
1339
1428
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1429
|
+
const settled = await Promise.allSettled(tasks);
|
|
1430
|
+
|
|
1431
|
+
const gList = (settled[0]?.status === 'fulfilled' && Array.isArray(settled[0].value)) ? settled[0].value : [];
|
|
1432
|
+
const bList = (settled[1]?.status === 'fulfilled' && Array.isArray(settled[1].value)) ? settled[1].value : [];
|
|
1433
|
+
const yList = (settled[2]?.status === 'fulfilled' && Array.isArray(settled[2].value)) ? settled[2].value : [];
|
|
1434
|
+
const ddgList = (settled[3]?.status === 'fulfilled' && Array.isArray(settled[3].value)) ? settled[3].value : [];
|
|
1435
|
+
const imgRes = settled[4]?.status === 'fulfilled' ? settled[4].value : null;
|
|
1436
|
+
const gCnList = (settled[5]?.status === 'fulfilled' && Array.isArray(settled[5].value)) ? settled[5].value : [];
|
|
1437
|
+
const bCnList = (settled[6]?.status === 'fulfilled' && Array.isArray(settled[6].value)) ? settled[6].value : [];
|
|
1345
1438
|
|
|
1346
|
-
const gList = (gSettled.status === 'fulfilled' && Array.isArray(gSettled.value)) ? gSettled.value : [];
|
|
1347
|
-
const bList = (bSettled.status === 'fulfilled' && Array.isArray(bSettled.value)) ? bSettled.value : [];
|
|
1348
|
-
const yList = (ySettled.status === 'fulfilled' && Array.isArray(ySettled.value)) ? ySettled.value : [];
|
|
1349
|
-
const ddgList = (ddgSettled.status === 'fulfilled' && Array.isArray(ddgSettled.value)) ? ddgSettled.value : [];
|
|
1350
|
-
const imgRes = imgSettled.status === 'fulfilled' ? imgSettled.value : null;
|
|
1351
1439
|
const newsImages = Array.isArray(imgRes?.results) ? imgRes.results.slice(0, 6) : [];
|
|
1352
1440
|
|
|
1353
1441
|
let i = 0;
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
if (i <
|
|
1357
|
-
if (i <
|
|
1358
|
-
if (i <
|
|
1442
|
+
const maxLen = Math.max(gList.length, bList.length, yList.length, ddgList.length, gCnList.length, bCnList.length);
|
|
1443
|
+
while (i < maxLen) {
|
|
1444
|
+
if (i < gList.length) add(gList[i], 'Google News');
|
|
1445
|
+
if (i < bList.length) add(bList[i], 'Bing News');
|
|
1446
|
+
if (i < gCnList.length) add(gCnList[i], 'Google News (CN)');
|
|
1447
|
+
if (i < bCnList.length) add(bCnList[i], 'Bing News (CN)');
|
|
1448
|
+
if (i < yList.length) add(yList[i], 'Yahoo News');
|
|
1449
|
+
if (i < ddgList.length) add({ ...ddgList[i], url: ddgList[i].url || ddgList[i].link }, 'Web News');
|
|
1359
1450
|
i++;
|
|
1360
1451
|
}
|
|
1361
1452
|
|
|
1453
|
+
const deduped = dedupeNewsItems(rawItems);
|
|
1454
|
+
const ranked = rankNewsItems(deduped);
|
|
1455
|
+
const finalResults = rerankSearchResults(ranked, queryPlan, { limit: capped });
|
|
1456
|
+
|
|
1362
1457
|
return {
|
|
1363
|
-
status:
|
|
1364
|
-
query,
|
|
1458
|
+
status: finalResults.length > 0 ? 'success' : 'no_results',
|
|
1459
|
+
query: queryPlan.originalQuery || query,
|
|
1460
|
+
normalizedQuery: queryPlan.keywords,
|
|
1365
1461
|
language: lang,
|
|
1366
|
-
count:
|
|
1462
|
+
count: finalResults.length,
|
|
1367
1463
|
sources,
|
|
1368
|
-
results,
|
|
1464
|
+
results: finalResults,
|
|
1369
1465
|
...(newsImages.length > 0 ? { images: newsImages } : {})
|
|
1370
1466
|
};
|
|
1371
1467
|
}
|
|
@@ -1869,6 +1965,23 @@ export function parseBaiduResults(html, maxResults = 8) {
|
|
|
1869
1965
|
results.push({ title, url: match[1], snippet, source: 'Baidu' });
|
|
1870
1966
|
}
|
|
1871
1967
|
}
|
|
1968
|
+
|
|
1969
|
+
if (results.length === 0) {
|
|
1970
|
+
const altRegex = /<(?:a[^>]+href="([^"]+)"[^>]*>[\s\S]*?<h3[^>]*>([\s\S]*?)<\/h3>|<h3[^>]*>[\s\S]*?<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>|<div[^>]+class="[^"]*c-title[^"]*"[^>]*>[\s\S]*?<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>)([\s\S]*?)(?=<article|<div[^>]+class="[^"]*c-result|<h3|<\/body>|$)/gi;
|
|
1971
|
+
while ((match = altRegex.exec(html)) !== null && results.length < maxResults) {
|
|
1972
|
+
const url = match[1] || match[3] || match[5];
|
|
1973
|
+
const rawTitle = match[2] || match[4] || match[6];
|
|
1974
|
+
const afterText = match[7] || '';
|
|
1975
|
+
if (!url || !rawTitle) continue;
|
|
1976
|
+
const title = cleanHtml(rawTitle);
|
|
1977
|
+
const snippet = cleanHtml(afterText).replace(/["']?\}[\],\}\s]+.*$/g, '').trim().slice(0, 320);
|
|
1978
|
+
if (title && url && !seenUrls.has(url)) {
|
|
1979
|
+
seenUrls.add(url);
|
|
1980
|
+
results.push({ title, url, snippet, source: 'Baidu' });
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1872
1985
|
return results;
|
|
1873
1986
|
}
|
|
1874
1987
|
|
|
@@ -1877,9 +1990,13 @@ export async function searchChineseWeb(query, maxResults = 8, options = {}) {
|
|
|
1877
1990
|
return { status: 'error', message: 'Query string is required for Chinese web search.' };
|
|
1878
1991
|
}
|
|
1879
1992
|
|
|
1880
|
-
const
|
|
1881
|
-
|
|
1882
|
-
|
|
1993
|
+
const ownedPlan = !options.queryPlan;
|
|
1994
|
+
const queryPlan = options.queryPlan || planSearchRequest({ keywords: query, domain_filter: options.allowedDomains }, { kind: 'chinese' });
|
|
1995
|
+
const fetchBase = ownedPlan ? (queryPlan.keywords || query) : query;
|
|
1996
|
+
const enrichedQuery = typeof options.queryEnricher === 'function'
|
|
1997
|
+
? options.queryEnricher(fetchBase, 'chinese_web')
|
|
1998
|
+
: getContextualizedSearchQuery(fetchBase, 'chinese_web');
|
|
1999
|
+
const effectiveQuery = appendDomainFilters(enrichedQuery, queryPlan.domains);
|
|
1883
2000
|
|
|
1884
2001
|
const capped = Math.min(Math.max(maxResults, 1), 20);
|
|
1885
2002
|
const results = [];
|
|
@@ -1896,21 +2013,51 @@ export async function searchChineseWeb(query, maxResults = 8, options = {}) {
|
|
|
1896
2013
|
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
1897
2014
|
'Referer': 'https://www.baidu.com/'
|
|
1898
2015
|
}
|
|
1899
|
-
},
|
|
2016
|
+
}, 4500);
|
|
1900
2017
|
if (res.ok) {
|
|
1901
2018
|
const html = await readResponseTextLimited(res, 3_000_000);
|
|
1902
|
-
if (html.includes('百度安全验证')
|
|
2019
|
+
if (!html.includes('百度安全验证') && !html.includes('wappass.baidu.com')) {
|
|
2020
|
+
const items = parseBaiduResults(html, capped);
|
|
2021
|
+
if (items.length > 0) {
|
|
2022
|
+
attempts.push({ source: 'Baidu', status: 'success', count: items.length, durationMs: Date.now() - t0 });
|
|
2023
|
+
return items;
|
|
2024
|
+
}
|
|
2025
|
+
} else {
|
|
1903
2026
|
attempts.push({ source: 'Baidu', status: 'blocked', message: 'Baidu anti-bot verification challenge.', durationMs: Date.now() - t0 });
|
|
1904
|
-
return [];
|
|
1905
2027
|
}
|
|
1906
|
-
|
|
1907
|
-
attempts.push({ source: 'Baidu', status:
|
|
1908
|
-
return items;
|
|
2028
|
+
} else {
|
|
2029
|
+
attempts.push({ source: 'Baidu', status: 'error', message: `HTTP ${res.status}`, durationMs: Date.now() - t0 });
|
|
1909
2030
|
}
|
|
1910
|
-
attempts.push({ source: 'Baidu', status: 'error', message: `HTTP ${res.status}`, durationMs: Date.now() - t0 });
|
|
1911
2031
|
} catch (err) {
|
|
1912
|
-
attempts.push({ source: 'Baidu', status: 'error', message: err.name === 'AbortError' ? 'Baidu timeout (
|
|
2032
|
+
attempts.push({ source: 'Baidu', status: 'error', message: err.name === 'AbortError' ? 'Baidu timeout (4.5s)' : err.message, durationMs: Date.now() - t0 });
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
// Mobile Baidu fallback (less prone to desktop captcha challenges)
|
|
2036
|
+
try {
|
|
2037
|
+
const tMobile = Date.now();
|
|
2038
|
+
const mobileUrl = `https://m.baidu.com/s?word=${encodeURIComponent(effectiveQuery)}`;
|
|
2039
|
+
const resM = await fetchWithTimeout(mobileUrl, {
|
|
2040
|
+
headers: {
|
|
2041
|
+
...BROWSER_HEADERS,
|
|
2042
|
+
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
|
2043
|
+
'Accept-Language': 'zh-CN,zh;q=0.9',
|
|
2044
|
+
'Referer': 'https://m.baidu.com/'
|
|
2045
|
+
}
|
|
2046
|
+
}, 3500);
|
|
2047
|
+
if (resM.ok) {
|
|
2048
|
+
const htmlM = await readResponseTextLimited(resM, 3_000_000);
|
|
2049
|
+
if (!htmlM.includes('百度安全验证') && !htmlM.includes('wappass.baidu.com')) {
|
|
2050
|
+
const mItems = parseBaiduResults(htmlM, capped);
|
|
2051
|
+
if (mItems.length > 0) {
|
|
2052
|
+
attempts.push({ source: 'Baidu (Mobile)', status: 'success', count: mItems.length, durationMs: Date.now() - tMobile });
|
|
2053
|
+
return mItems;
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
} catch (_mErr) {
|
|
2058
|
+
// Graceful mobile fallback failure
|
|
1913
2059
|
}
|
|
2060
|
+
|
|
1914
2061
|
return [];
|
|
1915
2062
|
};
|
|
1916
2063
|
|
|
@@ -2012,7 +2159,7 @@ export async function searchChineseWeb(query, maxResults = 8, options = {}) {
|
|
|
2012
2159
|
const fetchOpenWeb = async () => {
|
|
2013
2160
|
const t0 = Date.now();
|
|
2014
2161
|
try {
|
|
2015
|
-
const cnDomains = options.site ? [options.site] : ['sina.com.cn', 'sohu.com', 'weibo.com', '163.com', 'qq.com'];
|
|
2162
|
+
const cnDomains = options.site ? [options.site] : (queryPlan.domains.length > 0 ? queryPlan.domains : ['sina.com.cn', 'sohu.com', 'weibo.com', '163.com', 'qq.com']);
|
|
2016
2163
|
const ddgPromises = cnDomains.slice(0, 3).map(domain =>
|
|
2017
2164
|
searchDuckDuckGo(`${effectiveQuery} site:${domain}`, 2)
|
|
2018
2165
|
.then(items => items.map(it => ({ ...it, source: `Web (${domain})` })))
|
|
@@ -2078,13 +2225,15 @@ export async function searchChineseWeb(query, maxResults = 8, options = {}) {
|
|
|
2078
2225
|
}
|
|
2079
2226
|
} catch {}
|
|
2080
2227
|
|
|
2228
|
+
const rankedResults = rerankSearchResults(results, queryPlan, { limit: capped });
|
|
2081
2229
|
return {
|
|
2082
2230
|
status: results.length > 0 ? 'success' : 'no_results',
|
|
2083
|
-
query,
|
|
2084
|
-
|
|
2231
|
+
query: queryPlan.originalQuery || query,
|
|
2232
|
+
normalizedQuery: queryPlan.keywords,
|
|
2233
|
+
count: rankedResults.length,
|
|
2085
2234
|
sources: [...new Set(sources)],
|
|
2086
2235
|
attempts,
|
|
2087
|
-
results,
|
|
2236
|
+
results: rankedResults,
|
|
2088
2237
|
...(cnImages.length > 0 ? { images: cnImages } : {})
|
|
2089
2238
|
};
|
|
2090
2239
|
}
|
|
@@ -2119,20 +2268,76 @@ export function decodeBaiduUrl(url) {
|
|
|
2119
2268
|
return res;
|
|
2120
2269
|
}
|
|
2121
2270
|
|
|
2122
|
-
|
|
2271
|
+
const ARTIST_IMAGE_TOKEN_RE = /(鞠婧祎|ju\s?jing\s?yi|jujingyi|จวีจิ้งอี|จิ้งอี|小鞠|kikuu?|蜜橘)/i;
|
|
2272
|
+
|
|
2273
|
+
export function titleMentionsArtist(title) {
|
|
2274
|
+
return ARTIST_IMAGE_TOKEN_RE.test(String(title || '').replace(/<[^>]*>/g, ''));
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
export function isLowValueImageUrl(url) {
|
|
2278
|
+
const raw = String(url || '');
|
|
2279
|
+
if (!raw) return true;
|
|
2280
|
+
if (/^data:/i.test(raw)) return true;
|
|
2281
|
+
if (/\.svg(?:[?#]|$)/i.test(raw)) return true;
|
|
2282
|
+
return /\/(?:logo|icons?|favicon|avatar|emoji|sprite)[^/]*$/i.test(raw);
|
|
2283
|
+
}
|
|
2284
|
+
|
|
2285
|
+
export function imageHostKey(url) {
|
|
2286
|
+
try {
|
|
2287
|
+
const parsed = new URL(url);
|
|
2288
|
+
return parsed.host + parsed.pathname.toLowerCase();
|
|
2289
|
+
} catch {
|
|
2290
|
+
return String(url || '');
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
export function buildImageSearchQuery(query) {
|
|
2295
|
+
if (!isQueryInDomain(query)) {
|
|
2296
|
+
return String(query || '').trim();
|
|
2297
|
+
}
|
|
2298
|
+
const effective = getContextualizedSearchQuery(query, 'chinese_web');
|
|
2299
|
+
if (ARTIST_IMAGE_TOKEN_RE.test(effective)) return effective;
|
|
2300
|
+
return ('鞠婧祎 ' + effective).trim();
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
export async function searchWebImages(query, maxResults = 8, options = {}) {
|
|
2123
2304
|
if (!query || typeof query !== 'string') {
|
|
2124
2305
|
return { status: 'error', message: 'Query string is required for image search.' };
|
|
2125
2306
|
}
|
|
2126
2307
|
|
|
2127
|
-
const
|
|
2308
|
+
const ownedPlan = !options.queryPlan;
|
|
2309
|
+
const queryPlan = options.queryPlan || planSearchRequest(query, { kind: 'image' });
|
|
2310
|
+
const fetchBase = ownedPlan ? (queryPlan.keywords || query) : query;
|
|
2311
|
+
const inDomain = isQueryInDomain(fetchBase);
|
|
2312
|
+
const effectiveQuery = appendDomainFilters(buildImageSearchQuery(fetchBase), queryPlan.domains);
|
|
2313
|
+
const requireArtistMatch = inDomain && ARTIST_IMAGE_TOKEN_RE.test(effectiveQuery);
|
|
2128
2314
|
const capped = Math.min(Math.max(maxResults, 1), 25);
|
|
2129
2315
|
const results = [];
|
|
2130
2316
|
const sources = [];
|
|
2131
2317
|
const seenUrls = new Set();
|
|
2318
|
+
const seenImageKeys = new Set();
|
|
2319
|
+
|
|
2320
|
+
const matchesDomainFilter = (...urls) => {
|
|
2321
|
+
if (queryPlan.domains.length === 0) return true;
|
|
2322
|
+
return urls.some(candidate => {
|
|
2323
|
+
try {
|
|
2324
|
+
const host = new URL(candidate).hostname.replace(/^www\./, '');
|
|
2325
|
+
return queryPlan.domains.some(domain => host === domain || host.endsWith(`.${domain}`));
|
|
2326
|
+
} catch {
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
};
|
|
2132
2331
|
|
|
2133
|
-
const addImage = (item, defaultSource) => {
|
|
2332
|
+
const addImage = (item, defaultSource, { trusted = false } = {}) => {
|
|
2134
2333
|
if (!item?.imageUrl || seenUrls.has(item.imageUrl)) return false;
|
|
2334
|
+
if (isLowValueImageUrl(item.imageUrl)) return false;
|
|
2335
|
+
const hostKey = imageHostKey(item.imageUrl);
|
|
2336
|
+
if (seenImageKeys.has(hostKey)) return false;
|
|
2337
|
+
if (item.width && item.width < 200) return false;
|
|
2338
|
+
if (requireArtistMatch && !trusted && !titleMentionsArtist(item.title)) return false;
|
|
2135
2339
|
seenUrls.add(item.imageUrl);
|
|
2340
|
+
seenImageKeys.add(hostKey);
|
|
2136
2341
|
const src = item.source || defaultSource;
|
|
2137
2342
|
let sourcePageUrl = item.sourcePageUrl || '';
|
|
2138
2343
|
if (sourcePageUrl && (sourcePageUrl.includes('ippr_z2C$q') || sourcePageUrl.includes('AzdH3F'))) {
|
|
@@ -2141,19 +2346,23 @@ export async function searchWebImages(query, maxResults = 8) {
|
|
|
2141
2346
|
if (sourcePageUrl && !/^https?:\/\//i.test(sourcePageUrl)) {
|
|
2142
2347
|
sourcePageUrl = '';
|
|
2143
2348
|
}
|
|
2349
|
+
if (!matchesDomainFilter(sourcePageUrl, item.imageUrl)) return false;
|
|
2144
2350
|
results.push({ ...item, sourcePageUrl, source: src });
|
|
2145
2351
|
if (!sources.includes(src)) sources.push(src);
|
|
2146
2352
|
return true;
|
|
2147
2353
|
};
|
|
2148
2354
|
|
|
2149
2355
|
|
|
2150
|
-
if (Array.isArray(galleryData)) {
|
|
2151
|
-
const
|
|
2152
|
-
const localMatches = galleryData.filter(g =>
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2356
|
+
if (inDomain && Array.isArray(galleryData)) {
|
|
2357
|
+
const matchBases = [...new Set([fetchBase, queryPlan.originalQuery].map(v => String(v || '').toLowerCase()).filter(Boolean))];
|
|
2358
|
+
const localMatches = galleryData.filter(g => {
|
|
2359
|
+
const haystacks = [
|
|
2360
|
+
(g.title && g.title.toLowerCase()) || '',
|
|
2361
|
+
(g.category && g.category.toLowerCase()) || '',
|
|
2362
|
+
...((g.tags || []).map(t => String(t).toLowerCase())),
|
|
2363
|
+
];
|
|
2364
|
+
return matchBases.some(base => haystacks.some(hay => hay && (hay.includes(base) || base.split(/\s+/).some(token => token.length > 1 && hay.includes(token)))));
|
|
2365
|
+
});
|
|
2157
2366
|
for (const g of localMatches.slice(0, 3)) {
|
|
2158
2367
|
addImage({
|
|
2159
2368
|
title: g.title || effectiveQuery,
|
|
@@ -2163,14 +2372,14 @@ export async function searchWebImages(query, maxResults = 8) {
|
|
|
2163
2372
|
width: 1080,
|
|
2164
2373
|
height: 1440,
|
|
2165
2374
|
source: 'Official Gallery'
|
|
2166
|
-
}, 'Official Gallery');
|
|
2375
|
+
}, 'Official Gallery', { trusted: true });
|
|
2167
2376
|
}
|
|
2168
2377
|
}
|
|
2169
2378
|
|
|
2170
2379
|
|
|
2171
|
-
const fetchBaiduImages = async () => {
|
|
2380
|
+
const fetchBaiduImages = async (searchQuery = effectiveQuery) => {
|
|
2172
2381
|
try {
|
|
2173
|
-
const baiduImgUrl = `https://image.baidu.com/search/acjson?tn=resultjson_com&logid=${Date.now()}&ipn=rj&ct=201326592&is=&fp=result&word=${encodeURIComponent(
|
|
2382
|
+
const baiduImgUrl = `https://image.baidu.com/search/acjson?tn=resultjson_com&logid=${Date.now()}&ipn=rj&ct=201326592&is=&fp=result&word=${encodeURIComponent(searchQuery)}&queryWord=${encodeURIComponent(searchQuery)}&cl=2&lm=-1&ie=utf-8&oe=utf-8&pn=0&rn=${Math.min(capped * 2, 30)}&gsm=1e`;
|
|
2174
2383
|
const res = await fetchWithTimeout(baiduImgUrl, {
|
|
2175
2384
|
headers: {
|
|
2176
2385
|
...BROWSER_HEADERS,
|
|
@@ -2224,9 +2433,9 @@ export async function searchWebImages(query, maxResults = 8) {
|
|
|
2224
2433
|
};
|
|
2225
2434
|
|
|
2226
2435
|
|
|
2227
|
-
const fetchBingImages = async () => {
|
|
2436
|
+
const fetchBingImages = async (searchQuery = effectiveQuery) => {
|
|
2228
2437
|
try {
|
|
2229
|
-
const url = `https://www.bing.com/images/async?q=${encodeURIComponent(
|
|
2438
|
+
const url = `https://www.bing.com/images/async?q=${encodeURIComponent(searchQuery)}&mmasync=1`;
|
|
2230
2439
|
const res = await fetchWithTimeout(url, {
|
|
2231
2440
|
headers: {
|
|
2232
2441
|
...BROWSER_HEADERS,
|
|
@@ -2261,9 +2470,9 @@ export async function searchWebImages(query, maxResults = 8) {
|
|
|
2261
2470
|
};
|
|
2262
2471
|
|
|
2263
2472
|
|
|
2264
|
-
const fetchBilibiliCovers = async () => {
|
|
2473
|
+
const fetchBilibiliCovers = async (searchQuery = effectiveQuery) => {
|
|
2265
2474
|
try {
|
|
2266
|
-
const url = `https://api.bilibili.com/x/web-interface/search/all/v2?keyword=${encodeURIComponent(
|
|
2475
|
+
const url = `https://api.bilibili.com/x/web-interface/search/all/v2?keyword=${encodeURIComponent(searchQuery)}`;
|
|
2267
2476
|
const res = await fetchWithTimeout(url, {
|
|
2268
2477
|
headers: {
|
|
2269
2478
|
...BROWSER_HEADERS,
|
|
@@ -2294,6 +2503,16 @@ export async function searchWebImages(query, maxResults = 8) {
|
|
|
2294
2503
|
};
|
|
2295
2504
|
|
|
2296
2505
|
|
|
2506
|
+
const mergeImageRound = (bList, biList, blList) => {
|
|
2507
|
+
let idx = 0;
|
|
2508
|
+
while (results.length < capped && (idx < bList.length || idx < biList.length || idx < blList.length)) {
|
|
2509
|
+
if (idx < bList.length && results.length < capped) addImage(bList[idx], 'Baidu Images');
|
|
2510
|
+
if (idx < biList.length && results.length < capped) addImage(biList[idx], 'Bing Images');
|
|
2511
|
+
if (idx < blList.length && results.length < capped) addImage(blList[idx], 'Bilibili Covers');
|
|
2512
|
+
idx++;
|
|
2513
|
+
}
|
|
2514
|
+
};
|
|
2515
|
+
|
|
2297
2516
|
const [baiduImgs, bingImgs, biliImgs] = await Promise.allSettled([
|
|
2298
2517
|
fetchBaiduImages(),
|
|
2299
2518
|
fetchBingImages(),
|
|
@@ -2303,18 +2522,26 @@ export async function searchWebImages(query, maxResults = 8) {
|
|
|
2303
2522
|
const bList = baiduImgs.status === 'fulfilled' && Array.isArray(baiduImgs.value) ? baiduImgs.value : [];
|
|
2304
2523
|
const biList = bingImgs.status === 'fulfilled' && Array.isArray(bingImgs.value) ? bingImgs.value : [];
|
|
2305
2524
|
const blList = biliImgs.status === 'fulfilled' && Array.isArray(biliImgs.value) ? biliImgs.value : [];
|
|
2525
|
+
mergeImageRound(bList, biList, blList);
|
|
2306
2526
|
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2527
|
+
if (inDomain && results.length < capped) {
|
|
2528
|
+
const [baidu2, bing2, bili2] = await Promise.allSettled([
|
|
2529
|
+
fetchBaiduImages('鞠婧祎'),
|
|
2530
|
+
fetchBingImages('鞠婧祎'),
|
|
2531
|
+
fetchBilibiliCovers('鞠婧祎')
|
|
2532
|
+
]);
|
|
2533
|
+
mergeImageRound(
|
|
2534
|
+
baidu2.status === 'fulfilled' && Array.isArray(baidu2.value) ? baidu2.value : [],
|
|
2535
|
+
bing2.status === 'fulfilled' && Array.isArray(bing2.value) ? bing2.value : [],
|
|
2536
|
+
bili2.status === 'fulfilled' && Array.isArray(bili2.value) ? bili2.value : []
|
|
2537
|
+
);
|
|
2313
2538
|
}
|
|
2314
2539
|
|
|
2315
2540
|
if (results.length < capped) {
|
|
2316
2541
|
try {
|
|
2317
|
-
const webRes =
|
|
2542
|
+
const webRes = inDomain
|
|
2543
|
+
? await searchChineseWeb(query, Math.min(capped - results.length, 3))
|
|
2544
|
+
: await defaultMetaSearch(query, { limit: Math.min(capped - results.length, 3) });
|
|
2318
2545
|
const webResults = webRes?.results || [];
|
|
2319
2546
|
const fetchedPages = await Promise.all(webResults.map(async r => {
|
|
2320
2547
|
try {
|
|
@@ -2343,12 +2570,14 @@ export async function searchWebImages(query, maxResults = 8) {
|
|
|
2343
2570
|
} catch {}
|
|
2344
2571
|
}
|
|
2345
2572
|
|
|
2573
|
+
const rankedResults = rerankSearchResults(results, queryPlan, { limit: capped });
|
|
2346
2574
|
return {
|
|
2347
2575
|
status: results.length > 0 ? 'success' : 'no_results',
|
|
2348
|
-
query,
|
|
2349
|
-
|
|
2576
|
+
query: queryPlan.originalQuery || query,
|
|
2577
|
+
normalizedQuery: queryPlan.keywords,
|
|
2578
|
+
count: rankedResults.length,
|
|
2350
2579
|
sources: [...new Set(sources)],
|
|
2351
|
-
results:
|
|
2580
|
+
results: rankedResults
|
|
2352
2581
|
};
|
|
2353
2582
|
}
|
|
2354
2583
|
|
|
@@ -2628,6 +2857,21 @@ export async function searchBilibili(keyword, maxResults = 8) {
|
|
|
2628
2857
|
};
|
|
2629
2858
|
}
|
|
2630
2859
|
|
|
2860
|
+
const extractTopicTokens = (str) => {
|
|
2861
|
+
return String(str || '')
|
|
2862
|
+
.replace(/(?:鞠婧祎|小鞠|jujingyi|ju\s*jing\s*yi|kiku|จวีจิ้งอี|จิ้งอี)/gi, ' ')
|
|
2863
|
+
.replace(/(?:bilibili|b站|video|clip|คลิป|วิดีโอ|ค้นหา|ดู|หา|บน|ใน|ที่|มีไหม|บ้าง|ของ)/gi, ' ')
|
|
2864
|
+
.trim()
|
|
2865
|
+
.toLowerCase()
|
|
2866
|
+
.split(/[\s,+/|&_-]+/)
|
|
2867
|
+
.filter(t => t.length >= 2 || /^[0-9a-z]$/i.test(t));
|
|
2868
|
+
};
|
|
2869
|
+
|
|
2870
|
+
const topicTokens = Array.from(new Set([
|
|
2871
|
+
...extractTopicTokens(keyword),
|
|
2872
|
+
...extractTopicTokens(effectiveKeyword)
|
|
2873
|
+
]));
|
|
2874
|
+
|
|
2631
2875
|
const items = (json.data?.result || []).slice(0, capped).map(r => {
|
|
2632
2876
|
const bvid = r.bvid || '';
|
|
2633
2877
|
const rawPic = r.pic || '';
|
|
@@ -2643,33 +2887,56 @@ export async function searchBilibili(keyword, maxResults = 8) {
|
|
|
2643
2887
|
const rawDesc = String(r.description || '');
|
|
2644
2888
|
const lowerDesc = rawDesc.toLowerCase();
|
|
2645
2889
|
|
|
2646
|
-
|
|
2647
2890
|
const hasArtistInAuthor = lowerAuthor.includes('鞠婧祎工作室') || lowerAuthor.includes('鞠婧祎') || lowerAuthor.includes('studio');
|
|
2648
2891
|
const hasArtistInTitle = cleanTitle.includes('鞠婧祎') || cleanTitle.includes('小鞠') || lowerTitle.includes('kiku') || lowerTitle.includes('jujingyi');
|
|
2649
2892
|
const hasArtistInDesc = rawDesc.includes('鞠婧祎') || rawDesc.includes('小鞠');
|
|
2650
2893
|
|
|
2651
2894
|
if (hasArtistInAuthor) {
|
|
2652
|
-
rankScore += 100;
|
|
2653
|
-
} else if (hasArtistInTitle) {
|
|
2654
2895
|
rankScore += 80;
|
|
2896
|
+
} else if (hasArtistInTitle) {
|
|
2897
|
+
rankScore += 60;
|
|
2655
2898
|
} else if (hasArtistInDesc) {
|
|
2656
|
-
rankScore +=
|
|
2899
|
+
rankScore += 25;
|
|
2657
2900
|
} else {
|
|
2658
|
-
|
|
2659
|
-
rankScore -= 40;
|
|
2901
|
+
rankScore -= 50;
|
|
2660
2902
|
}
|
|
2661
2903
|
|
|
2904
|
+
let topicMatchTitleCount = 0;
|
|
2905
|
+
let topicMatchDescCount = 0;
|
|
2906
|
+
if (topicTokens.length > 0) {
|
|
2907
|
+
for (const token of topicTokens) {
|
|
2908
|
+
if (lowerTitle.includes(token)) topicMatchTitleCount++;
|
|
2909
|
+
if (lowerDesc.includes(token)) topicMatchDescCount++;
|
|
2910
|
+
}
|
|
2911
|
+
if (topicMatchTitleCount > 0) {
|
|
2912
|
+
rankScore += 180 + (topicMatchTitleCount * 30);
|
|
2913
|
+
} else if (topicMatchDescCount > 0) {
|
|
2914
|
+
rankScore += 70 + (topicMatchDescCount * 15);
|
|
2915
|
+
} else {
|
|
2916
|
+
rankScore -= 160;
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2662
2919
|
|
|
2663
|
-
|
|
2664
|
-
|
|
2920
|
+
const hasSnhQuery = topicTokens.some(t => t.includes('snh') || t.includes('塞纳河') || t.includes('总选'));
|
|
2921
|
+
if (hasSnhQuery && (lowerAuthor.includes('snh48') || lowerTitle.includes('snh48'))) {
|
|
2922
|
+
rankScore += 70;
|
|
2665
2923
|
}
|
|
2666
2924
|
|
|
2667
|
-
|
|
2668
|
-
if (
|
|
2669
|
-
rankScore +=
|
|
2925
|
+
const hasPrsrQuery = topicTokens.some(t => t.includes('prsr') || t.includes('帕莎') || t.includes('pasha') || t.includes('眼镜'));
|
|
2926
|
+
if (hasPrsrQuery && (lowerAuthor.includes('帕莎') || lowerAuthor.includes('prsr') || lowerTitle.includes('帕莎') || lowerTitle.includes('prsr'))) {
|
|
2927
|
+
rankScore += 80;
|
|
2670
2928
|
}
|
|
2671
2929
|
|
|
2672
|
-
|
|
2930
|
+
const isVerifiedMediaOrStudio = lowerAuthor.includes('鞠婧祎工作室') || lowerAuthor.includes('腾讯') || lowerAuthor.includes('优酷') || lowerAuthor.includes('爱奇艺') || lowerAuthor.includes('芒果') || lowerAuthor.includes('官方');
|
|
2931
|
+
if (isVerifiedMediaOrStudio) {
|
|
2932
|
+
rankScore += 30;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
if (lowerTitle.includes('mv') || cleanTitle.includes('官方') || lowerTitle.includes('official') || cleanTitle.includes('舞台') || cleanTitle.includes('现场') || cleanTitle.includes('代言') || cleanTitle.includes('花絮') || cleanTitle.includes('广告')) {
|
|
2936
|
+
rankScore += 25;
|
|
2937
|
+
}
|
|
2938
|
+
|
|
2939
|
+
if (play > 0) rankScore += Math.min(Math.log10(play) * 8, 30);
|
|
2673
2940
|
|
|
2674
2941
|
return {
|
|
2675
2942
|
title: cleanTitle,
|
|
@@ -2683,19 +2950,31 @@ export async function searchBilibili(keyword, maxResults = 8) {
|
|
|
2683
2950
|
embedUrl: bvid ? buildBilibiliEmbedUrl(bvid) : '',
|
|
2684
2951
|
description: rawDesc.slice(0, 300),
|
|
2685
2952
|
url: bvid ? `https://www.bilibili.com/video/${bvid}` : '',
|
|
2686
|
-
rankScore: Math.round(rankScore * 10) / 10
|
|
2953
|
+
rankScore: Math.round(rankScore * 10) / 10,
|
|
2954
|
+
topicMatchTitleCount,
|
|
2955
|
+
topicMatchDescCount
|
|
2687
2956
|
};
|
|
2688
2957
|
}).filter(r => r.title && r.url);
|
|
2689
2958
|
|
|
2690
|
-
|
|
2959
|
+
const hasTopicSpecificTokens = topicTokens.length > 0;
|
|
2960
|
+
const hasAnyTopicMatch = items.some(item => item.topicMatchTitleCount > 0 || item.topicMatchDescCount > 0);
|
|
2961
|
+
|
|
2962
|
+
const finalItems = items.filter(r => {
|
|
2963
|
+
if (hasTopicSpecificTokens && hasAnyTopicMatch) {
|
|
2964
|
+
return (r.topicMatchTitleCount > 0 || r.topicMatchDescCount > 0) && r.rankScore > 0;
|
|
2965
|
+
}
|
|
2966
|
+
return r.rankScore > -50;
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2969
|
+
finalItems.sort((a, b) => (b.rankScore || 0) - (a.rankScore || 0));
|
|
2691
2970
|
|
|
2692
2971
|
return {
|
|
2693
|
-
status:
|
|
2972
|
+
status: finalItems.length > 0 ? 'success' : 'no_results',
|
|
2694
2973
|
keyword,
|
|
2695
2974
|
searchQuery: effectiveKeyword,
|
|
2696
|
-
count:
|
|
2975
|
+
count: finalItems.length,
|
|
2697
2976
|
source: 'Bilibili',
|
|
2698
|
-
results:
|
|
2977
|
+
results: finalItems
|
|
2699
2978
|
};
|
|
2700
2979
|
} catch (err) {
|
|
2701
2980
|
return {
|