240 lines
38 KiB
JavaScript
240 lines
38 KiB
JavaScript
/** Recovery consumes retained REA artifacts only; no original application runtime or filesystem reads. */
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import crypto from 'node:crypto'
|
|
import assert from 'node:assert/strict'
|
|
import { parse } from '@babel/parser'
|
|
import postcss from 'postcss'
|
|
|
|
const directory = 'src/renderer/src/capabilities'
|
|
const check = process.argv.includes('--check')
|
|
const sha = value => crypto.createHash('sha256').update(value).digest('hex')
|
|
const bundle = JSON.parse(fs.readFileSync('.tmp/rea-runtime-scripts.json', 'utf8'))
|
|
const pins = {
|
|
'contexts-BMeWSUHk.js': '5f35b7489b107e21ef452c8ee88a95265756f6fc444841eef869228bbc44768b',
|
|
'src-DPDj7_qD.js': '747e41d215375e1214c366213aea7e077d85421923029c61cbb6e9562be58de0',
|
|
'src-0FGg1seX.js': 'f2c6dbae949041b4e2bb456e8a23ae2837f2d7893ac24bf471485f77c7b15178'
|
|
}
|
|
const manifest = {
|
|
version: 'WorkBuddy 5.8.2', rootSha256: '035254bc76310ea46317433e4840602094543ef323d2d5c5aff7c131729de918',
|
|
evidenceId: bundle.evidence_id, sources: [], bodies: [], dictionaries: [], css: [],
|
|
adaptations: [
|
|
'Only selected component/function bodies execute, using the host React singleton. Original module initialization and global service hooks never execute.',
|
|
'CreatedExpertsPanel changes one static SVG URL to the previously REA-exported identical local SVG.',
|
|
'The public facade omits all cloud/share callbacks; ic and oc explicitly return false. Cloud upload/share adapters cannot report success.',
|
|
'Local host adapters provide portal positioning/dismiss/focus, tooltip title, locale, local avatar URL resolution, preview and IPC callbacks.',
|
|
'Original Dropdown, DropdownMenuList/Label, ConfirmDialog and cb Button bodies are preserved; Popover is an explicitly bounded local host adapter.',
|
|
'Original cb theme variable declarations are restored locally; IDE error foreground/background bridge to shipped WB semantic tokens. ConfirmDialog receives the actual host theme. This host bridge is not a runtime color-parity claim.',
|
|
'Styles retain declarations and conditions with a local selector scope. Local portal wrappers carry both capability and created-expert source scopes.'
|
|
]
|
|
}
|
|
function load(name) {
|
|
const item = bundle.items.find(item => item.file_path.endsWith(name)), artifact = item?.source?.artifact
|
|
assert.ok(artifact && sha(artifact.text) === pins[name] && artifact.sha256 === pins[name], `REA source identity: ${name}`)
|
|
manifest.sources.push({ file: name, sha256: artifact.sha256 })
|
|
const entries = new Map(), strings = new Map(), objects = new Map(), dictionaries = []
|
|
function walk(n) {
|
|
if (!n || typeof n !== 'object') return
|
|
if (n.type === 'FunctionDeclaration' && n.id) entries.set(n.id.name, n)
|
|
if (n.type === 'AssignmentExpression' && n.left.type === 'Identifier') {
|
|
entries.set(n.left.name, n)
|
|
if (n.right.type === 'ObjectExpression') objects.set(n.left.name, n.right)
|
|
const value = literal(n.right)
|
|
if (typeof value === 'string') strings.set(n.left.name, value)
|
|
}
|
|
if (n.type === 'ObjectExpression' && n.properties.some(p => p.key?.value === 'myExperts.created.ctxView')) dictionaries.push(n)
|
|
for (const [key, value] of Object.entries(n)) {
|
|
if (key === 'loc' || key === 'comments') continue
|
|
if (Array.isArray(value)) value.forEach(walk)
|
|
else if (value && typeof value === 'object') walk(value)
|
|
}
|
|
}
|
|
walk(parse(artifact.text, { sourceType: 'module' }).program)
|
|
return { ...artifact, name, entries, strings, objects, dictionaries }
|
|
}
|
|
function literal(n) {
|
|
return n?.type === 'StringLiteral' ? n.value : n?.type === 'TemplateLiteral' && !n.expressions.length ? n.quasis[0].value.cooked : undefined
|
|
}
|
|
const contexts = load('contexts-BMeWSUHk.js'), icons = load('src-DPDj7_qD.js'), primitives = load('src-0FGg1seX.js')
|
|
const emptyAsset=directory+'/assets/empty-created-expert.svg'
|
|
assert.equal(sha(fs.readFileSync(emptyAsset)),'157f07ef40d6cdf5f3aed362509861151fbdde0a8a9c89c98c26de6f35f664b7')
|
|
manifest.localDependencies=[{file:emptyAsset,sha256:sha(fs.readFileSync(emptyAsset)),evidenceId:'ev_e5515aa84117724605f73439d065222c2e23b6ab8226be5d03f1ee0c7b387a9b'},{file:directory+'/assets/source-ui.mjs',sha256:sha(fs.readFileSync(directory+'/assets/source-ui.mjs')),recovery:'scripts/rea/recover-capability-center.mjs'}]
|
|
function recover(source, name, adapt = value => value) {
|
|
const n = source.entries.get(name)
|
|
assert.ok(n, `Source body: ${name}`)
|
|
const original = source.text.slice(n.start, n.end), body = adapt(original)
|
|
manifest.bodies.push({ file: source.name, name, start: n.start, end: n.end, sha256: sha(original), ...(body !== original ? { adaptedSha256: sha(body) } : {}) })
|
|
return { code: n.type === 'FunctionDeclaration' ? body : `const ${body};`, body }
|
|
}
|
|
function classMap(source, name) {
|
|
const n = source.objects.get(name)
|
|
assert.ok(n, `Class map: ${name}`)
|
|
return Object.fromEntries(n.properties.map(p => {
|
|
const value = p.value.type === 'Identifier' ? source.strings.get(p.value.name) : literal(p.value)
|
|
assert.equal(typeof value, 'string')
|
|
return [p.key.name ?? p.key.value, value]
|
|
}))
|
|
}
|
|
const ws = classMap(primitives, 'WS'), ei = classMap(contexts, 'EI')
|
|
let selected = []
|
|
for (const name of ['resolveCardI18nText', 'resolveShareExpertName', 'resolveLocalDeleteContentKey', 'MoreDotsIcon$1', 'CloudUploadIcon', 'CreatedExpertDeleteDialog', 'CreatedExpertCard']) selected.push(recover(contexts, name))
|
|
selected.push(recover(contexts, 'CreatedExpertsPanel', text => text.replace('new URL(`empty-created-expert-BOI6XYXm.svg`,import.meta.url)', 'new URL(`./empty-created-expert.svg`,import.meta.url)')))
|
|
for (const name of ['Dropdown', 'DropdownMenuList', 'DropdownItemLabel', 'highlightLabelText', 'useDropdownHoverClose', 'createIcon', 'resolveSize', 'bn']) selected.push(recover(icons, name))
|
|
for (const name of ['ConfirmDialog', 'Ku', 'CloseIcon$2']) selected.push(recover(primitives, name))
|
|
const sourceIconNames = ['Au', 'bF', 'Pl', 'wd', 'Zl', 'ShareIcon']
|
|
for (const name of sourceIconNames) selected.push(recover(icons, name))
|
|
assert.equal(manifest.bodies.find(item => item.name === 'CreatedExpertCard').sha256, 'ecbaba2a34fe1e6afdb608845bf52e3d1e58518c17f8f7108d4ef43b865d3128')
|
|
assert.equal(manifest.bodies.find(item => item.name === 'CreatedExpertsPanel').sha256, '73d81b6f1c3205fa1b8c67639fa821af60f0b29d236be4d2260406a75f579e47')
|
|
|
|
const wantedKeys = new Set(['expertSection', 'teamSection', 'more', 'common.cancel', 'noTeamSearchResult', 'noSearchResult', 'myExperts.created.emptyTitle', 'myExperts.created.teamEmptyTitle'])
|
|
for (const { body } of selected) for (const match of body.matchAll(/`((?:myExperts\.|share\.|common\.)[^`]+)`/g)) wantedKeys.add(match[1])
|
|
const locales = {}
|
|
for (const n of contexts.dictionaries) {
|
|
const entries = Object.fromEntries(n.properties.filter(p => p.type === 'ObjectProperty').map(p => [p.key.name ?? p.key.value, literal(p.value)]))
|
|
const locale = entries['myExperts.created.ctxView'] === '查看专家' ? 'zh' : entries['myExperts.created.ctxView'] === 'View Expert' ? 'en' : null
|
|
if (!locale) continue
|
|
locales[locale] = Object.fromEntries([...wantedKeys].filter(key => entries[key] !== undefined).map(key => [key, entries[key]]))
|
|
manifest.dictionaries.push({ locale, file: contexts.name, start: n.start, end: n.end, sha256: sha(contexts.text.slice(n.start, n.end)), keys: Object.keys(locales[locale]) })
|
|
}
|
|
assert.equal(locales.zh['myExperts.created.ctxDelete'], '删除')
|
|
const aliases = new Map()
|
|
for (const { body } of selected) {
|
|
for (const m of body.matchAll(/\b([\w$]+)\.(useState|useRef|useCallback|useMemo|useEffect|useLayoutEffect|useId|forwardRef|cloneElement|isValidElement)\b/g)) aliases.set(m[1], 'React')
|
|
for (const m of body.matchAll(/\b([\w$]+)\.(jsx|jsxs)\b/g)) aliases.set(m[1], 'JSX')
|
|
}
|
|
aliases.set('RR', 'JSX'); aliases.set('BR', 'JSX'); aliases.set('qS', 'PortalHost')
|
|
const bindingPreamble = [...aliases].map(([name, target]) => `const ${name}=${target};`).join('\n')
|
|
const body = `// Generated by scripts/rea/recover-created-experts.mjs; pinned REA source identities are in the adjacent manifest.
|
|
import * as React from 'react';
|
|
import * as JSX from 'react/jsx-runtime';
|
|
import {createPortal} from 'react-dom';
|
|
import {computePosition,offset,flip,shift,autoUpdate} from '@floating-ui/react';
|
|
import {ExpertCard as ExpertCard$1,ExpertListEmptyState,filterExpertsByKeyword} from './source-ui.mjs';
|
|
import '../created-expert-source.css';
|
|
const dictionaries=${JSON.stringify(locales)};
|
|
const EI=${JSON.stringify(ei)},WS=${JSON.stringify(ws)};
|
|
const safeAsset=value=>/^(?:data:image\\/(?:png|jpeg|webp|gif|svg\\+xml);|blob:|file:|ycode:)/i.test(value||'')?value:'';
|
|
const defaultRef={current:{}};
|
|
function PortalScope({children}){
|
|
const [theme,setTheme]=React.useState(()=>document.documentElement.dataset.theme==='dark'?'dark':'light');
|
|
React.useEffect(()=>{const observer=new MutationObserver(()=>setTheme(document.documentElement.dataset.theme==='dark'?'dark':'light'));observer.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});return()=>observer.disconnect()},[]);
|
|
return JSX.jsx('div',{className:'yc-capability-center yc-created-expert-source expert-center-'+theme,style:{display:'contents'},children});
|
|
}
|
|
const PortalHost={createPortal:(children,container)=>createPortal(JSX.jsx(PortalScope,{children}),container)};
|
|
function HostPopover({trigger,open,onOpenChange,disabled,placement='bottom-start',offsetDistance=4,className='',style,children,role='menu',ariaLabel,onFloatingMouseEnter,onFloatingMouseLeave}){
|
|
const anchor=React.useRef(null),floating=React.useRef(null),id=React.useId(),[position,setPosition]=React.useState({left:0,top:0,visibility:'hidden'}),[phase,setPhase]=React.useState('initial'),[present,setPresent]=React.useState(open);
|
|
React.useLayoutEffect(()=>{if(open){setPresent(true);setPhase('initial');const raf=requestAnimationFrame(()=>setPhase('open'));return()=>cancelAnimationFrame(raf)}setPhase('close');const timer=setTimeout(()=>setPresent(false),window.matchMedia('(prefers-reduced-motion: reduce)').matches?1:150);return()=>clearTimeout(timer)},[open]);
|
|
React.useLayoutEffect(()=>{if(!present||!anchor.current||!floating.current)return;let cancelled=false;const update=()=>computePosition(anchor.current,floating.current,{strategy:'fixed',placement,middleware:[offset(offsetDistance),flip(),shift({padding:8})]}).then(({x,y})=>{if(!cancelled)setPosition({left:x,top:y,visibility:'visible'})});const stop=autoUpdate(anchor.current,floating.current,update);return()=>{cancelled=true;stop()}},[present,placement,offsetDistance]);
|
|
React.useEffect(()=>{if(!open)return;const close=e=>{if(!anchor.current?.contains(e.target)&&!floating.current?.contains(e.target))onOpenChange?.(false)};const key=e=>{if(e.key==='Escape'){e.stopPropagation();onOpenChange?.(false);anchor.current?.focus()}if(['ArrowDown','ArrowUp','Home','End'].includes(e.key)&&floating.current?.contains(document.activeElement)){const items=[...floating.current.querySelectorAll('[role=menuitem]:not([aria-disabled=true])')];if(!items.length)return;e.preventDefault();let index=items.indexOf(document.activeElement);index=e.key==='Home'?0:e.key==='End'?items.length-1:(index+(e.key==='ArrowDown'?1:-1)+items.length)%items.length;items[index]?.focus()}};document.addEventListener('pointerdown',close,true);document.addEventListener('keydown',key,true);return()=>{document.removeEventListener('pointerdown',close,true);document.removeEventListener('keydown',key,true)}},[open,onOpenChange]);
|
|
const probe=React.cloneElement(trigger,{ref:anchor,'aria-expanded':!!open,'aria-haspopup':role,'aria-controls':open?id:undefined,onClick:e=>{trigger.props.onClick?.(e);if(!disabled)onOpenChange?.(!open)},onKeyDown:e=>{trigger.props.onKeyDown?.(e);if(['ArrowDown','ArrowUp'].includes(e.key)&&!disabled){e.preventDefault();onOpenChange?.(true);requestAnimationFrame(()=>floating.current?.querySelector('[role=menuitem]')?.focus())}}});
|
|
return JSX.jsxs(JSX.Fragment,{children:[probe,present&&PortalHost.createPortal(JSX.jsx('div',{ref:floating,id,role,'aria-label':ariaLabel,className:'wb-popover wb-popover--placement-bottom wb-popover--expand-'+phase+' '+className,style:{...style,...position,position:'fixed',zIndex:100010,transformOrigin:'top left'},onClick:e=>e.stopPropagation(),onMouseEnter:onFloatingMouseEnter,onMouseLeave:onFloatingMouseLeave,children}),document.body)]});
|
|
}
|
|
function createBindings(hostRef){
|
|
const host=()=>hostRef.current;
|
|
function useLocale$1(){return host().locale==='en'?'en':'zh'}
|
|
function useExpertI18n(){const locale=useLocale$1();return React.useCallback((key,values)=>{let text=dictionaries[locale]?.[key]??dictionaries.zh[key]??key;for(const[k,v]of Object.entries(values??{}))text=text.replaceAll('{'+k+'}',String(v));return text},[locale])}
|
|
const J=({message,type})=>host().notify?.(message,type),Ua={warning:message=>J({message,type:'warning'})};
|
|
const unavailable=()=>Promise.resolve({success:false,errorMessage:'云端专家服务暂不可用'});
|
|
const resetSecurityState=()=>{},resolveSecurityDialog=()=>{};
|
|
const useShareExpert=()=>({sharing:false,securityBlocked:false,shareExpert:unavailable,securityDialog:null,resetSecurityState,resolveSecurityDialog});
|
|
const ic=()=>false,oc=()=>false;
|
|
const al=()=>({adapter:{},reporter:{reportEvent:()=>{},Events:{}},wb:{platform:{type:'desktop'},shell:{openPath:async value=>{if(!host().openPath)throw Error('打开文件夹暂不可用');return host().openPath(value)}}}});
|
|
const resolveExpertAssetUrl=value=>host().resolveAssetUrl?host().resolveAssetUrl(value):safeAsset(value);
|
|
const useExpertAvatar=value=>({finalAvatarUrl:value});
|
|
const Tooltip=({content,children})=>React.cloneElement(children,content?{title:typeof content==='string'?content:undefined}:{});
|
|
const Popover=HostPopover,Az=160,yn={sm:14,md:16,lg:20,xl:24},hn=()=>null;
|
|
const VirtualDropdownMenuList=()=>null,DropdownFooter=()=>null,ML=()=>null,L_=()=>null,Fs=()=>null,uc=()=>null;
|
|
const ExpertCloudSyncBadge=()=>null,ExpertShareModal=()=>null,CreatedExpertCloudUploadConfirmModal=()=>null;
|
|
const openExpertCloudUploadFlow=unavailable,shouldSkipExpertCloudUploadConfirm=()=>false,setSkipExpertCloudUploadConfirm=()=>{};
|
|
const getCreateUploadSlotCount=()=>0,getQuotaExceededMessage=()=>'',createInitialUploadItemResults=()=>[],getBatchUploadItemResults=()=>[];
|
|
const Uu={default:(...values)=>values.flatMap(value=>typeof value==='string'?[value]:value&&typeof value==='object'?Object.keys(value).filter(key=>value[key]):[]).filter(Boolean).join(' ')};
|
|
const Z={'action.close':'关闭'};
|
|
${bindingPreamble}
|
|
${selected.map(s => s.code).join('\n')}
|
|
const $te=createIcon(Au),lo=createIcon(bF),bo=createIcon(Pl),uo=createIcon(wd),ote=createIcon(Zl,{size:'sm'}),Oa=ShareIcon,Wa=Dropdown;
|
|
function Ie(props){const[theme,setTheme]=React.useState(()=>document.documentElement.dataset.theme==='dark'?'dark':'light');React.useEffect(()=>{const o=new MutationObserver(()=>setTheme(document.documentElement.dataset.theme==='dark'?'dark':'light'));o.observe(document.documentElement,{attributes:true,attributeFilter:['data-theme']});return()=>o.disconnect()},[]);return JSX.jsx(ConfirmDialog,{...props,theme})}
|
|
function TeamDetail({expert,onClose,onSummon}){const locale=useLocale$1();React.useEffect(()=>{const key=e=>{if(e.key==='Escape'&&!e.isComposing){e.stopPropagation();onClose()}};document.addEventListener('keydown',key,true);return()=>document.removeEventListener('keydown',key,true)},[onClose]);const content=host().renderPreview?.({expert,onClose,onSummon})??PortalHost.createPortal(JSX.jsx('div',{className:WS.dialogOverlay,onClick:onClose,children:JSX.jsxs('section',{className:WS.dialogContainer,role:'dialog','aria-modal':true,'aria-label':resolveShareExpertName(expert,locale),onClick:e=>e.stopPropagation(),children:[JSX.jsx('div',{className:WS.dialogHeader,children:resolveShareExpertName(expert,locale)}),JSX.jsx('div',{className:WS.dialogContent,children:resolveCardI18nText(expert.description,locale)}),JSX.jsx(Ku,{onClick:onClose,children:'关闭'})]})}),document.body);return JSX.jsx('div',{style:{display:'contents'},onClick:e=>e.stopPropagation(),onKeyDown:e=>e.stopPropagation(),children:content})}
|
|
return {Card:CreatedExpertCard,Panel:CreatedExpertsPanel};
|
|
}
|
|
const defaultBindings=createBindings(defaultRef),Context=React.createContext({ref:defaultRef,bindings:defaultBindings});
|
|
export function CreatedExpertSourceProvider({host,children}){const ref=React.useRef(host??{});ref.current=host??{};const bindings=React.useMemo(()=>createBindings(ref),[]);const value=React.useMemo(()=>({ref,bindings}),[bindings]);return JSX.jsx(Context.Provider,{value,children:JSX.jsx('div',{className:'yc-created-expert-source',style:{display:'contents'},children})})}
|
|
function useLocalCallbacks(props,ref){const fail=React.useCallback(()=>ref.current.notify?.('操作失败,请重试','warning'),[ref]);const onDelete=React.useCallback(async id=>{try{return await props.onDelete?.(id)}catch{fail();return false}},[props.onDelete,fail]);const onEdit=React.useCallback(expert=>{try{Promise.resolve(props.onEdit?.(expert)).catch(fail)}catch{fail()}},[props.onEdit,fail]);const onUse=React.useCallback((id,prompt)=>{try{Promise.resolve(props.onUse?.(id,prompt)).catch(fail)}catch{fail()}},[props.onUse,fail]);return{onUse,onDelete:props.onDelete?onDelete:undefined,onEdit:props.onEdit?onEdit:undefined}}
|
|
export function CreatedExpertCard(props){const{ref,bindings}=React.useContext(Context);const callbacks=useLocalCallbacks(props,ref);return JSX.jsx(bindings.Card,{expert:props.expert,...callbacks,isMenuOpen:props.isMenuOpen,onToggleMenu:props.onToggleMenu,highlightKeyword:props.highlightKeyword})}
|
|
export function CreatedExpertsPanel(props){const{ref,bindings}=React.useContext(Context);const callbacks=useLocalCallbacks(props,ref);return JSX.jsx(bindings.Panel,{createdExperts:props.createdExperts,activeTab:props.activeTab,onTabChange:props.onTabChange,...callbacks,searchKeyword:props.searchKeyword,toolbarExtra:props.toolbarExtra})}
|
|
`
|
|
|
|
const cssManifest = JSON.parse(fs.readFileSync('.tmp/rea-css-recovered/manifest.json', 'utf8'))
|
|
const cssNames = ['src-Bh3YZ35H.css', 'src-CBaV_A3S.css', 'contexts-BkjwYSZT.css']
|
|
const classes = Object.values(ws).join('|').replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replaceAll('\\|', '|')
|
|
const selectorMatch = new RegExp('ec-cr-|ec-my-|ec-delete-confirm|wb-dropdown|wb-popover|cb-button|'+classes)
|
|
function scope(n) {
|
|
const cloned = n.clone()
|
|
if (cloned.type === 'rule') cloned.selector = cloned.selector.split(',').map(s => ':where(.yc-created-expert-source) '+s.trim()).join(',')
|
|
else if (cloned.type === 'atrule' && !/keyframes$/.test(cloned.name)) cloned.nodes?.forEach((child,index)=>{cloned.nodes[index]=scope(child);cloned.nodes[index].parent=cloned})
|
|
return cloned
|
|
}
|
|
let css = '/* Generated from pinned REA CSS. Source declarations retained; local scope and explicit host portal wrapper. */\n.yc-created-expert-source{display:contents}\n'
|
|
for (const name of cssNames) {
|
|
const entry = cssManifest.files.find(item => item.file === name), text = fs.readFileSync('.tmp/rea-css-recovered/'+name,'utf8')
|
|
assert.equal(sha(text),entry.sha256)
|
|
manifest.sources.push({file:name,sha256:entry.sha256,evidenceId:cssManifest.sourceEvidenceId})
|
|
function include(node) {
|
|
if (node.type === 'rule') return selectorMatch.test(node.selector)
|
|
if (node.type === 'atrule' && /keyframes$/.test(node.name)) return /wb-popover|cb-button/.test(node.params)
|
|
return node.nodes?.some(include)
|
|
}
|
|
function prune(node) {const copy=node.clone();if(copy.type==='atrule'&&!/keyframes$/.test(copy.name))copy.nodes=copy.nodes.filter(include).map(prune);return copy}
|
|
const parsed=postcss.parse(text)
|
|
if(name==='src-CBaV_A3S.css')for(const node of parsed.nodes){
|
|
if(node.type!=='rule'||!node.nodes.some(n=>n.type==='decl'&&n.prop==='--cb-vscode-editor-background'))continue
|
|
let selector=node.selector===':root'?'.yc-created-expert-source':node.selector==='body'?'.yc-created-expert-source':node.selector.startsWith('.light,')?'.expert-center-light .yc-created-expert-source,.yc-created-expert-source.expert-center-light':node.selector.startsWith(':root.dark,')?'.expert-center-dark .yc-created-expert-source,.yc-created-expert-source.expert-center-dark':null
|
|
if(!selector)continue
|
|
const original=text.slice(node.source.start.offset,node.source.end.offset+1),clone=node.clone({selector});clone.nodes=clone.nodes.filter(n=>n.type==='decl'&&n.prop.startsWith('--'))
|
|
manifest.css.push({file:name,start:node.source.start.offset,end:node.source.end.offset+1,sha256:sha(original),adaptation:'Local cb theme token scope; declarations only'})
|
|
css+=clone.toString()+'\n'
|
|
}
|
|
for (const node of parsed.nodes.filter(include)) {
|
|
manifest.css.push({file:name,start:node.source.start.offset,end:node.source.end.offset+1,sha256:sha(text.slice(node.source.start.offset,node.source.end.offset+1))})
|
|
css += scope(prune(node)).toString()+'\n'
|
|
}
|
|
}
|
|
css+='/* Host IDE semantic bridge, not a recovered declaration. */\n.yc-created-expert-source{--vscode-errorForeground:var(--wb-color-text-error-default);--vscode-errorBackground:var(--wb-button-danger-fg-on-solid)}\n'
|
|
const declaration = `import type {ComponentType,ReactNode} from 'react';
|
|
export type ExpertLocaleText=string|{zh?:string;en?:string;[locale:string]:string|undefined};
|
|
export interface CreatedExpertRecord {id:string;name:string;displayName?:ExpertLocaleText;profession?:ExpertLocaleText;description?:ExpertLocaleText;expertType?:'expert'|'team'|string;expertRootDir?:string;avatar?:string;version?:string;updatedAt?:number|string;tags?:unknown[];skills?:unknown[];[field:string]:unknown}
|
|
export interface CreatedExpertPreviewProps {expert:CreatedExpertRecord;onClose():void;onSummon(id:string,unused?:unknown,initPrompt?:string):boolean|void}
|
|
export interface CreatedExpertSourceHost {locale?:'zh'|'en';openPath?(path:string):Promise<string|void>;resolveAssetUrl?(value:string):string;notify?(message:string,type?:string):void;renderPreview?(props:CreatedExpertPreviewProps):ReactNode}
|
|
export const CreatedExpertSourceProvider:ComponentType<{host?:CreatedExpertSourceHost;children?:ReactNode}>;
|
|
export interface CreatedExpertCallbacks {onUse(id:string,initPrompt?:string):void|Promise<void>;onDelete?(id:string):boolean|void|Promise<boolean|void>;onEdit?(expert:CreatedExpertRecord):void|Promise<void>}
|
|
export const CreatedExpertCard:ComponentType<CreatedExpertCallbacks&{expert:CreatedExpertRecord;isMenuOpen?:boolean;onToggleMenu?(id:string,open?:boolean):void;highlightKeyword?:string}>;
|
|
export const CreatedExpertsPanel:ComponentType<CreatedExpertCallbacks&{createdExperts:CreatedExpertRecord[];activeTab:'expert'|'team';onTabChange(tab:'expert'|'team'):void;searchKeyword?:string;toolbarExtra?:ReactNode}>;
|
|
`
|
|
parse(body,{sourceType:'module'})
|
|
const outputs = [[directory+'/assets/created-expert-source.mjs',body],[directory+'/assets/created-expert-source.d.mts',declaration],[directory+'/created-expert-source.css',css],[directory+'/assets/created-expert-source-manifest.json',JSON.stringify(manifest,null,2)+'\n']]
|
|
for (const [file,text] of outputs) {
|
|
if (check) assert.equal(fs.readFileSync(file,'utf8'),text,`Reproducible recovery: ${file}`)
|
|
else {fs.mkdirSync(path.dirname(file),{recursive:true});fs.writeFileSync(file,text)}
|
|
}
|
|
console.log('Created expert source '+(check?'CHECK':'RECOVERY')+' PASS: '+manifest.bodies.length+' bodies, '+manifest.css.length+' CSS records')
|
|
|
|
// Self-contained native regression: explicit test-owned manifests and callbacks only.
|
|
const nativeQaEntry="import React, { useState } from 'react'\nimport { createRoot } from 'react-dom/client'\nimport {CreatedExpertSourceProvider,CreatedExpertsPanel} from '../../src/renderer/src/capabilities/assets/created-expert-source.mjs'\nimport '../../src/renderer/src/workbuddy/typography/tokens.css'\nimport '../../src/renderer/src/styles/tokens.css'\nimport '../../src/renderer/src/styles/app.css'\nimport '../../src/renderer/src/capabilities/capability-center-source.css'\nimport '../../src/renderer/src/capabilities/expert-source.css'\nimport '../../src/renderer/src/capabilities/created-expert-source.css'\nconst w=window as any\nw.events=[];w.deleteMode='false'\nfunction Test(){const [tab,setTab]=useState<'expert'|'team'>('expert'),[keyword,setKeyword]=useState(''),[items,setItems]=useState(w.fixture),[revision,setRevision]=useState(0)\nw.setKeyword=setKeyword;w.setFixture=setItems;w.refresh=()=>setRevision(n=>n+1)\nreturn <div className=\"yc-capability-center\" style={{height:'100vh'}}><section className={'expert-center-page expert-center-'+(document.documentElement.dataset.theme||'light')} style={{height:'100%',padding:24}} data-revision={revision}>\n<CreatedExpertSourceProvider host={{openPath:async path=>{w.events.push(['openPath',path]);return ''},notify:(message,type)=>w.events.push(['notify',message,type]),renderPreview:({expert,onClose,onSummon})=><div role=\"dialog\" data-qa-preview><h2>{expert.name}</h2><button onClick={onClose}>关闭预览</button><button onClick={()=>onSummon(expert.id,undefined,'QA prompt')}>使用预览</button></div>}}>\n<CreatedExpertsPanel createdExperts={items} activeTab={tab} onTabChange={setTab} searchKeyword={keyword} onUse={(id,prompt)=>{w.events.push(['use',id,prompt])}} onEdit={expert=>{w.events.push(['edit',expert.id])}} onDelete={async id=>{w.events.push(['delete',id]);if(w.deleteMode==='throw')throw Error('controlled QA failure');if(w.deleteMode==='false')return false;if(w.deleteMode==='pending')return new Promise(resolve=>{w.finishDelete=()=>resolve(false)});setItems(items=>items.filter(x=>x.id!==id));return true}} />\n</CreatedExpertSourceProvider></section></div>}\ncreateRoot(document.getElementById('root')!).render(<Test/>)\n"
|
|
const nativeQaRunner="const fs=require('node:fs/promises'),path=require('node:path'),assert=require('node:assert/strict')\nif(!process.versions.electron){(async()=>{const env={...process.env};delete env.ELECTRON_RUN_AS_NODE;const c=require('node:child_process').spawn(require('electron'),['--no-sandbox',__filename],{env,windowsHide:true,stdio:['ignore','pipe','pipe']});let log='';c.stdout.on('data',b=>{log+=b;process.stdout.write(b)});c.stderr.on('data',b=>log+=b);const timer=setTimeout(()=>c.kill(),60000);c.once('exit',async code=>{clearTimeout(timer);await fs.writeFile(path.join(__dirname,'native.log'),log);if(code)console.error(log.slice(-4000));process.exitCode=code??1})})()}else{\nconst{app,BrowserWindow}=require('electron');app.disableHardwareAcceleration();app.commandLine.appendSwitch('use-angle','swiftshader');app.commandLine.appendSwitch('disable-gpu-compositing');app.setPath('userData',path.join(__dirname,'profile'));const checks=[],errors=[],requests=[],geometry=[];let win\nconst pause=ms=>new Promise(r=>setTimeout(r,ms)),read=code=>win.webContents.executeJavaScript(code)\nfunction check(label,actual,expected=true){const pass=JSON.stringify(actual)===JSON.stringify(expected);checks.push({label,actual,expected,pass});assert.ok(pass,label)}\nasync function wait(code){for(let i=0;i<200;i++){if(await read(code))return;await pause(20)}throw Error('wait '+code)}\nasync function item(text){await read(`(()=>{const e=[...document.querySelectorAll('[role=menuitem]')].find(e=>e.textContent.trim()===${JSON.stringify(text)});if(!e)throw Error('missing menu item');e.click()})()`);await pause(30)}\nasync function menu(index=0){await read(`document.querySelectorAll('.ec-cr-more-btn')[${index}].click()`);await wait(`document.querySelector('[role=menu]')?.getBoundingClientRect().width>0`);await pause(170)}\nasync function snap(name){await read(`Promise.all(document.getAnimations().map(a=>a.finished.catch(()=>{}))).then(()=>true)`);geometry.push({name,nodes:await read(`['.ec-my-created-toolbar','.ec-my-grid','.ec-cr-card','.ec-cr-more-btn','.wb-popover','.wb-dropdown__item','._dialogContainer_peiew_21','.ec-my-created-empty-icon'].map(selector=>{const e=document.querySelector(selector);if(!e)return{selector,missing:true};const r=e.getBoundingClientRect(),s=getComputedStyle(e);return{selector,bounds:[r.x,r.y,r.width,r.height],fontSize:s.fontSize,lineHeight:s.lineHeight,padding:s.padding,gap:s.gap}})`)});await fs.writeFile(path.join(__dirname,name+'.png'),(await win.webContents.capturePage()).toPNG())}\n;(async()=>{await app.whenReady();win=new BrowserWindow({width:1336,height:901,useContentSize:true,show:false,webPreferences:{contextIsolation:true,nodeIntegration:false}});win.webContents.on('console-message',e=>{if(e.level==='error'||e.level>=3)errors.push(e.message)});win.webContents.session.webRequest.onBeforeRequest((details,callback)=>{if(/^https?:/.test(details.url))requests.push(details.url);callback({})});await win.loadFile(path.join(__dirname,'index.html'));win.showInactive();await wait(`document.querySelectorAll('.ec-cr-card').length===2`);win.webContents.sendInputEvent({type:'mouseMove',x:100,y:100});await pause(180);await snap('cards')\ncheck('source local card without usage metadata height 128',await read(`document.querySelector('.ec-cr-card').getBoundingClientRect().height`),128)\ncheck('source card more button 24x24',await read(`(()=>{const r=document.querySelector('.ec-cr-more-btn').getBoundingClientRect();return[r.width,r.height]})()`),[24,24])\ncheck('source CreatedExpertsPanel toolbar is 24px',await read(`document.querySelector('.ec-my-created-toolbar').getBoundingClientRect().height`),24)\ncheck('local counts expert 2 and team 1',await read(`[...document.querySelectorAll('.ec-my-created-tab-count')].map(n=>n.textContent)`),['2','1'])\nawait menu();check('local menu exact four actions, no cloud/share',await read(`[...document.querySelectorAll('[role=menuitem]')].map(e=>e.textContent.trim())`),['查看专家','修改专家','打开文件夹','删除']);check('menu icons preserve 16x16 default',await read(`[...document.querySelectorAll('.wb-dropdown__icon svg')].map(n=>[n.getAttribute('width'),n.getAttribute('height')])`),[['16','16'],['16','16'],['16','16'],['16','16']]);await snap('menu')\ncheck('dropdown source item min height 32',await read(`document.querySelector('.wb-dropdown__item').getBoundingClientRect().height`),32)\ncheck('menu uses body portal scope',await read(`document.querySelector('[role=menu]').closest('.yc-created-expert-source').parentElement===document.body`));check('menu source bottom-start gap is 4px',await read(`document.querySelector('[role=menu]').getBoundingClientRect().top-document.querySelector('.ec-cr-more-btn').getBoundingClientRect().bottom`),4);await read(`document.querySelectorAll('.ec-cr-more-btn')[1].click()`);await pause(180);check('only one menu open across two cards',await read(`document.querySelectorAll('[role=menu]').length`),1);win.webContents.sendInputEvent({type:'keyDown',keyCode:'Escape'});await pause(180);check('Escape closes dropdown',await read(`!document.querySelector('[role=menu]')`));await menu();await item('修改专家');check('edit receives actual ID',await read(`events.at(-1)`),['edit','qa-local-a']);await pause(180);check('menu closes after selection',await read(`!document.querySelector('[role=menu]')`))\nawait menu();await item('打开文件夹');check('folder delegated to host path',await read(`events.at(-1)[0]`),'openPath')\nawait menu();await item('查看专家');check('view invokes preview adapter',await read(`!!document.querySelector('[data-qa-preview]')`));await read(`document.querySelector('[data-qa-preview] button:last-child').click()`);check('preview preserves init prompt argument',await read(`events.at(-1)`),['use','qa-local-a','QA prompt']);await read(`document.querySelector('[data-qa-preview] button').click()`);await pause(30);check('preview close does not bubble and reopen source card',await read(`!document.querySelector('[data-qa-preview]')`));await read(`document.querySelector('._summonBtn_1hdcm_408').click()`);check('source summon delegates actual local expert ID',await read(`events.at(-1).slice(0,2)`),['use','qa-local-a'])\nawait menu();await item('删除');await wait(`!!document.querySelector('._dialogContainer_peiew_21')`);await snap('delete-dialog');check('light dialog receives actual theme and source cb tokens',await read(`document.querySelector('._dialogOverlay_peiew_7').dataset.theme==='light'&&getComputedStyle(document.querySelector('._dialogContainer_peiew_21')).backgroundColor==='rgb(255, 255, 255)'`));check('source dialog max width 400',await read(`document.querySelector('._dialogContainer_peiew_21').getBoundingClientRect().width`),400)\nconst confirm=`[...document.querySelectorAll('._dialogContainer_peiew_21 button')].find(e=>e.textContent.trim()==='删除').click()`\nawait read(confirm);check('false keeps delete dialog',await read(`!!document.querySelector('._dialogContainer_peiew_21')`));await read(`deleteMode='throw';${confirm}`);await pause(30);check('rejection keeps delete dialog and honest warning',await read(`!!document.querySelector('._dialogContainer_peiew_21')&&events.at(-1)[0]==='notify'`));await read(`deleteMode='pending';${confirm}`);await wait(`!!finishDelete`);check('pending disables confirm',await read(`document.querySelector('._dialogContainer_peiew_21 .cb-button--danger').disabled`));win.webContents.sendInputEvent({type:'keyDown',keyCode:'Escape'});await pause(30);check('pending Escape cannot dismiss',await read(`!!document.querySelector('._dialogContainer_peiew_21')`));await read(`finishDelete()`);await pause(30);await read(`deleteMode='true';${confirm}`);await wait(`document.querySelectorAll('.ec-cr-card').length===1`);check('success removes actual controlled record and dialog',await read(`!document.querySelector('._dialogContainer_peiew_21')`))\nawait menu();await read(`refresh()`);await pause(30);check('host prop rerender preserves source component/menu state',await read(`!!document.querySelector('[role=menu]')`));await read(`setKeyword('NO_MATCH')`);await pause(200);check('search filters records and clears old menu',await read(`!document.querySelector('.ec-cr-card')&&!document.querySelector('[role=menu]')`));await read(`setKeyword('');document.querySelectorAll('[role=tab]')[1].click()`);await wait(`document.querySelectorAll('.ec-cr-card').length===1`);check('team tab uses actual team only',await read(`document.querySelector('.ec-cr-card').textContent.includes('QA临时专家团')`));await read(`setFixture([])`);await wait(`!!document.querySelector('.ec-my-created-empty-icon')`);check('source empty icon 64x64',await read(`(()=>{const r=document.querySelector('.ec-my-created-empty-icon').getBoundingClientRect();return[r.width,r.height]})()`),[64,64]);await snap('empty')\nawait read(`document.documentElement.dataset.theme='dark';setFixture(fixture);document.querySelectorAll('[role=tab]')[0].click()`);await wait(`document.querySelectorAll('.ec-cr-card').length===2`);await menu();await item('删除');await wait(`!!document.querySelector('._dialogContainer_peiew_21')`);check('dark dialog receives actual theme and source cb tokens',await read(`document.querySelector('._dialogOverlay_peiew_7').dataset.theme==='dark'&&getComputedStyle(document.querySelector('._dialogContainer_peiew_21')).backgroundColor==='rgb(30, 30, 30)'`));await snap('dark-dialog');win.webContents.sendInputEvent({type:'keyDown',keyCode:'Escape'});await pause(40);win.setContentSize(480,901);await pause(80);check('480px source grid uses one column',await read(`getComputedStyle(document.querySelector('.ec-my-grid')).gridTemplateColumns.split(' ').length`),1);await read(`setFixture(fixture.map(e=>({...e,profession:{zh:'QA长名称'.repeat(40)}})))`);await pause(80);check('480px document remains bounded',await read(`document.documentElement.scrollWidth<=innerWidth`));await snap('narrow-480');check('no external or WorkBuddy HTTP requests',requests,[]);check('no renderer errors',errors,[])\nawait fs.writeFile(path.join(__dirname,'report.json'),JSON.stringify({checks,geometry,errors,requests,limitations:['Independent production React/source UI fixture with explicitly test-owned local manifests; not full App integration or original live pixel validation.','Cloud/share services omitted and unavailable; Popover positioning/focus is a local adapter.']},null,2));console.log('CREATED_EXPERT_QA '+checks.length+'/'+checks.length+' PASS');app.exit(0)\n})().catch(async e=>{await fs.writeFile(path.join(__dirname,'report.json'),JSON.stringify({error:e.stack,checks,geometry,errors,requests},null,2));console.error(e.stack);app.exit(1)})}\n"
|
|
|
|
if(process.argv.includes('--native-qa')){
|
|
const {build}=await import('esbuild'),{spawn}=await import('node:child_process')
|
|
const qaDirectory=fs.mkdtempSync(path.resolve('.tmp/created-expert-source-'))
|
|
fs.writeFileSync(path.join(qaDirectory,'entry.tsx'),nativeQaEntry)
|
|
fs.writeFileSync(path.join(qaDirectory,'native.cjs'),nativeQaRunner)
|
|
const records=[['qa-local-a','QA临时专家甲','expert'],['qa-local-b','QA临时专家乙','expert'],['qa-local-team','QA临时专家团','team']].map(([id,name,expertType])=>{
|
|
const expertRootDir=path.join(qaDirectory,id);fs.mkdirSync(expertRootDir)
|
|
const record={id,name,expertType,expertRootDir,profession:{zh:name},description:{zh:'仅用于独立组件验收的本机临时目录'},tags:['QA'],version:'1.0.0'}
|
|
fs.writeFileSync(path.join(expertRootDir,'expert.json'),JSON.stringify(record));return record
|
|
})
|
|
await build({entryPoints:[path.join(qaDirectory,'entry.tsx')],bundle:true,platform:'browser',format:'esm',jsx:'automatic',outfile:path.join(qaDirectory,'fixture.js'),loader:{'.svg':'file','.woff2':'file','.woff':'file','.ttf':'file','.png':'file'},define:{'process.env.NODE_ENV':'"production"'},logLevel:'warning'})
|
|
fs.copyFileSync(emptyAsset,path.join(qaDirectory,'empty-created-expert.svg'))
|
|
fs.writeFileSync(path.join(qaDirectory,'index.html'),'<!doctype html><html data-theme="light"><head><meta charset="utf-8"><link rel="stylesheet" href="fixture.css"><style>html,body,#root{height:100%;margin:0}body{overflow:hidden}</style></head><body><div id="root"></div><script>window.fixture='+JSON.stringify(records)+'</script><script type="module" src="fixture.js"></script></body></html>')
|
|
await new Promise((resolve,reject)=>{const child=spawn(process.execPath,[path.join(qaDirectory,'native.cjs')],{stdio:'inherit',windowsHide:true});child.once('error',reject);child.once('exit',code=>code===0?resolve():reject(Error('Native QA failed: '+code)))})
|
|
console.log('Native fixture report: '+path.join(qaDirectory,'report.json'))
|
|
}
|