test(ui): restore offline renderer verification bridge

Add the startup API stubs required by the current renderer tree so home, conversation fixtures, and settings panels mount in the offline harness.
This commit is contained in:
xiaohan.yuan committed 2026-10-18 18:36:43 +08:00
1 parent e73c59b10e
commit 98cf32b6e2
1 file changed
+81 -7
+81 -7
View File
@@ -34,17 +34,90 @@ const MIME = {
'.ttf': 'font/ttf' '.ttf': 'font/ttf'
} }
// 假桥:只提供渲染进程首屏需要的调用,其余返回空/默认值 /**
* 假桥:只提供渲染进程首屏挂载需要的调用,其余返回空数组/空对象/undefined。
*
* 形状对齐 src/preload/index.ts 暴露的 window.ycode;主窗口(index.html)与设置窗口
* (settings.html / index.html?window=settings)共用同一份假桥。命名空间清单来自启动路径:
* App.tsx、stores/chat.ts、stores/settings.ts、ChatPane.tsx、Sidebar.tsx、
* SettingsDialog.tsx、settings.tsx、TitleBar.tsx、GeneralSettingsPanel.tsx、
* useComposerConnectors.ts、ComposerContextInfo.tsx。
*/
const STUB = `<script> const STUB = `<script>
window.ycode = { (function () {
function generalView() { return { autoLaunch: false, preventSleep: true, preventSleepActive: false, dataDir: '', dataBytes: 0, dataUsagePartial: false, disk: { totalBytes: 0, freeBytes: 0 }, workspaceRoot: '' } }
function personalizationView() { return { userCallName: '', aiName: '', tone: 'default', loadingTips: true, showFileChanges: false, customPrompt: '', persona: '', longTermMemory: '' } }
function memoryView() { return { conversationMemory: true, localMemorySkills: true, content: '' } }
function modelRegistry() { return { models: [], selectedModelId: null } }
function workspaceRegistry() { return { workspaces: [], selectedWorkspaceId: null } }
function sessionRecord(id) { return { id: id || 'offline-session', title: '新对话', createdAt: Date.now(), updatedAt: Date.now() } }
function fixtureValue(key, fallback) { var value = window.__YCODE_FIXTURE__ ? window.__YCODE_FIXTURE__[key] : undefined; return value === undefined ? fallback : value }
window.ycode = {
window: { minimize(){}, toggleMaximize(){}, close(){}, getState: async()=>({maximized:false,fullscreen:false}), onState:()=>()=>{}, onFocus:()=>()=>{} }, window: { minimize(){}, toggleMaximize(){}, close(){}, getState: async()=>({maximized:false,fullscreen:false}), onState:()=>()=>{}, onFocus:()=>()=>{} },
edit: { action: async()=>{} }, edit: { action: async()=>{} },
app: { info: async()=>({version:'0.1.0',electron:'37',chromium:'',node:'',configDir:''}), openConfigDir: async()=>{} }, app: {
info: async()=>({name:'YCode',version:'0.1.0',electron:'37',chromium:'',node:'',configDir:'',omp:{available:false,exePath:null,profile:'',model:null}}),
openConfigDir: async()=>{},
// 原版由主进程另开设置窗口(settings.html?window=settings);离线替身在同一标签内切换。
openSettings: async(section,action)=>{ try { const url=new URL('settings.html?window=settings',location.href); if(section) url.searchParams.set('section',String(section)); if(action) url.searchParams.set('action',String(action)); location.assign(url.href) } catch {} },
settingsReady: async()=>{},
settingsRendered: async()=>{},
onSettingsNavigate: ()=>()=>{},
onSettingsClosed: ()=>()=>{},
onSettingsChanged: ()=>()=>{}
},
config: { get: async()=>({baseUrl:'',model:'',hasApiKey:false,keyStorage:'plain',maxToolRounds:8}), update: async(p)=>({...p,hasApiKey:false,keyStorage:'plain'}) }, 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:''}) }, general: { get: async()=>generalView(), update: async()=>generalView(), changeWorkspaceRoot: async()=>generalView(), openFolder: async()=>{}, openSystemNotificationSettings: async()=>{} },
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()=>{} }, personalization: { get: async()=>personalizationView(), update: async()=>personalizationView() },
chat: { send: async()=>{}, cancel: async()=>{}, onEvent:()=>()=>{} }, agents: { get: async()=>({teamManagement:true}), update: async()=>({teamManagement:true}) },
memory: { get: async()=>memoryView(), update: async()=>memoryView(), import: async()=>memoryView() },
models: {
list: async()=>modelRegistry(), save: async()=>modelRegistry(), remove: async()=>modelRegistry(), select: async()=>modelRegistry(),
fetch: async()=>({models:[]}), test: async()=>({ok:true}), metadata: async()=>({}), metadataStatus: async()=>null, refreshMetadata: async()=>null
},
workspace: {
get: async()=>({root:'C:\\\\Users\\\\Administrator\\\\code\\\\YCode'}),
choose: async()=>({root:''}),
list: async()=>workspaceRegistry(), generate: async()=>workspaceRegistry(), attach: async()=>({attachedWorkspaceId:undefined,...workspaceRegistry()}),
select: async()=>workspaceRegistry(), rename: async()=>workspaceRegistry(), remove: async()=>workspaceRegistry(), open: async()=>{}
},
session: {
list: async()=>structuredClone(fixtureValue('sessions',[])),
listArchived: async()=>structuredClone(fixtureValue('archivedSessions',[])),
create: async()=>sessionRecord('offline-session'),
updateContext: async(id)=>sessionRecord(id),
messages: async(id)=>structuredClone(fixtureValue('messages',{})[id] ?? []),
contextUsage: async()=>({}),
rename: async()=>{}, remove: async()=>{}, setPinned: async(id)=>sessionRecord(id), setArchived: async(id)=>sessionRecord(id),
openFolder: async()=>{}, openWindow: async()=>{}
},
chat: {
send: async(payload)=>({runId:'offline-run',assistantMessageId:'offline-assistant',modelId:'',modelName:'离线假桥',userMessage:{id:'offline-user-message',role:'user',content:payload&&payload.text,createdAt:Date.now(),status:'done'}}),
cancel: async()=>{}, cancelAgent: async()=>{}, onEvent:()=>()=>{}, onStarted:()=>()=>{},
activeRun: async()=>null, inspectEdit: async()=>({editable:false,reason:'离线假桥不支持就地编辑'}),
revert: async()=>({messages:[]}), respond: async()=>({accepted:false}), feedback: async()=>{}
},
experts: { list: async()=>[], read: async()=>({}), activate: async()=>({}), remove: async()=>{}, openPath: async()=>{} },
skills: { list: async()=>[], importPath: async()=>{}, read: async()=>({}), setEnabled: async()=>({}), remove: async()=>{}, export: async()=>{} },
mcp: { list: async()=>[], save: async()=>({}), trust: async()=>({}), connect: async()=>({}), disconnect: async()=>({}), setEnabled: async()=>({}), setToolEnabled: async()=>({}), remove: async()=>{} },
projects: { list: async()=>[], create: async()=>({projectId:'offline-project',name:'离线项目',instructions:'',createdAt:0,updatedAt:0}), update: async()=>({projectId:'offline-project',name:'离线项目',instructions:'',createdAt:0,updatedAt:0}) },
automation: { list: async()=>[], listRuns: async()=>[], create: async()=>({}), update: async()=>({}), setEnabled: async()=>({}), remove: async()=>{}, runNow: async()=>({}), onChanged:()=>()=>{} },
library: { getSnapshot: async()=>({artifacts:[],generatedAt:Date.now()}), setFavorite: async()=>({version:1,favoriteArtifactIds:[],recent:[]}), recordRecent: async()=>({version:1,favoriteArtifactIds:[],recent:[]}) },
attachments: { pick: async()=>[], stage: async(input)=>({id:'offline-attachment',name:(input&&input.name)||'',mimeType:(input&&input.mimeType)||'',size:0}), preview: async()=>undefined, remove: async()=>{} },
speech: { transcribe: async()=>({text:''}) },
git: { branch: async()=>({repo:false,branch:'',unavailable:true}) },
browser: {
create: async()=>({id:'offline-browser',title:'离线浏览器',url:'about:blank'}),
get: async(id)=>({id:id,title:'',url:'about:blank',canGoBack:false,canGoForward:false,loading:false}),
navigate: async()=>({}), back: async()=>({}), forward: async()=>({}), reload: async()=>({}), bounds: async()=>{}, close: async()=>{}, rebind: async()=>{}
},
terminal: {
create: async()=>({id:'offline-terminal',title:'终端'}),
get: async(id)=>({id:id,sequence:0,exited:false}),
write: async()=>{}, resize: async()=>{}, clear: async()=>{}, close: async()=>{}, rebind: async()=>{}, onEvent:()=>()=>{}
},
resources: { resources: {
list: async()=>[],
openExternal: async(url)=>{document.documentElement.dataset.fixtureExternal=url}, openExternal: async(url)=>{document.documentElement.dataset.fixtureExternal=url},
classify: async(path)=>path==='src'?'directory':window.__YCODE_FIXTURE__?.files?.[path]!==undefined?'file':'unknown', 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), resolveImage: async(source)=>window.__YCODE_FIXTURE__?.images?.[source] ?? (/^(https?:|data:|blob:)/.test(source)?source:undefined),
@@ -65,7 +138,8 @@ window.ycode = {
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}}, 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'}} downloadImage: async(source)=>{document.documentElement.dataset.fixtureImageDownload=source.slice(0,48);return {saved:true,path:'offline-fixture/image'}}
} }
}; };
})();
</script>` </script>`
const server = createServer(async (req, res) => { const server = createServer(async (req, res) => {