importScripts("config.js"); const CONFIG = self.YFB_EXTENSION_CONFIG || {}; const CHAT_ENDPOINT = CONFIG.chatEndpoint || "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; const REQUEST_TIMEOUT_MS = Number(CONFIG.requestTimeoutMs) || 45000; chrome.runtime.onInstalled.addListener(() => { console.log("乙方宝招标筛选助手已安装"); }); chrome.action.onClicked.addListener((tab) => { if (!tab || !tab.id) { return; } chrome.tabs.sendMessage(tab.id, { type: "YFB_TOGGLE_PANEL" }, () => { void chrome.runtime.lastError; }); }); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (!message || message.type !== "YFB_ANALYZE_CANDIDATE") { return false; } void analyzeCandidate(message.payload) .then((data) => { sendResponse({ ok: true, data }); }) .catch((error) => { sendResponse({ ok: false, error: error instanceof Error ? error.message : "AI 分析失败" }); }); return true; }); async function analyzeCandidate(payload) { if (!CONFIG.apiKey) { throw new Error("缺少 DASHSCOPE_API_KEY,请先执行 npm.cmd run build。"); } const response = await fetchWithTimeout(CHAT_ENDPOINT, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${CONFIG.apiKey}` }, body: JSON.stringify({ model: CONFIG.model || "qwen-plus", temperature: 0.1, max_tokens: 120, messages: buildMessages(payload) }) }); if (!response.ok) { const errorText = await safeReadText(response); throw new Error(`AI 接口返回 ${response.status}: ${errorText || "未知错误"}`); } const data = await response.json(); const rawContent = extractMessageContent(data); const parsed = parseAiJson(rawContent); return normalizeAiResult(parsed, payload); } async function fetchWithTimeout(url, options) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { return await fetch(url, { ...options, signal: controller.signal }); } catch (error) { if (error && error.name === "AbortError") { throw new Error("AI 请求超时"); } throw error; } finally { clearTimeout(timeoutId); } } function buildMessages(payload) { const compactInput = buildCompactInput(payload); return [ { role: "system", content: '判断是否属于金融机构相关招标。仅当服务对象明确是银行/信托/保险/消费金融/金融租赁/汽车金融等金融机构,且业务属于"法律服务"、"催收/不良资产处置"、"调解服务"之一时命中。只返回 JSON:{"category":"法律服务|催收/不良资产处置|调解服务|不命中","confidence":0-100}。' }, { role: "user", content: JSON.stringify(compactInput) } ]; } function buildCompactInput(payload) { const keywordHints = payload?.keywordHints || {}; return { title: String(payload?.title || ""), type: String(payload?.type || ""), region: String(payload?.region || ""), publishTime: String(payload?.publishTime || ""), detailText: limitLength(String(payload?.detailText || ""), Number(CONFIG.maxAiChars) || 1800), attachmentNames: Array.isArray(payload?.attachmentNames) ? payload.attachmentNames.slice(0, 6) : [], keywordHints: { institutions: Array.isArray(keywordHints.institutions) ? keywordHints.institutions.slice(0, 6) : [], legal: Array.isArray(keywordHints.legal) ? keywordHints.legal.slice(0, 6) : [], collection: Array.isArray(keywordHints.collection) ? keywordHints.collection.slice(0, 6) : [], mediation: Array.isArray(keywordHints.mediation) ? keywordHints.mediation.slice(0, 6) : [], all: Array.isArray(keywordHints.all) ? keywordHints.all.slice(0, 12) : [] } }; } function extractMessageContent(apiResponse) { const content = apiResponse?.choices?.[0]?.message?.content; if (typeof content === "string") { return content; } if (Array.isArray(content)) { return content .map((part) => { if (typeof part === "string") { return part; } return part?.text || ""; }) .join("\n"); } return ""; } function parseAiJson(text) { const trimmed = String(text || "").trim(); if (!trimmed) { throw new Error("AI 返回为空"); } try { return JSON.parse(trimmed); } catch (error) { const startIndex = trimmed.indexOf("{"); const endIndex = trimmed.lastIndexOf("}"); if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) { throw new Error("AI 返回不是有效 JSON"); } return JSON.parse(trimmed.slice(startIndex, endIndex + 1)); } } function normalizeAiResult(result, payload) { const category = normalizeCategory(result?.category); const institutionType = Array.isArray(result?.institutionType) ? result.institutionType.filter(Boolean).map((item) => String(item).trim()) : Array.isArray(payload?.keywordHints?.institutions) ? payload.keywordHints.institutions.slice(0, 3) : []; const matchedKeywords = Array.isArray(result?.matchedKeywords) ? result.matchedKeywords.filter(Boolean).map((item) => String(item).trim()) : Array.isArray(payload?.keywordHints?.all) ? payload.keywordHints.all : []; return { isRelevant: (typeof result?.isRelevant === "boolean" ? result.isRelevant : category !== "不命中") && category !== "不命中", category, institutionType, confidence: normalizeConfidence(result?.confidence), titleSummary: limitLength(result?.titleSummary || payload?.title || "", 60), reason: limitLength(result?.reason || "", 120), matchedKeywords: dedupe(matchedKeywords) }; } function normalizeCategory(category) { const value = String(category || "").trim(); if (value === "法律服务" || value === "催收/不良资产处置" || value === "调解服务") { return value; } return "不命中"; } function normalizeConfidence(confidence) { const numeric = Number(confidence); if (!Number.isFinite(numeric)) { return 0; } return Math.max(0, Math.min(100, Math.round(numeric))); } function limitLength(text, maxLength) { const normalized = String(text || "").trim(); if (normalized.length <= maxLength) { return normalized; } return normalized.slice(0, maxLength - 1) + "…"; } function dedupe(list) { return Array.from(new Set(list.filter(Boolean))); } async function safeReadText(response) { try { return await response.text(); } catch (error) { return ""; } }