chore: establish YCode replication baseline and TODO

This commit is contained in:
xiaohan.yuan committed 2026-10-17 02:43:49 +08:00
commit 6dc59f89ba
1118 files changed
+196002

No files matched your search

+118
View File
@@ -0,0 +1,118 @@
import { WebContentsView, type BrowserWindow } from 'electron'
import { randomUUID } from 'node:crypto'
import { clampBrowserHostBounds, normalizeBrowserHostUrl, type BrowserHostBounds, type BrowserHostView } from '../shared/browser-host'
interface BrowserRecord { id: string; owner: BrowserWindow; sessionId?: string; view: WebContentsView; visible: boolean; error?: string; navigationVersion: number }
const browsers = new Map<string, BrowserRecord>()
const owners = new Set<number>()
function requireBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserRecord {
const browser = browsers.get(id)
if (!browser || browser.owner !== owner || browser.sessionId !== sessionId || owner.isDestroyed() || browser.view.webContents.isDestroyed()) throw new Error('浏览器标签不存在或不属于当前任务')
return browser
}
function snapshot(browser: BrowserRecord): BrowserHostView {
const contents = browser.view.webContents
return { id: browser.id, sessionId: browser.sessionId, url: contents.getURL() || 'about:blank',
title: contents.getTitle() || '新标签页', loading: contents.isLoading(), canGoBack: contents.navigationHistory.canGoBack(),
canGoForward: contents.navigationHistory.canGoForward(), visible: browser.visible, error: browser.error }
}
function release(browser: BrowserRecord): void {
browsers.delete(browser.id)
browser.visible = false
if (!browser.owner.isDestroyed()) browser.owner.contentView.removeChildView(browser.view)
if (!browser.view.webContents.isDestroyed()) browser.view.webContents.close({ waitForBeforeUnload: false })
}
function guardNavigation(browser: BrowserRecord, event: { preventDefault(): void }, url: string): void {
try { normalizeBrowserHostUrl(url) } catch { event.preventDefault(); browser.error = '仅支持 HTTP 或 HTTPS 网页地址' }
}
function aborted(error: unknown): boolean { return !!error && typeof error === 'object' && 'code' in error && error.code === 'ERR_ABORTED' }
async function load(browser: BrowserRecord, url: string): Promise<void> {
const version = ++browser.navigationVersion
browser.error = undefined
try { await browser.view.webContents.loadURL(url) }
catch (error) { if (version === browser.navigationVersion && !browser.view.webContents.isDestroyed() && !aborted(error)) browser.error = error instanceof Error ? error.message : String(error) }
}
export function createBrowser(owner: BrowserWindow, sessionId?: string, initialUrl = 'about:blank'): BrowserHostView {
if (owner.isDestroyed()) throw new Error('宿主窗口已关闭')
if (sessionId !== undefined && (typeof sessionId !== 'string' || sessionId.length > 256 || !sessionId)) throw new Error('任务标识无效')
const url = normalizeBrowserHostUrl(initialUrl)
const id = randomUUID()
const view = new WebContentsView({ webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false,
webSecurity: true, allowRunningInsecureContent: false, partition: `ycode-browser-${id}` } })
const browser: BrowserRecord = { id, owner, sessionId, view, visible: false, navigationVersion: 0 }
browsers.set(id, browser)
const contents = view.webContents
contents.session.setPermissionRequestHandler((_contents, _permission, callback) => callback(false))
contents.session.setPermissionCheckHandler(() => false)
contents.session.on('will-download', event => event.preventDefault())
contents.on('will-navigate', (event, nextUrl) => guardNavigation(browser, event, nextUrl))
contents.on('will-redirect', (event, nextUrl) => guardNavigation(browser, event, nextUrl))
// No unmanaged popup escapes the owner/session. Exact original popup routing is unverified.
contents.setWindowOpenHandler(({ url: nextUrl }) => {
try { void load(browser, normalizeBrowserHostUrl(nextUrl)) }
catch { browser.error = '仅支持 HTTP 或 HTTPS 网页地址' }
return { action: 'deny' }
})
contents.on('did-start-navigation', (_event, _nextUrl, _isInPlace, isMainFrame) => { if (isMainFrame) browser.error = undefined })
contents.on('did-fail-load', (_event, code, description, _url, isMainFrame) => { if (isMainFrame && code !== -3) browser.error = description })
contents.on('render-process-gone', (_event, details) => { browser.error = `浏览器进程已结束:${details.reason}` })
contents.on('destroyed', () => browsers.delete(id))
owner.contentView.addChildView(view)
view.setVisible(false)
if (!owners.has(owner.id)) {
owners.add(owner.id)
owner.once('closed', () => { owners.delete(owner.id); for (const record of [...browsers.values()]) if (record.owner === owner) release(record) })
owner.on('resize', () => { if (!owner.isDestroyed()) for (const record of browsers.values()) if (record.owner === owner && record.visible) {
const [width, height] = owner.getContentSize(); const bounds = clampBrowserHostBounds(record.view.getBounds(), width, height)
record.view.setBounds(bounds)
} })
}
if (url !== 'about:blank') void load(browser, url)
return snapshot(browser)
}
export function getBrowserState(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView { return snapshot(requireBrowser(owner, id, sessionId)) }
/** The IPC caller validates the destination YCode session before transferring a draft tab. */
export function rebindBrowser(owner: BrowserWindow, id: string, previousSessionId: string | undefined, nextSessionId: string): BrowserHostView {
const browser = requireBrowser(owner, id, previousSessionId)
if (typeof nextSessionId !== 'string' || !nextSessionId || nextSessionId.length > 256) throw new Error('目标任务标识无效')
browser.sessionId = nextSessionId
return snapshot(browser)
}
export async function navigateBrowser(owner: BrowserWindow, id: string, url: string, sessionId?: string): Promise<BrowserHostView> {
const browser = requireBrowser(owner, id, sessionId)
const target = normalizeBrowserHostUrl(url)
await load(browser, target)
return snapshot(browser)
}
export function backBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView {
const browser = requireBrowser(owner, id, sessionId)
if (browser.view.webContents.navigationHistory.canGoBack()) browser.view.webContents.navigationHistory.goBack()
return snapshot(browser)
}
export function forwardBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView {
const browser = requireBrowser(owner, id, sessionId)
if (browser.view.webContents.navigationHistory.canGoForward()) browser.view.webContents.navigationHistory.goForward()
return snapshot(browser)
}
export function reloadBrowser(owner: BrowserWindow, id: string, sessionId?: string): BrowserHostView {
const browser = requireBrowser(owner, id, sessionId)
browser.error = undefined; browser.view.webContents.reload()
return snapshot(browser)
}
export function setBrowserBounds(owner: BrowserWindow, id: string, bounds: BrowserHostBounds | null, sessionId?: string): void {
// Component teardown can finish after the tab has already released its native view.
if (bounds === null && !browsers.has(id)) return
const browser = requireBrowser(owner, id, sessionId)
if (bounds === null) { browser.view.setVisible(false); browser.visible = false; return }
const [width, height] = owner.getContentSize()
const clamped = clampBrowserHostBounds(bounds, width, height)
browser.view.setBounds(clamped)
browser.visible = clamped.width > 0 && clamped.height > 0
browser.view.setVisible(browser.visible)
}
export function hideBrowser(owner: BrowserWindow, id: string, sessionId?: string): void { setBrowserBounds(owner, id, null, sessionId) }
export function closeBrowser(owner: BrowserWindow, id: string, sessionId?: string): void {
if (!browsers.has(id)) return
release(requireBrowser(owner, id, sessionId))
}
export function disposeOwnerBrowsers(owner: BrowserWindow): void { for (const browser of [...browsers.values()]) if (browser.owner === owner) release(browser) }