feat: 开发采集插件

This commit is contained in:
R524809
2026-08-11 17:09:23 +08:00
parent 6c4356de24
commit b27e42dc75
148 changed files with 6898 additions and 9691 deletions
+75
View File
@@ -0,0 +1,75 @@
/**
* 文本提取 - 标题、价格、参数表、描述
* 从 docs/extension/plan.md §6.6 移植(简化版)
*/
import type { SiteProfile, TextRule } from '../profiles/types';
export interface TextMaterial {
kind: TextRule['kind'];
content: string;
pairs?: Array<{ key: string; value: string }>; // table 模式的结构化结果
}
function clean(s: string): string {
return s.replace(/\s+/g, ' ').trim();
}
function extractOne(rule: TextRule): TextMaterial | null {
for (const sel of rule.selectors) {
let nodes: NodeListOf<Element>;
try {
nodes = document.querySelectorAll(sel);
} catch {
continue;
}
if (!nodes.length) continue;
// table 模式:参数表
if (rule.extract === 'table') {
const pairs: Array<{ key: string; value: string }> = [];
nodes.forEach(row => {
const k = clean(row.querySelector(rule.tableKeySelector ?? '')?.textContent ?? '');
const v = clean(row.querySelector(rule.tableValueSelector ?? '')?.textContent ?? '');
if (k && v) pairs.push({ key: k.replace(/[:]$/, ''), value: v });
});
if (pairs.length) {
return {
kind: rule.kind,
content: pairs.map(p => `${p.key}: ${p.value}`).join('\n'),
pairs
};
}
continue;
}
// join 模式:标题被拆成多个 span
if (rule.extract === 'join') {
let text = '';
nodes.forEach(n => { text += n.textContent ?? ''; });
text = clean(text);
if (text) return { kind: rule.kind, content: text };
continue;
}
// first 模式:只取第一个
const first = clean(nodes[0].textContent ?? '');
if (first) return { kind: rule.kind, content: first };
}
return null;
}
export function collectTexts(profile: SiteProfile): {
materials: TextMaterial[];
missingRequired: string[];
} {
const materials: TextMaterial[] = [];
const missingRequired: string[] = [];
for (const rule of profile.textRules) {
const m = extractOne(rule);
if (m) materials.push(m);
else if (rule.required) missingRequired.push(rule.kind);
}
return { materials, missingRequired };
}