Merge branch 'main' into fix(adapters)-shorten-Windows-paths-in-tool-summaries

This commit is contained in:
Raphaelowo
2026-07-14 01:57:00 +08:00
committed by GitHub
206 changed files with 19384 additions and 1660 deletions
+11 -2
View File
@@ -13,6 +13,7 @@ assignees: ''
- [ ] 我已经升级到最新版本后复现过这个问题
- [ ] 我已经搜索过[现有 issues](https://github.com/NanmiCoder/cc-haha/issues),确认没有重复问题
- [ ] 我已经隐藏截图和日志中的 API Key、Token、Cookie 等敏感信息
- [ ] 我已在分享前检查自动生成的诊断报告或诊断包,确认其中没有不应公开的私密元数据
## 问题描述
<!-- 请用 1-3 句话说明实际发生了什么、你原本期望发生什么 -->
@@ -56,6 +57,16 @@ assignees: ''
在此粘贴错误信息和日志
```
## 诊断信息(可选)
<!--
桌面端可在「设置 -> 诊断」中复制 Event ID;复制的 Issue 报告是 Markdown,请粘贴到 Issue 正文中。导出的诊断包是文件,请作为附件上传。
自动生成的报告和诊断包采用尽力脱敏;请在公开分享前自行检查其中的路径、主机名及其他私密元数据。
-->
- 相关 Event IDs:
- 已粘贴到 Issue 正文的诊断报告: 是 / 否
- 诊断包文件附件:
## 截图或录屏
<!-- 请提供错误截图、配置截图或复现录屏;截图前请隐藏敏感信息 -->
@@ -65,5 +76,3 @@ assignees: ''
---
+5
View File
@@ -95,6 +95,11 @@ jobs:
working-directory: adapters
run: bun install
- name: Verify Windows legacy data recovery
if: matrix.smoke_platform == 'windows'
working-directory: desktop
run: bun run test:windows-storage-recovery
- name: Build sidecars
working-directory: desktop
env:
+5
View File
@@ -168,6 +168,11 @@ jobs:
working-directory: adapters
run: bun install
- name: Verify Windows legacy data recovery
if: matrix.smoke_platform == 'windows'
working-directory: desktop
run: bun run test:windows-storage-recovery
- name: Build sidecars
working-directory: desktop
env:
+2 -2
View File
@@ -23,8 +23,8 @@ fi
# Force recovery CLI (simple readline REPL, no Ink TUI)
if [[ "${CLAUDE_CODE_FORCE_RECOVERY_CLI:-0}" == "1" ]]; then
exec bun $ENV_FILE_FLAG ./src/localRecoveryCli.ts "$@"
exec bun --feature=TRANSCRIPT_CLASSIFIER $ENV_FILE_FLAG ./src/localRecoveryCli.ts "$@"
fi
# Default: full CLI with Ink TUI
exec bun $ENV_FILE_FLAG ./src/entrypoints/cli.tsx "$@"
exec bun --feature=TRANSCRIPT_CLASSIFIER $ENV_FILE_FLAG ./src/entrypoints/cli.tsx "$@"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-1
View File
@@ -112,7 +112,6 @@ export const ELECTRON_IPC_VALIDATORS = {
[ELECTRON_IPC_CHANNELS.previewMessage]: () => true,
[ELECTRON_IPC_CHANNELS.appModeGet]: noPayload,
[ELECTRON_IPC_CHANNELS.appModeSet]: optionalRecord,
[ELECTRON_IPC_CHANNELS.appModeDetectPortableDir]: noPayload,
[ELECTRON_IPC_CHANNELS.appModePrepareRestart]: noPayload,
[ELECTRON_IPC_CHANNELS.appModeRestart]: noPayload,
[ELECTRON_IPC_CHANNELS.adaptersRestartSidecar]: noPayload,
-1
View File
@@ -41,7 +41,6 @@ export const ELECTRON_IPC_CHANNELS = {
previewMessage: 'desktop:preview:message',
appModeGet: 'desktop:app-mode:get',
appModeSet: 'desktop:app-mode:set',
appModeDetectPortableDir: 'desktop:app-mode:detect-portable-dir',
appModePrepareRestart: 'desktop:app-mode:prepare-restart',
appModeRestart: 'desktop:app-mode:restart',
adaptersRestartSidecar: 'desktop:adapters:restart-sidecar',
+42
View File
@@ -0,0 +1,42 @@
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
import {
configurePreviewSessionPermissions,
PREVIEW_SESSION_PARTITION,
} from './services/previewSession'
const desktopRoot = existsSync(path.resolve(process.cwd(), 'electron', 'main.ts'))
? process.cwd()
: path.resolve(process.cwd(), 'desktop')
const mainSource = readFileSync(path.join(desktopRoot, 'electron', 'main.ts'), 'utf8')
describe('Electron preview security boundary', () => {
it('uses a dedicated in-memory session partition for remote previews', () => {
expect(PREVIEW_SESSION_PARTITION).toBe('cc-haha-preview')
expect(PREVIEW_SESSION_PARTITION.startsWith('persist:')).toBe(false)
expect(mainSource).toContain('partition: PREVIEW_SESSION_PARTITION')
})
it('denies preview permission checks and requests by default', () => {
const handlers: {
check?: (...args: unknown[]) => boolean
request?: (...args: unknown[]) => void
} = {}
const session = {
setPermissionCheckHandler(handler: (...args: unknown[]) => boolean) {
handlers.check = handler
},
setPermissionRequestHandler(handler: (...args: unknown[]) => void) {
handlers.request = handler
},
}
configurePreviewSessionPermissions(session as never)
expect(handlers.check?.()).toBe(false)
const callback = (allowed: boolean) => expect(allowed).toBe(false)
handlers.request?.(null, 'media', callback)
expect(mainSource).toContain('configurePreviewSessionPermissions(view.webContents.session)')
})
})
+9 -4
View File
@@ -4,6 +4,7 @@ import path from 'node:path'
import { ELECTRON_EVENT_CHANNELS, ELECTRON_INTERNAL_CHANNELS, ELECTRON_IPC_CHANNELS, type ElectronIpcChannel } from './ipc/channels'
import { isElectronIpcChannel, validateElectronIpcPayload } from './ipc/capabilities'
import { ElectronServerRuntime } from './services/serverRuntime'
import { electronHostDiagnosticsFile } from './services/sidecarManager'
import { openDialog, saveDialog } from './services/dialogs'
import { openExternalUrl, openSystemPath, openSystemSettingsUrl } from './services/shell'
import {
@@ -18,12 +19,14 @@ import { ElectronUpdaterService, updaterSessionProxyConfig } from './services/up
import { createUpdateSmokeUpdaterFromEnv } from './services/updateSmoke'
import { ElectronTerminalService, type TerminalSpawnInput } from './services/terminal'
import { ElectronPreviewService, type PreviewBounds } from './services/preview'
import {
configurePreviewSessionPermissions,
PREVIEW_SESSION_PARTITION,
} from './services/previewSession'
import {
applyStartupPortableMode,
detectPortableDir,
getAppMode,
setAppMode,
type PortableDetection,
} from './services/appMode'
import { installMacOsChromiumKeychainPromptGuard } from './services/keychain'
import { applyWindowsAppUserModelId } from './services/appIdentity'
@@ -141,6 +144,7 @@ function getServerRuntime() {
desktopRoot: unpackedRoot(),
appRoot: appRoot(),
h5DistDir: path.join(unpackedRoot(), 'dist'),
diagnosticsFile: electronHostDiagnosticsFile(process.env),
resolveSystemProxy: (url) => session.defaultSession.resolveProxy(url),
})
return serverRuntime
@@ -185,11 +189,13 @@ function getPreviewService() {
const view = new WebContentsView({
webPreferences: {
preload: previewPreloadPath(),
partition: PREVIEW_SESSION_PARTITION,
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
},
})
configurePreviewSessionPermissions(view.webContents.session)
installPreviewNavigationGuards(view.webContents, { openExternal: openExternalUrl })
return view
},
@@ -331,8 +337,7 @@ function registerIpcHandlers() {
registerHandler(ELECTRON_IPC_CHANNELS.previewMessage, (event, payload) => getPreviewService().message(payload, event.sender))
registerHandler(ELECTRON_IPC_CHANNELS.appModeGet, () => getAppMode(app))
registerHandler(ELECTRON_IPC_CHANNELS.appModeSet, (_event, payload) => setAppMode(app, payload as Parameters<typeof setAppMode>[1]))
registerHandler(ELECTRON_IPC_CHANNELS.appModeDetectPortableDir, () => detectPortableDir(app) as PortableDetection)
registerHandler(ELECTRON_IPC_CHANNELS.appModePrepareRestart, () => getServerRuntime().stopAll())
registerHandler(ELECTRON_IPC_CHANNELS.appModePrepareRestart, () => getServerRuntime().stopAll(true))
registerHandler(ELECTRON_IPC_CHANNELS.appModeRestart, () => {
isQuitting = true
app.relaunch()
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,17 @@
import { readFileSync } from 'node:fs'
import path from 'node:path'
import { describe, expect, it } from 'vitest'
describe('app mode restart lifecycle', () => {
it('synchronously stops Windows sidecars before relaunching with another data directory', () => {
const desktopDir = path.basename(process.cwd()) === 'desktop'
? process.cwd()
: path.join(process.cwd(), 'desktop')
const source = readFileSync(path.join(desktopDir, 'electron', 'main.ts'), 'utf8')
const handler = source.match(
/registerHandler\(ELECTRON_IPC_CHANNELS\.appModePrepareRestart,[\s\S]*?\n\s*registerHandler/,
)?.[0]
expect(handler).toContain('getServerRuntime().stopAll(true)')
})
})
File diff suppressed because it is too large Load Diff
+89 -4
View File
@@ -11,13 +11,26 @@ export type PreviewBounds = {
height: number
}
type PreviewCaptureRect = PreviewBounds
type PreviewDebuggerLike = {
isAttached(): boolean
attach(protocolVersion?: string): void
detach(): void
sendCommand(method: string, commandParams?: Record<string, unknown>): Promise<unknown>
}
const FULL_CAPTURE_MAX_EDGE = 16_384
const FULL_CAPTURE_MAX_PIXELS = 32_000_000
export type PreviewWebContentsLike = {
loadURL(url: string): Promise<unknown>
executeJavaScript(script: string): Promise<unknown>
on(event: 'did-finish-load', handler: () => void): unknown
close?(): void
isDestroyed?(): boolean
capturePage?(): Promise<{ toDataURL(): string }>
capturePage?(rect?: PreviewCaptureRect): Promise<{ toDataURL(): string }>
debugger?: PreviewDebuggerLike
setZoomFactor?(factor: number): void
send(channel: string, payload: unknown): void
}
@@ -121,6 +134,10 @@ export class ElectronPreviewService {
private parent: PreviewParentWindowLike | null = null
private requestedBounds: PreviewBounds | null = null
private zoomFactor = 1
private fullCapture: {
webContents: PreviewWebContentsLike
promise: Promise<string>
} | null = null
constructor(options: ElectronPreviewServiceOptions) {
this.createView = options.createView
@@ -217,13 +234,81 @@ export class ElectronPreviewService {
await view.webContents.executeJavaScript(script)
}
private async captureNativeDataUrl(): Promise<string> {
private async captureNativeDataUrl(kind: PreviewHostCaptureMessage['kind'] = 'viewport'): Promise<string> {
const webContents = this.requireView().webContents
if (kind === 'full') return this.captureFullPageDataUrl(webContents)
if (!webContents.capturePage) throw new Error('native preview capture unavailable')
const image = await webContents.capturePage()
return image.toDataURL()
}
private async captureFullPageDataUrl(webContents: PreviewWebContentsLike): Promise<string> {
if (this.fullCapture?.webContents === webContents) {
return await this.fullCapture.promise
}
const promise = this.captureFullPageDataUrlOnce(webContents)
const capture = { webContents, promise }
this.fullCapture = capture
try {
return await promise
} finally {
if (this.fullCapture === capture) this.fullCapture = null
}
}
private async captureFullPageDataUrlOnce(webContents: PreviewWebContentsLike): Promise<string> {
const debuggerApi = webContents.debugger
if (!debuggerApi) throw new Error('full preview capture unavailable')
let attachedHere = false
try {
if (!debuggerApi.isAttached()) {
debuggerApi.attach('1.3')
attachedHere = true
}
const metrics = await debuggerApi.sendCommand('Page.getLayoutMetrics')
if (!isPlainRecord(metrics)) throw new Error('invalid full preview layout metrics')
const contentSize = isPlainRecord(metrics.cssContentSize)
? metrics.cssContentSize
: metrics.contentSize
if (!isPlainRecord(contentSize)) throw new Error('invalid full preview layout metrics')
const width = Math.ceil(Number(contentSize.width))
const height = Math.ceil(Number(contentSize.height))
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
throw new Error('invalid full preview dimensions')
}
if (
width > FULL_CAPTURE_MAX_EDGE ||
height > FULL_CAPTURE_MAX_EDGE ||
width * height > FULL_CAPTURE_MAX_PIXELS
) {
throw new Error(`full preview capture exceeds safety limit: ${width}x${height}`)
}
const screenshot = await debuggerApi.sendCommand('Page.captureScreenshot', {
format: 'png',
fromSurface: true,
captureBeyondViewport: true,
clip: { x: 0, y: 0, width, height, scale: 1 },
})
if (!isPlainRecord(screenshot) || typeof screenshot.data !== 'string' || !screenshot.data) {
throw new Error('invalid full preview screenshot data')
}
return `data:image/png;base64,${screenshot.data}`
} finally {
if (attachedHere) {
try {
if (debuggerApi.isAttached()) debuggerApi.detach()
} catch {
// The page may close while a full-page capture is in flight.
}
}
}
}
private applyZoomFactor(view: PreviewViewLike | null): void {
view?.webContents.setZoomFactor?.(this.zoomFactor)
}
@@ -239,7 +324,7 @@ export class ElectronPreviewService {
renderer.send(ELECTRON_EVENT_CHANNELS.previewEvent, {
v: 1,
type: 'screenshot',
dataUrl: await this.captureNativeDataUrl(),
dataUrl: await this.captureNativeDataUrl(kind),
kind,
})
} catch (error) {
@@ -262,7 +347,7 @@ export class ElectronPreviewService {
screenshot: {
...screenshot,
kind: screenshot.kind ?? 'region',
dataUrl: await this.captureNativeDataUrl(),
dataUrl: await this.captureNativeDataUrl('viewport'),
},
},
}
@@ -0,0 +1,12 @@
import type { Session } from 'electron'
export const PREVIEW_SESSION_PARTITION = 'cc-haha-preview'
export function configurePreviewSessionPermissions(
session: Pick<Session, 'setPermissionCheckHandler' | 'setPermissionRequestHandler'>,
): void {
session.setPermissionCheckHandler(() => false)
session.setPermissionRequestHandler((_webContents, _permission, callback) => {
callback(false)
})
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More