Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/presenter/browser/YoBrowserPresenter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class YoBrowserPresenter implements IYoBrowserPresenter {
private readonly screenshotManager = new ScreenshotManager(this.cdpManager)
private readonly downloadManager = new DownloadManager()
private readonly windowPresenter: IWindowPresenter
private readonly embeddedHostReadyTimeoutMs = 2000
private readonly embeddedHostReadyTimeoutMs = 5000
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Rename constant to follow SCREAMING_SNAKE_CASE convention.

The embeddedHostReadyTimeoutMs constant uses camelCase, but should use SCREAMING_SNAKE_CASE per the coding guidelines. As per coding guidelines, constants must use SCREAMING_SNAKE_CASE naming for files in src/**/*.{ts,tsx,js,jsx,vue}.

♻️ Proposed fix to rename the constant
-  private readonly embeddedHostReadyTimeoutMs = 5000
+  private readonly EMBEDDED_HOST_READY_TIMEOUT_MS = 5000

Also update all references to use the new name (line 632 and 635):

// Line 632-633
const error = new Error(
  `Session browser host ${hostWindowId} did not become ready within ${this.EMBEDDED_HOST_READY_TIMEOUT_MS}ms`
)
// Line 635
}, this.EMBEDDED_HOST_READY_TIMEOUT_MS)

Note: Consider also updating embeddedHostReadyStableMs on line 58 for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/presenter/browser/YoBrowserPresenter.ts` at line 57, Rename the
instance constant embeddedHostReadyTimeoutMs to SCREAMING_SNAKE_CASE
(EMBEDDED_HOST_READY_TIMEOUT_MS) and update all usages to the new name (e.g.,
references inside methods that use this.embeddedHostReadyTimeoutMs such as the
error message and setTimeout call); also consider renaming
embeddedHostReadyStableMs to EMBEDDED_HOST_READY_STABLE_MS for consistency and
update all its references similarly. Ensure the property declaration and every
access uses the new identifier(s) (this.EMBEDDED_HOST_READY_TIMEOUT_MS and
this.EMBEDDED_HOST_READY_STABLE_MS) so TypeScript references remain valid.

private readonly embeddedHostReadyStableMs = 120
readonly toolHandler: YoBrowserToolHandler

Expand Down
26 changes: 26 additions & 0 deletions src/main/presenter/windowPresenter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class WindowPresenter implements IWindowPresenter {
private settingsWindowReady = false
private pendingSettingsMessages: PendingSettingsMessage[] = []
private pendingSettingsProviderInstalls: ProviderInstallPreview[] = []
private readonly blockedWindowOpenProtocols = new Set(['about:', 'blob:', 'data:', 'javascript:'])

constructor(configPresenter: IConfigPresenter) {
this.windows = new Map()
Expand Down Expand Up @@ -155,6 +156,30 @@ export class WindowPresenter implements IWindowPresenter {
})
}

private setupManagedWindowOpenHandler(window: BrowserWindow): void {
window.webContents.setWindowOpenHandler(({ url }) => {
if (!url?.trim()) {
return { action: 'deny' }
}

try {
const parsedUrl = new URL(url)
if (this.blockedWindowOpenProtocols.has(parsedUrl.protocol)) {
console.warn(`Blocked attempt to open disallowed URL from managed window: ${url}`)
return { action: 'deny' }
}

shell.openExternal(url).catch((error) => {
console.error(`Failed to open external URL from managed window: ${url}`, error)
})
} catch (error) {
console.warn(`Blocked attempt to open invalid URL from managed window: ${url}`, error)
}

return { action: 'deny' }
})
}

/**
* @deprecated Use openOrFocusSettingsWindow() instead. Settings is now an independent window.
* Open Settings tab if not exists, otherwise focus existing one in the given window.
Expand Down Expand Up @@ -654,6 +679,7 @@ export class WindowPresenter implements IWindowPresenter {
this.windows.set(windowId, appWindow) // 将窗口实例存入 Map

managedWindowState.manage(appWindow) // 管理窗口状态
this.setupManagedWindowOpenHandler(appWindow)

// 应用内容保护设置
const contentProtectionEnabled = this.configPresenter.getContentProtectionEnabled()
Expand Down
136 changes: 126 additions & 10 deletions src/main/presenter/workspacePresenter/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import path from 'path'
import fs from 'fs'
import { execFile } from 'child_process'
import { fileURLToPath } from 'url'
import { promisify } from 'util'
import { shell } from 'electron'
import { FSWatcher, watch } from 'chokidar'
Expand All @@ -9,13 +10,17 @@ import { WORKSPACE_EVENTS } from '@/events'
import { readDirectoryShallow } from './directoryReader'
import { searchWorkspaceFiles } from './workspaceFileSearch'
import {
createWorkspacePreviewFileUrl,
createWorkspacePreviewUrl,
registerWorkspacePreviewFile,
registerWorkspacePreviewRoot,
unregisterWorkspacePreviewFile,
unregisterWorkspacePreviewRoot
} from './workspacePreviewProtocol'
import type {
IFilePresenter,
IWorkspacePresenter,
ResolveMarkdownLinkedFileInput,
WorkspaceFileNode,
WorkspaceFilePreview,
WorkspaceFilePreviewKind,
Expand All @@ -24,7 +29,8 @@ import type {
WorkspaceGitState,
WorkspaceInvalidationEvent,
WorkspaceInvalidationKind,
WorkspaceInvalidationSource
WorkspaceInvalidationSource,
WorkspaceLinkedFileResolution
} from '@shared/presenter'

const execFileAsync = promisify(execFile)
Expand Down Expand Up @@ -96,6 +102,7 @@ const getInvalidationPriority = (kind: WorkspaceInvalidationKind): number => {
*/
export class WorkspacePresenter implements IWorkspacePresenter {
private readonly allowedPaths = new Set<string>()
private readonly allowedExactPaths = new Set<string>()
private readonly filePresenter: IFilePresenter
private readonly watchRuntimes = new Map<string, WorkspaceWatchRuntime>()

Expand Down Expand Up @@ -174,6 +181,11 @@ export class WorkspacePresenter implements IWorkspacePresenter {
for (const runtime of runtimes) {
void this.disposeRuntime(runtime)
}

for (const exactPath of this.allowedExactPaths) {
unregisterWorkspacePreviewFile(exactPath)
}
this.allowedExactPaths.clear()
}

private createContentWatcher(workspacePath: string): FSWatcher {
Expand Down Expand Up @@ -400,6 +412,10 @@ export class WorkspacePresenter implements IWorkspacePresenter {
? normalizedTarget
: `${normalizedTarget}${path.sep}`

if (this.allowedExactPaths.has(normalizedTarget)) {
return true
}

for (const workspace of this.allowedPaths) {
const normalizedWorkspace = this.normalizePathForAccess(workspace)
const workspaceWithSep = normalizedWorkspace.endsWith(path.sep)
Expand Down Expand Up @@ -496,11 +512,78 @@ export class WorkspacePresenter implements IWorkspacePresenter {
filePath: string,
kind: WorkspaceFilePreviewKind
): string | undefined {
if (!workspaceRoot || (kind !== 'html' && kind !== 'pdf' && kind !== 'svg')) {
if (kind !== 'html' && kind !== 'pdf' && kind !== 'svg') {
return undefined
}

return createWorkspacePreviewUrl(workspaceRoot, filePath) ?? undefined
if (workspaceRoot) {
return createWorkspacePreviewUrl(workspaceRoot, filePath) ?? undefined
}

return createWorkspacePreviewFileUrl(filePath)
}

private authorizeExactFile(filePath: string): string {
const normalizedFilePath = this.normalizePathForAccess(filePath)
this.allowedExactPaths.add(normalizedFilePath)
registerWorkspacePreviewFile(normalizedFilePath)
return normalizedFilePath
}

private stripMarkdownLinkDecorators(href: string): string {
const trimmedHref = href.trim()
const queryIndex = trimmedHref.indexOf('?')
const hashIndex = trimmedHref.indexOf('#')
const firstDecoratorIndex = [queryIndex, hashIndex]
.filter((index) => index >= 0)
.sort((left, right) => left - right)[0]

if (firstDecoratorIndex == null) {
return trimmedHref
}

return trimmedHref.slice(0, firstDecoratorIndex)
}

private isAbsoluteWindowsPath(value: string): boolean {
return /^[a-zA-Z]:[\\/]/.test(value)
}

private isAbsoluteMarkdownPath(value: string): boolean {
return value.startsWith('/') || this.isAbsoluteWindowsPath(value)
}

private resolveMarkdownLinkedPath(input: ResolveMarkdownLinkedFileInput): string | null {
const rawHref = this.stripMarkdownLinkDecorators(input.href)
if (!rawHref) {
return null
}

if (rawHref.startsWith('file://')) {
try {
return this.normalizePathForAccess(fileURLToPath(rawHref))
} catch {
return null
}
}

if (this.isAbsoluteMarkdownPath(rawHref)) {
return this.normalizePathForAccess(rawHref)
}

const sourceFilePath = input.sourceFilePath?.trim() || null
const workspacePath = input.workspacePath?.trim() || null
const baseDir = sourceFilePath
? path.dirname(sourceFilePath)
: workspacePath
? workspacePath
: null

if (!baseDir) {
return null
}

return this.normalizePathForAccess(path.resolve(baseDir, rawHref))
}

private async runGitCommand(workspacePath: string, args: string[]): Promise<string | null> {
Expand Down Expand Up @@ -643,6 +726,38 @@ export class WorkspacePresenter implements IWorkspacePresenter {
}
}

async resolveMarkdownLinkedFile(
input: ResolveMarkdownLinkedFileInput
): Promise<WorkspaceLinkedFileResolution | null> {
const resolvedPath = this.resolveMarkdownLinkedPath(input)
if (!resolvedPath) {
return null
}

let stat: fs.Stats
try {
stat = fs.statSync(resolvedPath)
} catch {
return null
}

if (!stat.isFile()) {
return null
}

const normalizedPath = this.authorizeExactFile(resolvedPath)
const workspaceRoot = this.getWorkspaceRootForPath(normalizedPath)

return {
path: normalizedPath,
name: path.basename(normalizedPath),
relativePath: workspaceRoot
? this.toRelativeWorkspacePath(workspaceRoot, normalizedPath)
: normalizedPath,
workspaceRoot
}
}

async readFilePreview(filePath: string): Promise<WorkspaceFilePreview | null> {
if (!this.isPathAllowed(filePath)) {
console.warn(`[Workspace] Blocked preview attempt for unauthorized path: ${filePath}`)
Expand All @@ -655,21 +770,22 @@ export class WorkspacePresenter implements IWorkspacePresenter {
undefined,
'origin'
)
const workspaceRoot = this.getWorkspaceRootForPath(filePath)
const kind = this.resolvePreviewKind(preparedFile.mimeType, filePath)
const normalizedPreparedPath = this.normalizePathForAccess(preparedFile.path)
const workspaceRoot = this.getWorkspaceRootForPath(normalizedPreparedPath)
const kind = this.resolvePreviewKind(preparedFile.mimeType, normalizedPreparedPath)

return {
path: preparedFile.path,
path: normalizedPreparedPath,
relativePath: workspaceRoot
? this.toRelativeWorkspacePath(workspaceRoot, preparedFile.path)
: path.basename(preparedFile.path),
? this.toRelativeWorkspacePath(workspaceRoot, normalizedPreparedPath)
: normalizedPreparedPath,
name: preparedFile.name,
mimeType: preparedFile.mimeType,
kind,
content: kind === 'image' ? (preparedFile.thumbnail ?? '') : (preparedFile.content ?? ''),
previewUrl: this.resolvePreviewUrl(workspaceRoot, preparedFile.path, kind),
previewUrl: this.resolvePreviewUrl(workspaceRoot, normalizedPreparedPath, kind),
thumbnail: preparedFile.thumbnail,
language: this.inferLanguage(filePath, kind),
language: this.inferLanguage(normalizedPreparedPath, kind),
metadata: {
...preparedFile.metadata
}
Expand Down
Loading
Loading