101 lines
6.2 KiB
JavaScript
101 lines
6.2 KiB
JavaScript
/**
|
|
* 无人值守 UI 验证:用本地静态服务器托管已构建的渲染产物,
|
|
* 注入 window.ycode 假桥(真实主进程不可用时的替身),供浏览器读取布局。
|
|
*
|
|
* 用法:node scripts/verify-ui.mjs [port]
|
|
* 打开 http://127.0.0.1:<port>/ 后用 CDP/浏览器读取 .app-shell 网格列宽。
|
|
*/
|
|
|
|
import { createServer } from 'node:http'
|
|
import { readFile } from 'node:fs/promises'
|
|
import { extname, join, normalize } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
import { chatParityFixture } from './fixtures/chat-parity.mjs'
|
|
import { sourceRecoveryFixture } from './fixtures/source-recovery.mjs'
|
|
import { dialogueMediaFixture } from './fixtures/dialogue-media.mjs'
|
|
import { typographyFixture, typographyProbes } from './fixtures/typography.mjs'
|
|
import { conversationCompleteFixture, conversationCompleteBridge } from './fixtures/conversation-complete.mjs'
|
|
import { artifactSourceFixture } from './fixtures/artifact-source.mjs'
|
|
import { pierreDiffFixture } from './fixtures/pierre-diff.mjs'
|
|
import { conversationStatesFixture, conversationStatesBridge } from './fixtures/conversation-states.mjs'
|
|
|
|
const root = fileURLToPath(new URL('../out/renderer', import.meta.url))
|
|
const port = Number(process.argv[2] ?? 8788)
|
|
|
|
const MIME = {
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.svg': 'image/svg+xml',
|
|
'.png': 'image/png',
|
|
'.woff2': 'font/woff2',
|
|
'.woff': 'font/woff',
|
|
'.ttf': 'font/ttf'
|
|
}
|
|
|
|
// 假桥:只提供渲染进程首屏需要的调用,其余返回空/默认值
|
|
const STUB = `<script>
|
|
window.ycode = {
|
|
window: { minimize(){}, toggleMaximize(){}, close(){}, getState: async()=>({maximized:false,fullscreen:false}), onState:()=>()=>{}, onFocus:()=>()=>{} },
|
|
edit: { action: async()=>{} },
|
|
app: { info: async()=>({version:'0.1.0',electron:'37',chromium:'',node:'',configDir:''}), openConfigDir: async()=>{} },
|
|
config: { get: async()=>({baseUrl:'',model:'',hasApiKey:false,keyStorage:'plain',maxToolRounds:8}), update: async(p)=>({...p,hasApiKey:false,keyStorage:'plain'}) },
|
|
workspace: { get: async()=>({root:'C:\\\\Users\\\\Administrator\\\\code\\\\YCode'}), choose: async()=>({root:''}) },
|
|
session: { list: async()=>structuredClone(window.__YCODE_FIXTURE__?.sessions ?? []), create: async()=>({id:'s1',title:'新对话'}), messages: async(id)=>structuredClone(window.__YCODE_FIXTURE__?.messages[id] ?? []), rename: async()=>{}, remove: async()=>{} },
|
|
chat: { send: async()=>{}, cancel: async()=>{}, onEvent:()=>()=>{} },
|
|
resources: {
|
|
openExternal: async(url)=>{document.documentElement.dataset.fixtureExternal=url},
|
|
classify: async(path)=>path==='src'?'directory':window.__YCODE_FIXTURE__?.files?.[path]!==undefined?'file':'unknown',
|
|
resolveImage: async(source)=>window.__YCODE_FIXTURE__?.images?.[source] ?? (/^(https?:|data:|blob:)/.test(source)?source:undefined),
|
|
read: async(path,range)=>{
|
|
if(path==='src') return {kind:'directory',path,name:'src',entries:[{name:'example.ts',path:'src/example.ts',type:'file'}],truncated:false};
|
|
const content=window.__YCODE_FIXTURE__?.files?.[path];
|
|
if(content===undefined) throw new Error('Offline fixture: file not found');
|
|
return {kind:'text',path,name:path.split('/').pop(),content,firstLine:1,totalLines:content.split('\\n').length,range,truncated:false};
|
|
},
|
|
readText: async(path)=>{
|
|
document.documentElement.dataset.fixtureReadText=path;
|
|
const content=window.__YCODE_FIXTURE__?.files?.[path];
|
|
const delay=window.__YCODE_FIXTURE__?.delays?.[path];
|
|
if(delay) await new Promise(resolve=>setTimeout(resolve,delay));
|
|
if(content===undefined) throw new Error('Offline fixture: file not found');
|
|
return content;
|
|
},
|
|
download: async(payload)=>{document.documentElement.dataset.fixtureDownload=JSON.stringify({filename:payload.filename,mimeType:payload.mimeType,encoding:payload.encoding,length:payload.data.length,prefix:payload.data.slice(0,24)});return {saved:true,path:'offline-fixture/'+payload.filename}},
|
|
downloadImage: async(source)=>{document.documentElement.dataset.fixtureImageDownload=source.slice(0,48);return {saved:true,path:'offline-fixture/image'}}
|
|
}
|
|
};
|
|
</script>`
|
|
|
|
const server = createServer(async (req, res) => {
|
|
try {
|
|
const urlPath = decodeURIComponent((req.url ?? '/').split('?')[0])
|
|
const rel = urlPath === '/' ? '/index.html' : urlPath
|
|
const filePath = normalize(join(root, rel))
|
|
if (!filePath.startsWith(root)) {
|
|
res.writeHead(403).end('forbidden')
|
|
return
|
|
}
|
|
let body = await readFile(filePath)
|
|
if (rel.endsWith('.html')) {
|
|
const fixture = new URL(req.url ?? '/', 'http://127.0.0.1').searchParams.get('fixture')
|
|
const selectedFixture = fixture === 'conversation-states' ? conversationStatesFixture : fixture === 'pierre-diff' ? pierreDiffFixture : fixture === 'artifact-source' ? artifactSourceFixture : fixture === 'conversation-complete' ? conversationCompleteFixture : fixture === 'chat-parity' ? chatParityFixture : fixture === 'source-recovery' ? sourceRecoveryFixture : fixture === 'dialogue-media' ? dialogueMediaFixture : fixture === 'typography' ? typographyFixture : null
|
|
const fixtureScript = selectedFixture
|
|
? `<script>window.__YCODE_FIXTURE__=${JSON.stringify(selectedFixture).replace(/</g, '\\u003c')};Object.defineProperty(navigator,'clipboard',{value:{writeText:async(text)=>{window.__fixtureCopied=text}},configurable:true});</script>`
|
|
: ''
|
|
// 在首个 module script 之前注入假桥
|
|
body = Buffer.from(body.toString('utf8').replace('<script type="module"', () => `${fixtureScript}${STUB}${fixture === 'conversation-states' ? conversationStatesBridge : fixture === 'conversation-complete' ? conversationCompleteBridge : ''}<script type="module"`))
|
|
if (fixture === 'typography') body = Buffer.from(body.toString('utf8').replace('</body>', () => `${typographyProbes}</body>`))
|
|
}
|
|
res.writeHead(200, { 'content-type': MIME[extname(filePath)] ?? 'application/octet-stream' })
|
|
res.end(body)
|
|
} catch {
|
|
res.writeHead(404).end('not found')
|
|
}
|
|
})
|
|
|
|
server.listen(port, '127.0.0.1', () => {
|
|
console.log(`verify-ui listening on http://127.0.0.1:${server.address().port}/`)
|
|
})
|