feat: init the repo
This commit is contained in:
commit
7a37b7604b
219
background.js
Normal file
219
background.js
Normal file
@ -0,0 +1,219 @@
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
10
config.js
Normal file
10
config.js
Normal file
@ -0,0 +1,10 @@
|
||||
globalThis.YFB_EXTENSION_CONFIG = Object.freeze({
|
||||
"apiKey": "sk-f9966506e15d4a678f560c1a9b67fee2",
|
||||
"model": "qwen3.5-flash",
|
||||
"chatEndpoint": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
|
||||
"requestTimeoutMs": 45000,
|
||||
"maxBodyChars": 12000,
|
||||
"maxAiChars": 1800,
|
||||
"maxListPreviewChars": 400,
|
||||
"maxLogEntries": 80
|
||||
});
|
||||
2705
content.js
Normal file
2705
content.js
Normal file
File diff suppressed because it is too large
Load Diff
33
manifest.json
Normal file
33
manifest.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "乙方宝招标筛选助手",
|
||||
"version": "0.1.0",
|
||||
"description": "在乙方宝页面内自动翻页抓取、AI筛选、高亮并导出金融机构相关招标信息。",
|
||||
"permissions": [
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"*://*.yfbzb.com/*",
|
||||
"*://*.qianlima.com/*",
|
||||
"https://dashscope.aliyuncs.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "乙方宝招标筛选助手"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"*://*.yfbzb.com/*",
|
||||
"*://*.qianlima.com/*"
|
||||
],
|
||||
"js": [
|
||||
"vendor/xlsx.full.min.js",
|
||||
"content.js"
|
||||
],
|
||||
"run_at": "document_idle"
|
||||
}
|
||||
]
|
||||
}
|
||||
22
vendor/xlsx.full.min.js
vendored
Normal file
22
vendor/xlsx.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user