mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 20:03:13 +08:00
Merge branch 'main' into fix(adapters)-shorten-Windows-paths-in-tool-summaries
This commit is contained in:
@@ -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: ''
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)')
|
||||
})
|
||||
})
|
||||
@@ -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
@@ -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
Reference in New Issue
Block a user