yfb-plugin/background.js
2026-04-08 13:07:01 +08:00

220 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 "";
}
}