import fs from 'node:fs/promises'; import path from 'node:path'; import * as cheerio from 'cheerio'; import { sites, languages } from './sites.mjs'; import { root, cacheRoot, jsonFile, write, writeJSON, canonical, routeOf, blockedPath, relative, escapeHTML as E } from './lib.mjs'; import { Assets } from './assets.mjs'; import { prepareTranslations, localize } from './translate.mjs'; import { addFonts } from './fonts.mjs'; const args=process.argv.slice(2); const selected=sites.filter(s=>!args.some(a=>!a.startsWith('--'))||args.includes(s.id)); const date=t=>new Date(t*1000).toISOString().slice(0,10); const detail=(item,type)=>item.detail?.[type]||item.detail||item; const jskTelegram={url:'https://t.me/JSKCDN999',handle:'@JSKCDN999'}; for(const site of selected){ const folder=path.join(root,site.id); const sourceFolder=path.join(cacheRoot,'sites',site.id); const data=await jsonFile(path.join(sourceFolder,'data.json')); const captured=await jsonFile(path.join(sourceFolder,'pages.json'),[]); if(!captured.some(p=>p.file==='index.html'))throw new Error(`Capture ${site.id} first`); const sourceReport=await jsonFile(path.join(sourceFolder,'capture-report.json'),{}); const asset=new Assets(site); const records=new Map(captured.map(p=>[p.file,p])); const routes=new Map(captured.map(p=>[canonical(p.url,site),p.file])); const template=captured.find(p=>p.file==='document.html')||captured.find(p=>p.file==='index.html'); function shell(file,title,body,url){ const $=cheerio.load(template.html);const header=$('header').first().prop('outerHTML')||'';const footer=$('footer').first().prop('outerHTML')||$('body > .uk-section-primary').map((_,el)=>$(el).prop('outerHTML')).get().join(''); $('body').empty().append(header).append(`
${body}
`).append(footer); $('title').text(`${title} - ${site.name}`); const entry={file,url:url||site.origin+'/'+file,html:$.html(),title};records.set(file,entry);routes.set(canonical(entry.url,site),file);return entry; } function list(items,type){const short=type==='announcement'?'announce':type;return ``;} for(const type of ['news','announcement']){ const short=type==='announcement'?'announce':type;const title=type==='announcement'?'官方公告':'新闻资讯';const items=data.contents[type]; shell(`${short}.html`,title,`

${title}

${list(items,type)}`); for(const item of items){ const article=detail(item,type);const content=cheerio.load(article.content||''); shell(`${short}/${item.id}.html`,article.title||item.title,`

${E(article.title||item.title)}

${date(item.create_time)}
${content('body').html()||''}返回列表
`,`${site.origin}/${short}-details.html?id=${item.id}`); } for(const category of data.api[`/${type}/type`]?.list||[]){ const filtered=items.filter(item=>String(detail(item,type)[`addon_idcsmart_${type}_type_id`])===String(category.id)); const pages=Math.max(1,Math.ceil(filtered.length/10)); for(let n=1;n<=pages;n++){ const file=`${short}-classify-id-${category.id}${n>1?'-page-'+n:''}.html`; const pagination=pages>1?``:''; shell(file,category.name,`

${E(category.name)}

${list(filtered.slice((n-1)*10,n*10),type)}${pagination}`,`${site.origin}/${short}-classify.html?id=${category.id}${n>1?'&page='+n:''}`); } } } for(const item of data.contents.help){ const article=detail(item,'help');const content=cheerio.load(article.content||''); content('span').each((_,el)=>{const $el=content(el);if(!$el.find('img,strong,b,i,em,a').length)$el.replaceWith($el.contents());}); shell(`docs/${item.id}.html`,article.title||item.title,`

${E(article.title||item.title)}

${content('body').html()||''}返回列表
`,`${site.origin}/plugin/26/helpTotal.htm?id=${item.id}`); } const docGroups=(data.api['/help']?.list||[]).map(group=>`

${E(group.name)}

`).join(''); shell('document.html','文档中心',`

文档中心

${docGroups}`); shell('document-result.html','文档搜索',`

文档搜索

`); if(site.id==='jskcloud'){ const contact=records.get('contact.html'); const $=cheerio.load(contact.html); const telegramImage=$('.aside-tools .tools-item img').first().attr('src'); const documentImage=$('.support-box').filter((_,el)=>$(el).text().includes('自助文档')).find('img').attr('src'); $('.server-banner-desc').text('我们关怀每一位客户,致力于提供更高品质的服务。无论是产品咨询还是技术问题,欢迎通过 Telegram 联系 JSKCloud。'); $('.contact-support,.product-help').each((_,el)=>{ const previous=el.prev; if(previous?.type==='text'&&!previous.data.trim())$(previous).remove(); $(el).remove(); }); $('.server-banner2').after(`

Telegram

${jskTelegram.handle}

产品选型、配置咨询与技术支持,请通过 Telegram 与我们沟通。

联系 JSKCloud

文档中心

查阅产品说明、使用指南与常见问题,快速获取所需信息。

查看帮助文档

咨询时请说明所需产品、部署地区与配置,或描述遇到的问题,方便我们为您提供帮助。

`); records.set(contact.file,{...contact,html:$.html()}); } const product=data.api['/product/4']?.product; const options=data.api['/product/4/mf_cloud/order_page']; if(!product||!options)throw new Error(`${site.id}: public cloud product data missing`); const values=(items,unit='')=>(items||[]).map(x=>x.type==='step'?`${x.min_value}–${x.max_value}${unit}`:x.value+unit).sort((a,b)=>parseFloat(a)-parseFloat(b)).join(' / '); const regions=options.data_center.map(country=>country.city.map(city=>city.area.map(area=>`${E(country.name)} / ${E(city.name)}${area.line.map(line=>E(line.name)).join(' / ')}`).join('')).join('')).join(''); const currency=data.api['/common']?.currency_code||''; const price=product.price!==undefined?`${E(currency)} ${E(product.price)} / ${E(product.cycle)}`:'联系咨询'; shell('cloud.html',product.name,`

${E(product.name)}

公开起售价

${price}

可选配置

CPU(核)${E(values(options.cpu))}
内存${E(values(options.memory,options.config.memory_unit))}
系统盘${E(values(options.system_disk,'GB'))}
数据盘${E(values(options.data_disk,'GB'))}

地区与线路

${regions}
地区线路

联系我们

`); routes.set(canonical('/cart/goods.htm?id=4',site),'cloud.html'); const prepared=[]; for(const page of records.values()){ const $=cheerio.load(page.html); if(site.id==='jskcloud'){ $('.aside-tools .tools-box-s').html(`
联系客服

${jskTelegram.handle}

`); $('.aside-tools .tools-item > img').wrap(``); $('footer .footer-nav-right .footer-nav-box').html(``); $('a[href]').each((_,el)=>{ const a=$(el); if(/^https?:\/\/(?:www\.)?t\.me\//i.test(a.attr('href')||''))a.attr('href',jskTelegram.url).attr('target','_blank'); }); } function stripComments(node){for(const child of [...node.children||[]]){if(child.type==='comment')$(child).remove();else stripComments(child);}} stripComments($.root()[0]); $('script').each((_,el)=>{const src=$(el).attr('src')||'';if(!/\/vender\/(?:jQuery|bootstrap|swiper|uikit)\//i.test(src))$(el).remove();}); $('base,noscript,iframe,object,embed,.no-login,.login-in,.login-menu,.header-clientarea,#loginBtn,#registBtn,#accountBtn,#financeBtn,#ticketBtn,#logout,.register-advert,.swiper-slide-duplicate').remove(); $('.js-buy,.add-cart').each((_,el)=>{const availability=$(el).find('.qty').text().trim();if(availability)$(el).replaceWith($('').text(availability));else $(el).remove();}); $('html').removeAttr('addons_js').removeAttr('id'); $('.static-price').removeAttr('data-no-translate'); $('*').each((_,el)=>{ for(const key of Object.keys(el.attribs||{}))if(/^on/i.test(key))$(el).removeAttr(key); if($(el).attr('uk-scrollspy')){$(el).removeAttr('uk-scrollspy');const style=$(el).attr('style');if(style)$(el).attr('style',style.replace(/(?:visibility\s*:\s*hidden|opacity\s*:\s*0)\s*;?/gi,''));} }); $('a[href]').each((_,el)=>{ const a=$(el);const href=(a.attr('href')||'').trim(); try{const u=new URL(href,page.url);if(site.hosts.includes(u.hostname)&&blockedPath(u.pathname)&&!/^\/cart\/goods\.htm$/.test(u.pathname)){if(a.closest('li').length)a.closest('li').remove();else a.remove();}} catch{/* Relative fragment links need no normalization here. */} if(/^mailto:\?@|^https?:\/\/t\.me\/\?/.test(href))a.remove(); }); $('a,button,.btn,.buy,.add-cart').each((_,el)=>{const text=$(el).text().trim();if(/^(?:立即购买|立即开通|立即创建|立即注册|免费试用|注册|登录|会员中心|控制台|Buy\s*now|Sign\s*up|Log\s*in|Client\s*Area|Create\s*now)$/i.test(text))$(el).remove();}); $('form').not('#static-search-form').each((_,el)=>$(el).replaceWith($(el).contents())); $('a[href=""],a[href="javascript:;"],a[href="javascript:void(0)"]').each((_,el)=>{$(el).attr('href','#');}); if(page.file==='index.html'){ for(const [selector,type] of [['#newsList','news'],['#announceList','announcement']]){ $(selector).html(data.contents[type].slice(0,5).map(item=>`
${E(item.title)}${date(item.create_time)}
`).join('')); } } $('header .uk-parent').each((_,el)=>{const children=$(el).find('.uk-navbar-dropdown a');if($(el).find('.uk-navbar-dropdown').length&&!children.length)$(el).remove();}); $('img').each((_,el)=>{const img=$(el);if(!img.attr('src')&&!img.attr('data-src')&&!img.attr('srcset'))img.remove();}); prepared.push({...page,$,html:$.html()}); } await write(path.join(folder,'assets','site.js'),await fs.readFile(path.join(root,'scripts','static','site.js'))); await write(path.join(folder,'assets','site.css'),await fs.readFile(path.join(root,'scripts','static','site.css'))); const variants=args.includes('--base-only')?['zh-cn']:languages; if(!args.includes('--base-only'))await prepareTranslations(prepared.map(p=>p.$)); const output=[]; for(const page of prepared){ for(const language of variants){ const targets=[`${language}/${page.file}`];if(language===site.defaultLanguage||args.includes('--base-only'))targets.push(page.file); for(const file of targets){ const $=cheerio.load(page.html);await localize($,language); $('html').attr('lang',language==='en'?'en':language==='zh-hk'?'zh-Hant':'zh-Hans');$('body').attr('data-static-site',site.id); $('a[href]').each((_,el)=>{ const a=$(el);const value=a.attr('href')?.trim();if(!value||/^(?:#|mailto:|tel:|javascript:|data:)/i.test(value))return; let u;try{u=new URL(value,page.url);}catch{return;} if(!site.hosts.includes(u.hostname)){a.attr('href',value.trim());if(a.attr('target')==='_blank')a.attr('rel','noopener noreferrer');return;} const key=canonical(u.href,site);let target=routes.get(key);const mapped=routeOf(u.href,site);if(!target&&records.has(mapped))target=mapped; if(!target){a.replaceWith(a.contents());return;} a.attr('href',relative(file,`${language}/${target}`)+u.hash).removeAttr('target'); }); const labels={'zh-cn':'简体中文','zh-hk':'繁體中文','en':'English'}; $('.language-option').each((_,el)=>{const a=$(el);const code=({'en-us':'en','zh':'zh-cn','ftzh':'zh-hk'})[a.attr('data-lang')]||a.attr('data-lang');if(!languages.includes(code))return;const link=$('').attr('class',a.attr('class')||'language-option').attr('data-lang',code).attr('href',relative(file,`${code}/${page.file}`)).attr('lang',code).attr('hreflang',code);if(language===code)link.attr('aria-current','true');link.text(labels[code]);a.replaceWith(link);}); $('.language-code,.current-lang-label').text(labels[language]); $('meta[http-equiv="Content-Language"]').attr('content',language);$('link[rel="canonical"],link[rel="alternate"][hreflang]').remove(); $('head').append(``); for(const lang of languages)$('head').append(``); for(const [selector,attr,hint] of [['img','src',''],['source','src',''],['video','src',''],['video','poster',''],['audio','src',''],['script[src]','src','.js'],['link[rel="stylesheet"]','href','.css'],['link[rel="icon"],link[rel="shortcut icon"]','href','']]){ $(selector).each((_,el)=>{const value=$(el).attr(attr);if(!value)return;const target=asset.ensure(value,page.url,hint);if(target)$(el).attr(attr,target.startsWith('data:')?target:relative(file,target));}); } $('[srcset]').each((_,el)=>{const value=$(el).attr('srcset');if(value.startsWith('data:'))return;$(el).attr('srcset',value.split(',').map(part=>{const [url,...size]=part.trim().split(/\s+/);const target=asset.ensure(url,page.url);return [relative(file,target),...size].join(' ');}).join(', '));}); $('[data-src]').each((_,el)=>{const value=$(el).attr('data-src');if(!value||value.startsWith('#'))return;const target=asset.ensure(value,page.url);if(target)$(el).attr('data-src',target.startsWith('data:')?target:relative(file,target));}); $('style').each((_,el)=>$(el).text(asset.css($(el).text(),page.url,file))); $('[style]').each((_,el)=>$(el).attr('style',asset.value($(el).attr('style'),page.url,file))); $('link[rel="preconnect"],link[rel="dns-prefetch"],link[rel="prefetch"],link[rel="preload"]').remove(); $('head').append(``); $('#document-input').attr('data-results',relative(file,`${language}/document-result.html`)); if(page.file==='document-result.html'){ const searchItems=[]; for(const item of data.contents.help){const source=prepared.find(p=>p.file===`docs/${item.id}.html`);const t=cheerio.load(source.html);await localize(t,language);searchItems.push({title:t('.static-article h1').text(),text:t('.static-article').text(),href:relative(file,`${language}/docs/${item.id}.html`)});} const searchData={items:searchItems,empty:language==='en'?'No results found.':language==='zh-hk'?'未找到相關內容':'未找到相关内容'}; $('body').append(``); } $('body').append(``); await write(path.join(folder,file),$.html());output.push({file,language,source:page.url,title:$('title').text()}); } } console.log(`${site.id}: generated ${page.file} (${variants.length} languages)`); } await addFonts(asset,site,output); await asset.settle(); const missingFonts=[...asset.records.values()].filter(entry=>entry.error&&/^https?:/.test(entry.url)&&/fonts\.googleapis\.com|fonts\.gstatic\.com/.test(new URL(entry.url).hostname)); if(missingFonts.length)throw new Error(`${site.id}: required offline fonts unavailable: ${missingFonts.map(entry=>entry.file).join(', ')}`); await asset.prune(output.map(item=>item.file)); await writeJSON(path.join(folder,'site-manifest.json'),{site:site.id,origin:site.origin,capturedAt:data.capturedAt,generatedAt:new Date().toISOString(),defaultLanguage:site.defaultLanguage,languages:variants,pages:output,sourceFailures:sourceReport.failures||[],assetFailures:asset.failures,sourceMap:Object.fromEntries(routes)}); console.log(`${site.id}: ${output.length} HTML files, ${asset.records.size} resources, ${asset.failures.length} resource failures`); }