yfb-plugin/background.js

220 lines
6.5 KiB
JavaScript
Raw Normal View History

2026-04-08 13:07:01 +08:00
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 "";
}
}