You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

75 lines
3.6 KiB

// 批量拉取 v29 商机模块客户触点页 HTML(OSS 公开直下)并提取文本
// 用法: node fetch-v29-customer-pages.mjs
// 产物: .scratch/lanhu-latest/v29-customer-pages/<page>.txt(id | 类型 | 名称 | 文本 全量)
import fs from 'node:fs';
import path from 'node:path';
const OSS = 'https://lanhu-axure-file.oss-cn-beijing.aliyuncs.com/';
const MANIFEST = 'e:/code/crm-backend-matt/.scratch/lanhu-latest/axure-v29.json';
const OUT_DIR = 'e:/code/crm-backend-matt/.scratch/lanhu-latest/v29-customer-pages';
// 商机侧客户触点页 + A4 详情(跳转目标证据)
const PAGES = [
'a3-1-1-2-4________.html', // 添加关联客户弹窗 ★
'a3-1-1-1-2(个人)商机详情_客户信息.html', // 客户信息 Tab ★
'a3-1-1-2-1__新增_编辑商机.html', // 新增商机表单(意向客户)
'a3-1-1-2-2_____.html', // 写跟进弹窗
'a3-1-1-2-3_______.html', // 新增方案卡弹窗
'a3-1-1-1-3(个人)商机详情_方案卡.html', // 方案卡列表
'a3-1-1-1-3-1______.html', // 编辑方案卡
'a3-1-1_销售机会列表.html', // 列表列头
'a3-1-1-1-1_(个人)商机详情_概览.html', // 详情概览
'a4-4-1_客户详情(客户信息).html', // A4 客户详情(跳转目标)
];
const decode = (s) => s
.replace(/&#x([0-9A-Fa-f]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)))
.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(parseInt(d, 10)))
.replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"');
// 与 extract-html-text.mjs 同算法,窗口放宽到 1000 字符(弹窗/表格控件文本较长)
function extract(html) {
const re = /<!--\s*(.*?)\s*-->\s*<div id="(u\d+)"[^>]*>/g;
const items = [];
let m;
while ((m = re.exec(html)) !== null) {
const [, comment, id] = m;
const tm = comment.match(/^(.*?)\s*[((](.+?)[))]$/);
const name = tm ? tm[1] : comment;
const type = tm ? tm[2] : '其他';
const after = html.slice(m.index, m.index + 1000);
const tr = after.match(new RegExp(`id="${id}_text"[^>]*>[\\s\\S]*?</div>`));
let text = '';
if (tr) {
text = (tr[0].match(/<p><span>([\s\S]*?)<\/span><\/p>/g) || [])
.map(p => decode(p.replace(/<\/?p>/g, '').replace(/<\/?span>/g, '')))
.join(' | ');
}
items.push({ id, name: decode(name), type: decode(type), text: text.trim() });
}
return items;
}
const manifest = JSON.parse(fs.readFileSync(MANIFEST, 'utf8'));
fs.mkdirSync(OUT_DIR, { recursive: true });
for (const page of PAGES) {
const entry = manifest.pages?.[page];
if (!entry?.html?.sign_md5) { console.log(`MISS ${page} (清单无条目)`); continue; }
const url = OSS + entry.html.sign_md5;
try {
const res = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0' } });
if (!res.ok) { console.log(`FAIL ${page} HTTP ${res.status}`); continue; }
const html = await res.text();
const items = extract(html);
const out = items.filter(i => i.text)
.map(i => `${i.id} | ${i.type} | ${i.name} | ${i.text}`)
.join('\n');
const outFile = path.join(OUT_DIR, page.replace(/\.html$/, '.txt'));
fs.writeFileSync(outFile, `# ${page}\n# 控件 ${items.length},有文本 ${items.filter(i => i.text).length}\n\n${out}\n`, 'utf8');
console.log(`OK ${page} 控件=${items.length} 文本=${items.filter(i => i.text).length}`);
} catch (e) {
console.log(`ERR ${page} ${String(e).slice(0, 120)}`);
}
}
console.log('done ->', OUT_DIR);