From ced5080019be56b236bf60a50cdabd6f4f8e4e8c Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 6 Apr 2026 10:11:03 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E5=BC=80=E5=90=AF=E9=BC=A0?= =?UTF-8?q?=E6=A0=87=E7=82=B9=E5=87=BB=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks/notifs/useNpmDeprecationNotification.tsx | 2 +- src/utils/fullscreen.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks/notifs/useNpmDeprecationNotification.tsx b/src/hooks/notifs/useNpmDeprecationNotification.tsx index ddcb68d82..e6d62bff7 100644 --- a/src/hooks/notifs/useNpmDeprecationNotification.tsx +++ b/src/hooks/notifs/useNpmDeprecationNotification.tsx @@ -4,7 +4,7 @@ import { isEnvTruthy } from 'src/utils/envUtils.js' import { useStartupNotification } from './useStartupNotification.js' const NPM_DEPRECATION_MESSAGE = - 'Claude Code has switched from npm to native installer. Run `claude install` or see https://docs.anthropic.com/en/docs/claude-code/getting-started for more options.' + '' export function useNpmDeprecationNotification(): void { useStartupNotification(async () => { diff --git a/src/utils/fullscreen.ts b/src/utils/fullscreen.ts index 9d344177f..be6c3c0bc 100644 --- a/src/utils/fullscreen.ts +++ b/src/utils/fullscreen.ts @@ -125,7 +125,7 @@ export function isFullscreenEnvEnabled(): boolean { } return false } - return process.env.USER_TYPE === 'ant' + return true } /** From cd70c1b7fd787f1b9bb65ee2661caf5071091a41 Mon Sep 17 00:00:00 2001 From: uk0 Date: Mon, 6 Apr 2026 10:15:56 +0800 Subject: [PATCH 2/7] fix MACRO fallback --- src/entrypoints/cli.tsx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx index a1f173e6c..632cc6d9b 100644 --- a/src/entrypoints/cli.tsx +++ b/src/entrypoints/cli.tsx @@ -1,6 +1,20 @@ #!/usr/bin/env bun import { feature } from 'bun:bundle' +// Runtime fallback for MACRO.* when not injected by build/dev defines. +// This happens when running cli.tsx directly (not via `bun run dev` or built dist/). +if (typeof globalThis.MACRO === 'undefined') { + ;(globalThis as any).MACRO = { + VERSION: process.env.CLAUDE_CODE_VERSION || '2.1.888', + BUILD_TIME: new Date().toISOString(), + FEEDBACK_CHANNEL: '', + ISSUES_EXPLAINER: '', + NATIVE_PACKAGE_URL: '', + PACKAGE_URL: '', + VERSION_CHANGELOG: '', + } +} + // Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons // eslint-disable-next-line custom-rules/no-top-level-side-effects process.env.COREPACK_ENABLE_AUTO_PIN = '0' From dee2ffd638f7b3487f077bbd5aeb6192bae4085b Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 6 Apr 2026 10:31:15 +0800 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E7=AE=80?= =?UTF-8?q?=E5=8C=96=E7=89=88=E6=9C=AC=E7=9A=84=20rg=20=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +- scripts/postinstall.cjs | 319 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 322 insertions(+), 2 deletions(-) create mode 100644 scripts/postinstall.cjs diff --git a/package.json b/package.json index fd859bdc6..6d066da69 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ ], "files": [ "dist", - "scripts/download-ripgrep.ts" + "scripts/download-ripgrep.ts", + "scripts/postinstall.cjs" ], "scripts": { "build": "bun run build.ts", @@ -48,7 +49,7 @@ "test": "bun test", "check:unused": "knip-bun", "health": "bun run scripts/health-check.ts", - "postinstall": "node dist/download-ripgrep.js || bun run scripts/download-ripgrep.ts || true", + "postinstall": "node scripts/postinstall.cjs", "docs:dev": "npx mintlify dev" }, "dependencies": {}, diff --git a/scripts/postinstall.cjs b/scripts/postinstall.cjs new file mode 100644 index 000000000..245b4f36d --- /dev/null +++ b/scripts/postinstall.cjs @@ -0,0 +1,319 @@ +#!/usr/bin/env node +/** + * Postinstall script — runs automatically after `bun install` or `npm install`. + * + * Downloads ripgrep binary (idempotent, skips if exists). + * Works in dev mode (src/ exists), published mode (dist/ exists), with bun or node. + * + * Usage: + * node scripts/postinstall.js + * node scripts/postinstall.js --force + * bun run scripts/postinstall.js + */ + +const { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, chmodSync } = + require("fs") +const { spawnSync } = require("child_process") +const { setDefaultResultOrder } = require("node:dns") +const path = require("path") +const os = require("os") + +// Prefer IPv4 first — Bun on Windows sometimes fails GitHub over broken IPv6 paths. +try { + setDefaultResultOrder("ipv4first") +} catch { + /* ignore */ +} + +// --- Config --- + +const RG_VERSION = "15.0.1" +const DEFAULT_RELEASE_BASE = `https://github.com/microsoft/ripgrep-prebuilt/releases/download/v${RG_VERSION}` +const RELEASE_BASE = (process.env.RIPGREP_DOWNLOAD_BASE ?? DEFAULT_RELEASE_BASE).replace(/\/$/, "") + +const scriptDir = path.dirname(__filename) +const projectRoot = path.resolve(scriptDir, "..") + +// --- Platform mapping --- + +function getPlatformMapping() { + const arch = process.arch + const platform = process.platform + + if (platform === "darwin") { + if (arch === "arm64") return { target: "aarch64-apple-darwin", ext: "tar.gz" } + if (arch === "x64") return { target: "x86_64-apple-darwin", ext: "tar.gz" } + throw new Error(`Unsupported macOS arch: ${arch}`) + } + + if (platform === "win32") { + if (arch === "x64") return { target: "x86_64-pc-windows-msvc", ext: "zip" } + if (arch === "arm64") return { target: "aarch64-pc-windows-msvc", ext: "zip" } + throw new Error(`Unsupported Windows arch: ${arch}`) + } + + if (platform === "linux") { + const isMusl = detectMusl() + if (arch === "x64") { + return { target: "x86_64-unknown-linux-musl", ext: "tar.gz" } + } + if (arch === "arm64") { + return isMusl + ? { target: "aarch64-unknown-linux-musl", ext: "tar.gz" } + : { target: "aarch64-unknown-linux-gnu", ext: "tar.gz" } + } + throw new Error(`Unsupported Linux arch: ${arch}`) + } + + throw new Error(`Unsupported platform: ${platform}`) +} + +function detectMusl() { + const muslArch = process.arch === "x64" ? "x86_64" : "aarch64" + try { + statSync(`/lib/libc.musl-${muslArch}.so.1`) + return true + } catch { + return false + } +} + +// --- Paths --- + +function getVendorDir() { + if (existsSync(path.join(projectRoot, "src"))) { + return path.resolve(projectRoot, "src", "utils", "vendor", "ripgrep") + } + return path.resolve(projectRoot, "dist", "vendor", "ripgrep") +} + +function getBinaryPath() { + const dir = getVendorDir() + const subdir = `${process.arch}-${process.platform}` + const binary = process.platform === "win32" ? "rg.exe" : "rg" + return path.resolve(dir, subdir, binary) +} + +// --- Download helpers --- + +function proxyEnvSet() { + const v = (s) => (s ?? "").trim() + return !!(v(process.env.HTTPS_PROXY) || v(process.env.HTTP_PROXY) || v(process.env.ALL_PROXY) || v(process.env.https_proxy) || v(process.env.http_proxy)) +} + +function tryPowerShellDownload(url, dest) { + const u = url.replace(/'/g, "''") + const d = dest.replace(/'/g, "''") + const cmd = `Invoke-WebRequest -Uri '${u}' -OutFile '${d}' -UseBasicParsing` + const result = spawnSync( + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", cmd], + { stdio: "pipe", windowsHide: true }, + ) + return result.status === 0 && existsSync(dest) && statSync(dest).size > 0 +} + +function tryCurlDownload(url, dest) { + const curl = process.platform === "win32" ? "curl.exe" : "curl" + const result = spawnSync(curl, ["-fsSL", "-L", "--fail", "-o", dest, url], { + stdio: "pipe", + windowsHide: true, + }) + return result.status === 0 && existsSync(dest) && statSync(dest).size > 0 +} + +async function fetchRelease(url) { + if (proxyEnvSet()) { + // Dynamic require so it works in node without bundling issues + const undici = require("undici") + return await undici.fetch(url, { + redirect: "follow", + dispatcher: new undici.EnvHttpProxyAgent(), + }) + } + // Node 18+ has global fetch, Bun has it too + return await fetch(url, { redirect: "follow" }) +} + +async function downloadUrlToBuffer(url) { + const response = await fetchRelease(url) + if (!response.ok) { + throw new Error(`Download failed: ${response.status} ${response.statusText}`) + } + return Buffer.from(await response.arrayBuffer()) +} + +async function downloadUrlToBufferWithFallback(url) { + let firstError + try { + return await downloadUrlToBuffer(url) + } catch (e) { + firstError = e + } + + const tmpRoot = path.join(os.tmpdir(), `ripgrep-dl-${process.pid}-${Date.now()}`) + const tmpFile = path.join(tmpRoot, "archive") + mkdirSync(tmpRoot, { recursive: true }) + try { + if (process.platform === "win32" && tryPowerShellDownload(url, tmpFile)) { + return readFileSync(tmpFile) + } + if (tryCurlDownload(url, tmpFile)) { + return readFileSync(tmpFile) + } + } finally { + rmSync(tmpRoot, { recursive: true, force: true }) + } + + throw firstError +} + +// --- Extract --- + +function findZipEntryKey(files, want) { + return Object.keys(files).find((k) => { + const norm = k.replace(/\\/g, "/") + return norm === want || norm.endsWith(`/${want}`) + }) +} + +async function extractZip(buffer, binaryPath, extractedBinary) { + const binaryDir = path.dirname(binaryPath) + // Try fflate first (bundled dep) + let fflateError + try { + const { unzipSync } = require("fflate") + const unzipped = unzipSync(new Uint8Array(buffer)) + const key = findZipEntryKey(unzipped, extractedBinary) + if (!key) { + throw new Error(`Binary ${extractedBinary} not found in zip`) + } + writeFileSync(binaryPath, Buffer.from(unzipped[key])) + return + } catch (e) { + fflateError = e + } + + // Fallback: PowerShell Expand-Archive or unzip CLI + const tmpDir = path.join(binaryDir, ".tmp-download") + rmSync(tmpDir, { recursive: true, force: true }) + mkdirSync(tmpDir, { recursive: true }) + try { + const assetName = `archive.zip` + const archivePath = path.join(tmpDir, assetName) + writeFileSync(archivePath, buffer) + + let extracted = false + if (process.platform === "win32") { + const psCmd = `Expand-Archive -Path '${archivePath.replace(/'/g, "''")}' -DestinationPath '${tmpDir.replace(/'/g, "''")}' -Force` + const psResult = spawnSync( + "powershell.exe", + ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", psCmd], + { stdio: "pipe", windowsHide: true }, + ) + if (psResult.status === 0) { + extracted = true + } + } + + if (!extracted) { + const result = spawnSync("unzip", ["-o", archivePath, "-d", tmpDir], { stdio: "pipe" }) + if (result.status !== 0) { + const unzipErr = result.stderr?.toString().trim() || "command not found" + const fflateMsg = fflateError instanceof Error ? fflateError.message : String(fflateError) + throw new Error(`zip extraction failed (fflate: ${fflateMsg}; unzip: ${unzipErr})`) + } + } + + const srcBinary = path.join(tmpDir, extractedBinary) + if (!existsSync(srcBinary)) { + throw new Error(`Binary not found at expected path: ${srcBinary}`) + } + renameSync(srcBinary, binaryPath) + } finally { + rmSync(tmpDir, { recursive: true, force: true }) + } +} + +async function extractTarGz(buffer, binaryPath, extractedBinary, assetName) { + const binaryDir = path.dirname(binaryPath) + const tmpDir = path.join(binaryDir, ".tmp-download") + rmSync(tmpDir, { recursive: true, force: true }) + mkdirSync(tmpDir, { recursive: true }) + try { + const archivePath = path.join(tmpDir, assetName) + writeFileSync(archivePath, buffer) + const result = spawnSync("tar", ["xzf", archivePath, "-C", tmpDir], { stdio: "pipe" }) + if (result.status !== 0) { + throw new Error(`tar extract failed: ${result.stderr?.toString()}`) + } + const srcBinary = path.join(tmpDir, extractedBinary) + if (!existsSync(srcBinary)) { + throw new Error(`Binary not found at expected path: ${srcBinary}`) + } + renameSync(srcBinary, binaryPath) + } finally { + rmSync(tmpDir, { recursive: true, force: true }) + } +} + +// --- Main --- + +async function downloadAndExtract() { + const { target, ext } = getPlatformMapping() + const assetName = `ripgrep-v${RG_VERSION}-${target}.${ext}` + const downloadUrl = `${RELEASE_BASE}/${assetName}` + + const binaryPath = getBinaryPath() + const binaryDir = path.dirname(binaryPath) + + const force = process.argv.includes("--force") + if (!force && existsSync(binaryPath)) { + const stat = statSync(binaryPath) + if (stat.size > 0) { + console.log(`[ripgrep] Binary already exists at ${binaryPath}, skipping.`) + return + } + } + + console.log(`[ripgrep] Downloading v${RG_VERSION} for ${target}...`) + console.log(`[ripgrep] URL: ${downloadUrl}`) + + const extractedBinary = process.platform === "win32" ? "rg.exe" : "rg" + + try { + const buffer = await downloadUrlToBufferWithFallback(downloadUrl) + console.log(`[ripgrep] Downloaded ${Math.round(buffer.length / 1024)} KB`) + + mkdirSync(binaryDir, { recursive: true }) + + if (ext === "tar.gz") { + await extractTarGz(buffer, binaryPath, extractedBinary, assetName) + } else { + await extractZip(buffer, binaryPath, extractedBinary) + } + + if (process.platform !== "win32") { + chmodSync(binaryPath, 0o755) + } + + console.log(`[ripgrep] Installed to ${binaryPath}`) + } catch (e) { + const msg = e instanceof Error ? e.message : String(e) + const hint = + "Check network or set HTTPS_PROXY. If GitHub is blocked, set RIPGREP_DOWNLOAD_BASE to a mirror (see script header)." + throw new Error(`${msg} ${hint}`) + } +} + +async function main() { + await downloadAndExtract() +} + +main().catch((error) => { + const msg = error instanceof Error ? error.message : String(error) + console.error(`[postinstall] ripgrep download failed (non-fatal): ${msg}`) + console.error(`[postinstall] You can install ripgrep manually: https://github.com/BurntSushi/ripgrep#installation`) + // Never exit with error code — postinstall must not break install + process.exit(0) +}) From eca1acc66247437c61858c7054d383ec66d955f3 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 6 Apr 2026 10:48:27 +0800 Subject: [PATCH 4/7] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=20openai=20?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../gemini/__tests__/convertMessages.test.ts | 65 +++++++++++++ src/services/api/gemini/convertMessages.ts | 20 ++++ src/services/api/gemini/types.ts | 6 ++ .../openai/__tests__/convertMessages.test.ts | 94 +++++++++++++++++++ src/services/api/openai/convertMessages.ts | 55 ++++++++++- 5 files changed, 238 insertions(+), 2 deletions(-) diff --git a/src/services/api/gemini/__tests__/convertMessages.test.ts b/src/services/api/gemini/__tests__/convertMessages.test.ts index 11d49ca37..63a9cf60a 100644 --- a/src/services/api/gemini/__tests__/convertMessages.test.ts +++ b/src/services/api/gemini/__tests__/convertMessages.test.ts @@ -199,4 +199,69 @@ describe('anthropicMessagesToGemini', () => { }, ]) }) + + test('converts base64 image to inlineData', () => { + const result = anthropicMessagesToGemini( + [makeUserMsg([ + { type: 'text', text: 'describe this' }, + { + type: 'image', + source: { + type: 'base64', + media_type: 'image/png', + data: 'iVBORw0KGgo=', + }, + }, + ])], + [] as any, + ) + expect(result.contents).toEqual([ + { + role: 'user', + parts: [ + { text: 'describe this' }, + { inlineData: { mimeType: 'image/png', data: 'iVBORw0KGgo=' } }, + ], + }, + ]) + }) + + test('converts url image to text fallback', () => { + const result = anthropicMessagesToGemini( + [makeUserMsg([ + { + type: 'image', + source: { + type: 'url', + url: 'https://example.com/img.png', + }, + }, + ])], + [] as any, + ) + expect(result.contents).toEqual([ + { + role: 'user', + parts: [{ text: '[image: https://example.com/img.png]' }], + }, + ]) + }) + + test('defaults to image/png when media_type is missing', () => { + const result = anthropicMessagesToGemini( + [makeUserMsg([ + { + type: 'image', + source: { + type: 'base64', + data: 'ABC123', + }, + }, + ])], + [] as any, + ) + expect(result.contents[0].parts[0]).toEqual({ + inlineData: { mimeType: 'image/png', data: 'ABC123' }, + }) + }) }) diff --git a/src/services/api/gemini/convertMessages.ts b/src/services/api/gemini/convertMessages.ts index 4ac3a209d..018efb1b6 100644 --- a/src/services/api/gemini/convertMessages.ts +++ b/src/services/api/gemini/convertMessages.ts @@ -113,6 +113,26 @@ function convertUserContentBlockToGeminiParts( ] } + // 将 Anthropic image 块转换为 Gemini inlineData + if (block.type === 'image') { + const source = block.source as Record | undefined + if (source?.type === 'base64' && typeof source.data === 'string') { + const mediaType = (source.media_type as string) || 'image/png' + return [ + { + inlineData: { + mimeType: mediaType, + data: source.data, + }, + }, + ] + } + // url 类型的图片,Gemini 不直接支持,转为文本描述 + if (source?.type === 'url' && typeof source.url === 'string') { + return createTextGeminiParts(`[image: ${source.url}]`) + } + } + return [] } diff --git a/src/services/api/gemini/types.ts b/src/services/api/gemini/types.ts index 829a09f13..e8718fecd 100644 --- a/src/services/api/gemini/types.ts +++ b/src/services/api/gemini/types.ts @@ -10,12 +10,18 @@ export type GeminiFunctionResponse = { response?: Record } +export type GeminiInlineData = { + mimeType: string + data: string +} + export type GeminiPart = { text?: string thought?: boolean thoughtSignature?: string functionCall?: GeminiFunctionCall functionResponse?: GeminiFunctionResponse + inlineData?: GeminiInlineData } export type GeminiContent = { diff --git a/src/services/api/openai/__tests__/convertMessages.test.ts b/src/services/api/openai/__tests__/convertMessages.test.ts index 0e69f1ca8..0ea52757b 100644 --- a/src/services/api/openai/__tests__/convertMessages.test.ts +++ b/src/services/api/openai/__tests__/convertMessages.test.ts @@ -154,4 +154,98 @@ describe('anthropicMessagesToOpenAI', () => { expect((result[2] as any).tool_calls).toBeDefined() expect(result[3].role).toBe('tool') }) + + test('converts base64 image to image_url', () => { + const result = anthropicMessagesToOpenAI( + [makeUserMsg([ + { type: 'text', text: 'what is this?' }, + { + type: 'image' as const, + source: { + type: 'base64', + media_type: 'image/png', + data: 'iVBORw0KGgo=', + }, + }, + ])], + [] as any, + ) + expect(result).toEqual([{ + role: 'user', + content: [ + { type: 'text', text: 'what is this?' }, + { + type: 'image_url', + image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' }, + }, + ], + }]) + }) + + test('converts url image to image_url', () => { + const result = anthropicMessagesToOpenAI( + [makeUserMsg([ + { + type: 'image' as const, + source: { + type: 'url', + url: 'https://example.com/img.png', + }, + }, + ])], + [] as any, + ) + expect(result).toEqual([{ + role: 'user', + content: [ + { + type: 'image_url', + image_url: { url: 'https://example.com/img.png' }, + }, + ], + }]) + }) + + test('converts image-only message without text', () => { + const result = anthropicMessagesToOpenAI( + [makeUserMsg([ + { + type: 'image' as const, + source: { + type: 'base64', + media_type: 'image/jpeg', + data: '/9j/4AAQ', + }, + }, + ])], + [] as any, + ) + expect(result).toEqual([{ + role: 'user', + content: [ + { + type: 'image_url', + image_url: { url: 'data:image/jpeg;base64,/9j/4AAQ' }, + }, + ], + }]) + }) + + test('defaults to image/png when media_type is missing', () => { + const result = anthropicMessagesToOpenAI( + [makeUserMsg([ + { + type: 'image' as const, + source: { + type: 'base64', + data: 'ABC123', + }, + }, + ])], + [] as any, + ) + expect((result[0].content as any[])[0].image_url.url).toBe( + 'data:image/png;base64,ABC123', + ) + }) }) diff --git a/src/services/api/openai/convertMessages.ts b/src/services/api/openai/convertMessages.ts index 63fe6c719..051b43d69 100644 --- a/src/services/api/openai/convertMessages.ts +++ b/src/services/api/openai/convertMessages.ts @@ -75,6 +75,7 @@ function convertInternalUserMessage( } else if (Array.isArray(content)) { const textParts: string[] = [] const toolResults: BetaToolResultBlockParam[] = [] + const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> = [] for (const block of content) { if (typeof block === 'string') { @@ -83,11 +84,26 @@ function convertInternalUserMessage( textParts.push(block.text) } else if (block.type === 'tool_result') { toolResults.push(block as BetaToolResultBlockParam) + } else if (block.type === 'image') { + const imagePart = convertImageBlockToOpenAI(block as Record) + if (imagePart) { + imageParts.push(imagePart) + } } - // Skip image, document, thinking, cache_edits, etc. } - if (textParts.length > 0) { + // 如果有图片,构建多模态 content 数组 + if (imageParts.length > 0) { + const multiContent: Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }> = [] + if (textParts.length > 0) { + multiContent.push({ type: 'text', text: textParts.join('\n') }) + } + multiContent.push(...imageParts) + result.push({ + role: 'user', + content: multiContent, + } satisfies ChatCompletionUserMessageParam) + } else if (textParts.length > 0) { result.push({ role: 'user', content: textParts.join('\n'), @@ -182,3 +198,38 @@ function convertInternalAssistantMessage( return [result] } + +/** + * 将 Anthropic image 块转换为 OpenAI image_url 格式。 + * + * Anthropic 格式: { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } } + * OpenAI 格式: { type: "image_url", image_url: { url: "data:image/png;base64,..." } } + */ +function convertImageBlockToOpenAI( + block: Record, +): { type: 'image_url'; image_url: { url: string } } | null { + const source = block.source as Record | undefined + if (!source) return null + + if (source.type === 'base64' && typeof source.data === 'string') { + const mediaType = (source.media_type as string) || 'image/png' + return { + type: 'image_url', + image_url: { + url: `data:${mediaType};base64,${source.data}`, + }, + } + } + + // url 类型的图片直接传递 + if (source.type === 'url' && typeof source.url === 'string') { + return { + type: 'image_url', + image_url: { + url: source.url, + }, + } + } + + return null +} From 258cc720f4f0b3de68c38ea22226100a418b774f Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 6 Apr 2026 11:04:13 +0800 Subject: [PATCH 5/7] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=E7=95=99?= =?UTF-8?q?=E5=BD=B1=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Friends.md | 38 ++++++++++++++++++++++++++++++++++++++ README.md | 5 ++--- 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 Friends.md diff --git a/Friends.md b/Friends.md new file mode 100644 index 000000000..612ff65e3 --- /dev/null +++ b/Friends.md @@ -0,0 +1,38 @@ +# 社区项目 & Blog 合集 + +> 每日更新,欢迎自荐! + +## 工具 & 应用 + +| 项目 | 描述 | 作者 | +|------|------|------| +| [4qtask.vercel.app](https://4qtask.vercel.app/) | 免费四象限时间管理工具 | @kevinhuky | +| [kaying.studio](https://kaying.studio/) | 个人 AI 工具箱 | @kayingai | +| [supsub.ai](https://supsub.ai/) | 高效阅读工具 | @hidumou | +| [x-video-download.net](https://x-video-download.net/) | 视频下载工具 | @syakadou | +| [1openapi.com](https://1openapi.com/) | API 中转站 | @thinker007 | +| [claw-z.com](https://claw-z.com/) | 一键部署 OpenClaw AI Agent(场景驱动、全面管理) | @uhhc | +| [gemini-watermark-remover.net](https://gemini-watermark-remover.net/) | Gemini 水印移除工具 | @syakadou | + +## GitHub 开源项目 + +| 项目 | 描述 | 作者 | +|------|------|------| +| [VersperClaw](https://github.com/versperai/VersperClaw) | 全自动科研流 | @versperai | +| [claude-reviews-claude](https://github.com/openedclaude/claude-reviews-claude) | 原汤化原食——Claude 如何看待眼中的老己 | @openedclaude | +| [agentica](https://github.com/shibing624/agentica) | 自研 Agent 框架,借鉴 claude-code 多 Agent 处理 | @shibing624 | +| [macman](https://github.com/tonngw/macman) | Mac 从 0 到 1 保姆级配置教程 | @tonngw | +| [SuperSpec](https://github.com/asasugar/SuperSpec) | SDD / Spec-Driven Development | @asasugar | +| [adnify](https://github.com/adnaan-worker/adnify) | 高颜值高定制化 AI 编辑器 | @adnaan-worker | +| [another-rule-engine](https://github.com/eatmoreduck/another-rule-engine) | 基于 Groovy 的开源多功能决策引擎 | @eatmoreduck | +| [creative_master](https://github.com/chatabc/creative_master) | AI 驱动的创意灵感管理工具 | @chatabc | +| [RapidDoc](https://github.com/RapidAI/RapidDoc) | Office 文件解析工具转 Markdown(支持 PDF/Image/Word/PPT/Excel) | @hzkitt | +| [token-share](https://github.com/leemysw/token-share) | macOS 原生菜单栏 LLM API 网关,支持 OpenAI 与 Anthropic 协议间的实时互译与流式转发 | @leemysw | +| [feishu-docx](https://github.com/leemysw/feishu-docx) | 飞书知识库导出、写入与云空间管理工具(支持 Markdown、公众号导入、CLI、TUI) | @leemysw | +| [web-search-fast](https://github.com/uk0/web-search-fast) | 快速网页搜索 | @uk0 | + +## Blog + +| 链接 | 作者 | +|------|------| +| [blog.xiaohuangyu.space](https://blog.xiaohuangyu.space/) | @eatmoreduck | diff --git a/README.md b/README.md index 3df85fe3d..995465772 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,7 @@ 牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)... -[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) - -[Discord 群组](https://discord.gg/qZU6zS7Q) +[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q) - [x] V4 — 测试补全、[Buddy](https://ccb.agent-aura.top/docs/features/buddy)、[Auto Mode](https://ccb.agent-aura.top/docs/safety/auto-mode)、环境变量 Feature 开关 - [x] V5 — [Sentry](https://ccb.agent-aura.top/docs/internals/sentry-setup) / [GrowthBook](https://ccb.agent-aura.top/docs/internals/growthbook-adapter) 企业监控、[自定义 Login](https://ccb.agent-aura.top/docs/features/custom-platform-login)、[OpenAI 兼容](https://ccb.agent-aura.top/docs/plans/openai-compatibility)、[Web Search](https://ccb.agent-aura.top/docs/features/web-browser-tool)、[Computer Use](https://ccb.agent-aura.top/docs/features/computer-use) / [Chrome Use](https://ccb.agent-aura.top/docs/features/claude-in-chrome-mcp)、[Voice Mode](https://ccb.agent-aura.top/docs/features/voice-mode)、[Bridge Mode](https://ccb.agent-aura.top/docs/features/bridge-mode)、[/dream 记忆整理](https://ccb.agent-aura.top/docs/features/auto-dream) @@ -77,6 +75,7 @@ bun run build ### 新人配置 /login 首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Anthropic Compatible** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。 +选择 OpenAI 和 Gemini 对应的栏目都是支持相应协议的 需要填写的字段: From 3923af4834cab1930c62fba0c1fac8037ba8c2f7 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 6 Apr 2026 11:23:49 +0800 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20login=20?= =?UTF-8?q?=E9=9D=A2=E6=9D=BF=E7=9A=84=E5=B7=A6=E5=8F=B3=E5=88=87=E6=8D=A2?= =?UTF-8?q?=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ConsoleOAuthFlow.tsx | 18 +++++++++--------- src/keybindings/defaultBindings.ts | 10 ++++++++++ 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 44c69025f..90a2e2b36 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -709,7 +709,7 @@ function OAuthStatusMessage({ setInputCursorOffset((displayValues[FIELDS[idx + 1]!] ?? '').length) } }, - { context: 'Tabs' }, + { context: 'FormField' }, ) useKeybinding( 'tabs:previous', @@ -721,7 +721,7 @@ function OAuthStatusMessage({ setInputCursorOffset((displayValues[FIELDS[idx - 1]!] ?? '').length) } }, - { context: 'Tabs' }, + { context: 'FormField' }, ) useKeybinding( 'confirm:no', @@ -782,7 +782,7 @@ function OAuthStatusMessage({ {renderRow('opus_model', 'Opus ')} - Tab to switch · Enter on last field to save · Esc to go back + ↑↓/Tab to switch · Enter on last field to save · Esc to go back ) @@ -916,7 +916,7 @@ function OAuthStatusMessage({ ) } }, - { context: 'Tabs' }, + { context: 'FormField' }, ) useKeybinding( 'tabs:previous', @@ -932,7 +932,7 @@ function OAuthStatusMessage({ ) } }, - { context: 'Tabs' }, + { context: 'FormField' }, ) useKeybinding( 'confirm:no', @@ -997,7 +997,7 @@ function OAuthStatusMessage({ {renderOpenAIRow('opus_model', 'Opus ')} - Tab to switch · Enter on last field to save · Esc to go back + ↑↓/Tab to switch · Enter on last field to save · Esc to go back ) @@ -1148,7 +1148,7 @@ function OAuthStatusMessage({ ) } }, - { context: 'Tabs' }, + { context: 'FormField' }, ) useKeybinding( 'tabs:previous', @@ -1164,7 +1164,7 @@ function OAuthStatusMessage({ ) } }, - { context: 'Tabs' }, + { context: 'FormField' }, ) useKeybinding( 'confirm:no', @@ -1229,7 +1229,7 @@ function OAuthStatusMessage({ {renderGeminiRow('opus_model', 'Opus ')} - Tab to switch · Enter on last field to save · Esc to go back + ↑↓/Tab to switch · Enter on last field to save · Esc to go back ) diff --git a/src/keybindings/defaultBindings.ts b/src/keybindings/defaultBindings.ts index 8629809d9..f33e764ac 100644 --- a/src/keybindings/defaultBindings.ts +++ b/src/keybindings/defaultBindings.ts @@ -147,6 +147,16 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [ 'ctrl+d': 'permission:toggleDebug', }, }, + { + context: 'FormField', + bindings: { + // Form field vertical navigation (login/setup panels) + tab: 'tabs:next', + 'shift+tab': 'tabs:previous', + up: 'tabs:previous', + down: 'tabs:next', + }, + }, { context: 'Tabs', bindings: { From 919011a37290db4cefeceb2978821acaf34e35e6 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Mon, 6 Apr 2026 11:28:01 +0800 Subject: [PATCH 7/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20login=20?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E7=9A=84=20enter=20=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ConsoleOAuthFlow.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 90a2e2b36..a9782fa5c 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -689,11 +689,12 @@ function OAuthStatusMessage({ const handleEnter = useCallback(() => { const idx = FIELDS.indexOf(activeField) - setOAuthStatus(buildState(activeField, inputValue)) if (idx === FIELDS.length - 1) { + setOAuthStatus(buildState(activeField, inputValue)) doSave() } else { const next = FIELDS[idx + 1]! + setOAuthStatus(buildState(activeField, inputValue, next)) setInputValue(displayValues[next] ?? '') setInputCursorOffset((displayValues[next] ?? '').length) } @@ -885,11 +886,12 @@ function OAuthStatusMessage({ const handleOpenAIEnter = useCallback(() => { const idx = OPENAI_FIELDS.indexOf(activeField) - setOAuthStatus(buildOpenAIState(activeField, openaiInputValue)) if (idx === OPENAI_FIELDS.length - 1) { + setOAuthStatus(buildOpenAIState(activeField, openaiInputValue)) doOpenAISave() } else { const next = OPENAI_FIELDS[idx + 1]! + setOAuthStatus(buildOpenAIState(activeField, openaiInputValue, next)) setOpenaiInputValue(openaiDisplayValues[next] ?? '') setOpenaiInputCursorOffset((openaiDisplayValues[next] ?? '').length) } @@ -1117,11 +1119,12 @@ function OAuthStatusMessage({ const handleGeminiEnter = useCallback(() => { const idx = GEMINI_FIELDS.indexOf(activeField) - setOAuthStatus(buildGeminiState(activeField, geminiInputValue)) if (idx === GEMINI_FIELDS.length - 1) { + setOAuthStatus(buildGeminiState(activeField, geminiInputValue)) doGeminiSave() } else { const next = GEMINI_FIELDS[idx + 1]! + setOAuthStatus(buildGeminiState(activeField, geminiInputValue, next)) setGeminiInputValue(geminiDisplayValues[next] ?? '') setGeminiInputCursorOffset((geminiDisplayValues[next] ?? '').length) }