feat(desktop): add handlePreviewLink action dispatcher

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
程序员阿江(Relakkes) 2026-05-29 12:36:59 +08:00
parent 8c4e8073ab
commit 13222b375d
2 changed files with 109 additions and 0 deletions

View File

@ -0,0 +1,66 @@
import { describe, expect, it, vi } from 'vitest'
import { handlePreviewLink, type PreviewLinkDeps } from './handlePreviewLink'
function makeDeps(overrides?: Partial<PreviewLinkDeps>): PreviewLinkDeps {
return {
sessionId: 's1',
serverBaseUrl: 'http://127.0.0.1:8787',
openBrowser: vi.fn(),
openFilePreview: vi.fn(),
openExternal: vi.fn(),
...overrides,
}
}
describe('handlePreviewLink', () => {
it('routes loopback urls to openBrowser with the url', () => {
const deps = makeDeps()
const handled = handlePreviewLink('http://localhost:5173/', deps)
expect(handled).toBe(true)
expect(deps.openBrowser).toHaveBeenCalledWith('s1', 'http://localhost:5173/')
expect(deps.openFilePreview).not.toHaveBeenCalled()
expect(deps.openExternal).not.toHaveBeenCalled()
})
it('routes absolute html file paths to openBrowser with the preview-fs url', () => {
const deps = makeDeps()
const handled = handlePreviewLink('/Users/x/index.html', deps)
expect(handled).toBe(true)
// previewFsUrl preserves the leading slash, yielding an intentional `//`
// between <sessionId> and the absolute path so the server resolves it as
// an absolute-within-workspace path.
expect(deps.openBrowser).toHaveBeenCalledWith(
's1',
'http://127.0.0.1:8787/preview-fs/s1//Users/x/index.html',
)
expect(deps.openFilePreview).not.toHaveBeenCalled()
expect(deps.openExternal).not.toHaveBeenCalled()
})
it('routes relative previewable docs to openFilePreview with the relative path', () => {
const deps = makeDeps()
const handled = handlePreviewLink('docs/report.md', deps)
expect(handled).toBe(true)
expect(deps.openFilePreview).toHaveBeenCalledWith('s1', 'docs/report.md')
expect(deps.openBrowser).not.toHaveBeenCalled()
expect(deps.openExternal).not.toHaveBeenCalled()
})
it('routes remote http(s) to openExternal with the url', () => {
const deps = makeDeps()
const handled = handlePreviewLink('https://example.com/', deps)
expect(handled).toBe(true)
expect(deps.openExternal).toHaveBeenCalledWith('https://example.com/')
expect(deps.openBrowser).not.toHaveBeenCalled()
expect(deps.openFilePreview).not.toHaveBeenCalled()
})
it('returns false for ignored links and calls no deps', () => {
const deps = makeDeps()
const handled = handlePreviewLink('#x', deps)
expect(handled).toBe(false)
expect(deps.openBrowser).not.toHaveBeenCalled()
expect(deps.openFilePreview).not.toHaveBeenCalled()
expect(deps.openExternal).not.toHaveBeenCalled()
})
})

View File

@ -0,0 +1,43 @@
import { classifyPreviewLink } from './previewLinkRouter'
export type PreviewLinkDeps = {
sessionId: string
serverBaseUrl: string
openBrowser: (sessionId: string, url: string) => void
openFilePreview: (sessionId: string, path: string) => void
openExternal: (url: string) => void
}
/**
* Build a `/preview-fs/<sessionId>/<path>` URL for the local server.
*
* Absolute file paths (leading slash) are preserved as-is, so the resulting URL
* carries a `//` between `<sessionId>` and the path. That double slash is
* intentional: the server slices everything after the `<sessionId>` segment and
* runs `path.resolve(workDir, relPath)`, so an absolute path is resolved as an
* absolute-within-workspace path and sandbox-checked against the work dir root.
*/
function previewFsUrl(base: string, sessionId: string, filePath: string): string {
return `${base.replace(/\/$/, '')}/preview-fs/${encodeURIComponent(sessionId)}/${filePath.replace(/^\/+/, '/')}`
}
/** Returns true if handled (caller should preventDefault). */
export function handlePreviewLink(href: string, deps: PreviewLinkDeps): boolean {
const cls = classifyPreviewLink(href)
switch (cls.kind) {
case 'browser-localhost':
deps.openBrowser(deps.sessionId, cls.url!)
return true
case 'browser-file':
deps.openBrowser(deps.sessionId, previewFsUrl(deps.serverBaseUrl, deps.sessionId, cls.path!))
return true
case 'file-preview':
deps.openFilePreview(deps.sessionId, cls.path!)
return true
case 'remote':
deps.openExternal(cls.url!)
return true
default:
return false
}
}