Files
ycode/scripts/rea/recover-capability-center.mjs

194 lines
16 KiB
JavaScript

/** Reproducible recovery from REA records only. No installed application reads or runtime actions. */
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
import { parse } from '@babel/parser'
import postcss from 'postcss'
const out = 'src/renderer/src/capabilities'
const check = process.argv.includes('--check')
const hash = text => crypto.createHash('sha256').update(text).digest('hex')
const bundle = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json', 'utf8'))
const source = name => {
const item = bundle.items.find(item => item.file_path.endsWith(name))
if (!item || hash(item.source.artifact.text) !== item.source.artifact.sha256) throw new Error(`REA identity mismatch: ${name}`)
return { ...item.source.artifact, file: name }
}
const c = source('contexts-BMeWSUHk.js')
const icons = source('src-DPDj7_qD.js')
const manifest = { evidenceId: bundle.evidence_id, version: '5.8.2', source: [
{ file: c.file, sha256: c.sha256 }, { file: icons.file, sha256: icons.sha256 }
], bodies: [], css: [], adaptations: [
'React and JSX runtime use the YCode singleton; original module initialization is omitted.',
'UnifiedMarketHeader keeps its source body; visible tabs, tooltip and icon URL resolver use explicit local host adapters.',
'ExpertSearchBar keeps its source body and 300ms debounce/IME behavior; the input primitive uses the matching DOM contract.',
'CSS declarations/order/media/keyframes are preserved and selectors scoped to .yc-capability-center; :root expert variables become local light-theme variables.'
] }
const assignments = new Map(), strings = new Map(), maps = new Map(), functions = new Map()
function index(text) {
const ast = parse(text, { sourceType: 'module' })
function visit(node) {
if (!node || typeof node !== 'object') return
if (node.type === 'FunctionDeclaration' && node.id) functions.set(node.id.name, { node, text })
if (node.type === 'AssignmentExpression' && node.left.type === 'Identifier') {
assignments.set(node.left.name, { node, text })
const r = node.right
if (r.type === 'StringLiteral') strings.set(node.left.name, r.value)
if (r.type === 'TemplateLiteral' && !r.expressions.length) strings.set(node.left.name, r.quasis[0].value.cooked)
if (r.type === 'ObjectExpression') maps.set(node.left.name, r)
}
for (const [key, value] of Object.entries(node)) {
if (key === 'loc' || key === 'comments') continue
if (Array.isArray(value)) value.forEach(visit)
else if (value && typeof value === 'object') visit(value)
}
}
visit(ast.program)
}
index(c.text)
function recover(name, kind = 'assignment') {
const { node, text } = (kind === 'function' ? functions : assignments).get(name) ?? {}
if (!node) throw new Error(`Missing source body: ${name}`)
const body = text.slice(node.start, node.end)
manifest.bodies.push({ name, file: text === c.text ? c.file : icons.file, start: node.start, end: node.end, sha256: hash(body) })
return kind === 'function' ? body : `const ${body};`
}
const classMaps = {}
for (const name of ['tz', 'EI', 'UF', 'lI', 'tI', '_L', 'KF', 'Vvt', 'NR']) {
const node = maps.get(name)
if (!node) throw new Error(`Missing class map: ${name}`)
classMaps[name] = Object.fromEntries(node.properties.filter(p => p.type === 'ObjectProperty').map(p => {
const key = p.key.name ?? p.key.value
const value = p.value.type === 'Identifier' ? strings.get(p.value.name)
: p.value.type === 'StringLiteral' ? p.value.value
: p.value.type === 'TemplateLiteral' && !p.value.expressions.length ? p.value.quasis[0].value.cooked : undefined
if (typeof value !== 'string') throw new Error(`Unresolved class: ${name}.${key}`)
return [key, value]
}))
}
let body = `import * as React from 'react';\nimport * as JSX from 'react/jsx-runtime';\nconst FD=JSX,mI=React,Uvt=JSX,qF=JSX,nI=React,rI=JSX,YF=React,XF=JSX,JF=React,p_t=JSX,ivt='1200px',avt=4,xMe=8;\n`
body += `const Vvt=${JSON.stringify(classMaps.Vvt)},KF=${JSON.stringify(classMaps.KF)},Wvt=300;\n`
body += `const tI=${JSON.stringify(classMaps.tI)};\nfunction useExpertI18n(){return (key,values)=>({searchPlaceholder:'搜索专家',clearSearch:'清除搜索',summonShort:'召唤',summonExpertWithName:'查看专家 '+(values?.name??''),summonTeamWithName:'查看专家团 '+(values?.name??''),'card.paidTag':'付费'}[key]??key)}\n`
body += `function useDebouncedValue$1(value,delay){const [v,set]=React.useState(value);React.useEffect(()=>{const t=setTimeout(()=>set(value),delay);return()=>clearTimeout(t)},[value,delay]);return v}\n`
body += `let SearchPrimitive;export function setExpertSearchPrimitive(value){SearchPrimitive=value}\nconst ka={Search:props=>JSX.jsx(SearchPrimitive,props)};\n`
body += `const _o=({content,children})=>React.cloneElement(children,{title:content});\nconst Ao=({iconUrl,fallback,size})=>iconUrl?JSX.jsx('img',{src:iconUrl,width:size,height:size,alt:''}):fallback;\nconst ID=20;\n`
const headerBody = recover('UnifiedMarketHeader')
const marketIcon = recover('MarketTabIcon')
const searchBody = recover('ExpertSearchBar')
const emptyBody = recover('ExpertListEmptyState')
const capIcon = recover('GraduationCapIcon')
const cardBody = recover('ExpertCard$1')
const pure = ['formatExpertUseCount', 'formatExpertVersion', 'resolveExpertCardTitle', 'resolveExpertCardAuthorName',
'textMatchesKeyword', 'getStrictLocaleSearchText', 'filterExpertsByKeyword', 'getLocalizedText$1', 'getLocalizedTag',
'getLocalizedTagTexts', 'getAvatarFallbackBackground', 'resolveExpertPaidFlags', 'buildExpertCardVisibleTags',
'getImportSecurityRiskText', 'useDeferredImageLoad', 'useRevealOnLoad', 'getFetchPriorityProps', 'ExpertCardAvatar'].map(name => recover(name, 'function')).join('\n')
const highlight = ['escapeRegExp$1', 'highlightText'].map(name => recover(name, 'function')).join('\n')
index(icons.text)
for (const [name, react, jsx] of [['Fu', 'Nu', 'Pu'], ['GF', 'UF', 'WF'], ['xl', 'yl', 'bl'], ['EM', 'wM', 'TM'], ['Bn', 'Rn', 'zn'], ['Ym', 'qm', 'Jm']]) {
body += `const ${react}=React,${jsx}=JSX;\n${recover(name)}\n`
}
body += `const useLocale$1=()=> 'zh';const al=()=>({reporter:{reportEvent:()=>{},Events:{}}});\nconst resolveExpertListAvatarUrl=value=>value;\nconst ImportSecurityRiskIcon=()=>null,ExpertCloudSyncBadge=()=>null;\n`
body += `function useVisibleMarketTabs(){return [{key:'experts',label:'专家',tooltip:'发现和管理专家',Icon:Fu},{key:'skills',label:'技能',tooltip:'发现和管理技能',Icon:GF},{key:'connectors',label:'连接器',tooltip:'连接工具与服务',Icon:xl}]}\n`
body += `${marketIcon}\n${headerBody}\n${searchBody}\n${emptyBody}\n${capIcon}\n${pure}\n${highlight}\n${cardBody}\nexport {UnifiedMarketHeader,ExpertSearchBar,ExpertListEmptyState,ExpertCard$1 as ExpertCard,Fu as ExpertIcon,GF as SkillIcon,xl as ConnectorIcon,EM as BackIcon,Bn as AddIcon,Ym as MyExpertIcon,filterExpertsByKeyword,formatExpertUseCount,formatExpertVersion};\n`
const cssManifest = JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json', 'utf8'))
function cssSource(name) {
const file = cssManifest.files.find(file => file.file === name)
const text = fs.readFileSync(`.tmp/rea-css-recovered/${name}`, 'utf8')
if (!file || hash(text) !== file.sha256) throw new Error(`REA CSS mismatch: ${name}`)
return { text, file }
}
const contextsCss = cssSource('contexts-BkjwYSZT.css')
manifest.source.push({ file: contextsCss.file.file, sha256: contextsCss.file.sha256, evidenceId: cssManifest.sourceEvidenceId })
const tree = postcss.parse(contextsCss.text)
const endRule = tree.nodes.find(n => n.type === 'rule' && n.selector.includes('._loadMoreSentinel_1yu3o_194'))
if (!endRule) throw new Error('Expert CSS boundary missing')
const end = endRule.source.end.offset
function scoped(node) {
const clone = node.clone()
if (clone.type === 'rule') clone.selector = clone.selector.split(',').map(selector => `:where(.yc-capability-center) ${selector.trim().replace(/:root/g, '.expert-center-light')}`).join(',')
else if (clone.type === 'atrule' && !/keyframes$/.test(clone.name)) clone.nodes?.forEach((child, index) => { clone.nodes[index] = scoped(child); clone.nodes[index].parent = clone })
return clone
}
const common = [], expert = []
const tokenCss = cssSource('tokens-Df-i-zsO.css')
manifest.source.push({ file: tokenCss.file.file, sha256: tokenCss.file.sha256 })
for (const node of postcss.parse(tokenCss.text).nodes) {
if (node.type !== 'rule' || !node.nodes.every(n => n.type === 'decl' && n.prop.startsWith('--'))) continue
let selector
if (node.selector.includes(':root')) selector = '.yc-capability-center'
else if (node.selector.startsWith('body[data-electron-desktop=true]')) selector = node.selector.replace('body', '.yc-capability-center')
else if (node.selector.includes('[data-theme=dark]')) selector = '.yc-capability-center .expert-center-dark'
if (!selector) continue
const clone = node.clone({ selector })
common.push(clone.toString())
manifest.css.push({ file: tokenCss.file.file, offset: node.source.start.offset, sha256: hash(node.toString()), selector: node.selector })
}
const primitiveCss = cssSource('src-CBaV_A3S.css')
manifest.source.push({ file: primitiveCss.file.file, sha256: primitiveCss.file.sha256 })
for (const node of postcss.parse(primitiveCss.text).nodes) {
if (node.type === 'rule' && /^\.wb-(input|button|segmented)(?:[\s.:#[_-]|$)/.test(node.selector)) common.push(scoped(node).toString())
}
const segmentedCss = cssSource('src-Bh3YZ35H.css')
manifest.source.push({ file: segmentedCss.file.file, sha256: segmentedCss.file.sha256, evidenceId: cssManifest.sourceEvidenceId })
for (const node of postcss.parse(segmentedCss.text).nodes) {
if ((node.type === 'rule' && /^\.wb-(segmented|input|button)(?:[\s.:#[_-]|$)/.test(node.selector))
|| (node.type === 'atrule' && /\.wb-(segmented|input|button)/.test(node.toString()))) {
common.push(scoped(node).toString())
manifest.css.push({ file: segmentedCss.file.file, offset: node.source.start.offset, sha256: hash(node.toString()), selector: node.selector })
}
}
for (const node of tree.nodes) {
const offset = node.source.start.offset
const text = node.toString()
if ((node.type === 'rule' || node.type === 'atrule') && /(?:\.um-header|\.um-tab|_1yu3o_)/.test(text)) common.push(scoped(node).toString())
else if (offset >= 673321 && offset <= end) expert.push(scoped(node).toString())
else if (node.type === 'rule' && /^\.wb-(input|button|segmented)(?:[\s.:#[_-]|$)/.test(node.selector)) common.push(scoped(node).toString())
if ((offset >= 673321 && offset <= end) || (node.type === 'rule' && /\.um-(?:header|tab)/.test(node.selector))) {
manifest.css.push({ offset, sha256: hash(text), type: node.type, selector: node.selector, name: node.name })
}
}
common.push(`/* YCode host sizing; the source Electron token supplies the 64px topbar. */
.yc-capability-center{height:100%;width:100%;min-height:0;min-width:0;display:flex;font-family:var(--wb-font-family);font-size:13px;isolation:isolate}
.yc-capability-center>.expert-center-page{flex:1;min-width:0}
.yc-capability-center .capability-page-host{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0;overflow:hidden}
.yc-capability-center .capability-title-slot{display:contents}
.yc-capability-center .wb-icon{flex:none;display:block}
.yc-capability-center .capability-icon-button{border:0;background:transparent;color:var(--wb-color-text-secondary);display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:6px;border-radius:6px}
.yc-capability-center .capability-text-button{border:0;background:transparent;color:var(--wb-color-text-secondary);font-size:13px;cursor:pointer;padding:4px 8px;border-radius:6px}
.yc-capability-center .capability-text-button:hover{background:var(--wb-color-bg-primary-hover)}
@media(max-width:700px){.yc-capability-center .ec-topbar{flex-wrap:wrap;height:auto;min-height:64px;gap:8px;padding-top:12px;padding-bottom:12px}.yc-capability-center .ec-topbar__right{max-width:100%;flex-wrap:wrap}.yc-capability-center .um-header__tabs{gap:16px}.yc-capability-center .um-tab{font-size:16px}.yc-capability-center ._root_mbn4e_1{width:140px}}
`)
expert.push(`/* Explicit offline-service adapters; source cards and their body/CSS remain recovered above. */
.yc-capability-center .capability-expert-body{flex:1;min-height:0;display:flex;flex-direction:column}
.yc-capability-center .capability-market-panel{display:contents}
.yc-capability-center .capability-expert-empty{flex:1;min-height:280px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:12px}
.yc-capability-center .capability-service-note{font-size:13px;line-height:20px;color:var(--wb-color-text-tertiary);margin:0}
.yc-capability-center .capability-catalog-status{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:8px 0 16px;flex-wrap:wrap}
.yc-capability-center .ec-my-created-empty-title{margin:0}
.capability-detail-host{position:fixed;inset:0;z-index:1000}
`)
const generated = [
['assets/source-ui.mjs', body],
['assets/source-ui.d.mts', `import type {ComponentType,SVGProps} from 'react';\nexport const UnifiedMarketHeader:ComponentType<{activeTab:'experts'|'skills'|'connectors';onTabChange(tab:'experts'|'skills'|'connectors'):void}>;\nexport const ExpertSearchBar:ComponentType<{value:string;onChange(value:string):void;placeholder?:string;variant?:string}>;\nexport const ExpertListEmptyState:ComponentType<{message:string;showIcon?:boolean}>;\nexport const ExpertCard:ComponentType<{expert:Record<string,unknown>;avatarUrl?:string;cardPosition:number;onSummon():void;onExpertClick(expert:Record<string,unknown>):void;appearance?:string;highlightKeyword?:string;hideTopSummonButton?:boolean;priorityAvatarLoad?:boolean;isFirst?:boolean}>;\nexport const ExpertIcon:ComponentType<SVGProps<SVGSVGElement>>;export const SkillIcon:typeof ExpertIcon;export const ConnectorIcon:typeof ExpertIcon;\nexport function setExpertSearchPrimitive(value:ComponentType<any>):void;\nexport function filterExpertsByKeyword<T>(items:T[],keyword:string,locale:string):T[];\nexport function formatExpertUseCount(value:number,locale:string):string;\nexport function formatExpertVersion(value:string):string;\n`],
['assets/source-classes.json', JSON.stringify(classMaps, null, 2)+'\n'],
['capability-center-source.css', '/* REA recovered declarations; generated by recover-capability-center.mjs. */\n'+common.join('\n')+'\n'],
['expert-source.css', '/* REA recovered declarations; generated by recover-capability-center.mjs. */\n'+expert.join('\n')+'\n']
]
generated[1][1] += 'export const BackIcon:typeof ExpertIcon;export const AddIcon:typeof ExpertIcon;export const MyExpertIcon:typeof ExpertIcon;\n'
const emptyEvidence = JSON.parse(fs.readFileSync('.tmp/rea-expert-empty-asset-evidence-20261016.json', 'utf8'))
if (emptyEvidence.normalized_result.root_sha256 !== '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918') throw new Error('Empty icon root pin mismatch')
const emptyFile = emptyEvidence.normalized_result.files[0]
const emptySvg = Buffer.from(emptyFile.data_base64, 'base64')
if (hash(emptySvg) !== emptyFile.sha256) throw new Error('Empty expert icon SHA mismatch')
generated.push(['assets/empty-created-expert.svg', emptySvg.toString('utf8')])
manifest.source.push({ file: emptyFile.path, sha256: emptyFile.sha256, evidenceId: emptyEvidence.evidence_id })
manifest.outputs = generated.map(([file, text]) => ({ file, sha256: hash(text), bytes: Buffer.byteLength(text) }))
generated.push(['assets/recovery-manifest.json', JSON.stringify(manifest, null, 2)+'\n'])
for (const [file, text] of generated) {
const target = path.join(out, file)
if (check) { if (!fs.existsSync(target) || fs.readFileSync(target, 'utf8') !== text) throw new Error(`Recovery drift: ${target}`) }
else { fs.mkdirSync(path.dirname(target), { recursive: true }); fs.writeFileSync(target, text) }
}
console.log(JSON.stringify({ status: 'PASS', check, bodies: manifest.bodies.length, cssRules: manifest.css.length, outputs: generated.length }))