mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
Merge branch 'claude-code-best:main' into main
This commit is contained in:
38
Friends.md
Normal file
38
Friends.md
Normal file
@@ -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 |
|
||||||
@@ -12,9 +12,7 @@
|
|||||||
|
|
||||||
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)...
|
牢 A (Anthropic) 官方 [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI 工具的源码反编译/逆向还原项目。目标是将 Claude Code 大部分功能及工程化能力复现 (问就是老佛爷已经付过钱了)。虽然很难绷, 但是它叫做 CCB(踩踩背)...
|
||||||
|
|
||||||
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/)
|
[文档在这里, 支持投稿 PR](https://ccb.agent-aura.top/) | [留影文档在这里](./Friends.md) | [Discord 群组](https://discord.gg/qZU6zS7Q)
|
||||||
|
|
||||||
[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] 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)
|
- [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
|
### 新人配置 /login
|
||||||
|
|
||||||
首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Anthropic Compatible** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。
|
首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Anthropic Compatible** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。
|
||||||
|
选择 OpenAI 和 Gemini 对应的栏目都是支持相应协议的
|
||||||
|
|
||||||
需要填写的字段:
|
需要填写的字段:
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"dist",
|
||||||
"scripts/download-ripgrep.ts"
|
"scripts/download-ripgrep.ts",
|
||||||
|
"scripts/postinstall.cjs"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun run build.ts",
|
"build": "bun run build.ts",
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"check:unused": "knip-bun",
|
"check:unused": "knip-bun",
|
||||||
"health": "bun run scripts/health-check.ts",
|
"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"
|
"docs:dev": "npx mintlify dev"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
|
|||||||
319
scripts/postinstall.cjs
Normal file
319
scripts/postinstall.cjs
Normal file
@@ -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)
|
||||||
|
})
|
||||||
@@ -706,11 +706,12 @@ function OAuthStatusMessage({
|
|||||||
|
|
||||||
const handleEnter = useCallback(() => {
|
const handleEnter = useCallback(() => {
|
||||||
const idx = FIELDS.indexOf(activeField)
|
const idx = FIELDS.indexOf(activeField)
|
||||||
setOAuthStatus(buildState(activeField, inputValue))
|
|
||||||
if (idx === FIELDS.length - 1) {
|
if (idx === FIELDS.length - 1) {
|
||||||
|
setOAuthStatus(buildState(activeField, inputValue))
|
||||||
doSave()
|
doSave()
|
||||||
} else {
|
} else {
|
||||||
const next = FIELDS[idx + 1]!
|
const next = FIELDS[idx + 1]!
|
||||||
|
setOAuthStatus(buildState(activeField, inputValue, next))
|
||||||
setInputValue(displayValues[next] ?? '')
|
setInputValue(displayValues[next] ?? '')
|
||||||
setInputCursorOffset((displayValues[next] ?? '').length)
|
setInputCursorOffset((displayValues[next] ?? '').length)
|
||||||
}
|
}
|
||||||
@@ -726,7 +727,7 @@ function OAuthStatusMessage({
|
|||||||
setInputCursorOffset((displayValues[FIELDS[idx + 1]!] ?? '').length)
|
setInputCursorOffset((displayValues[FIELDS[idx + 1]!] ?? '').length)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ context: 'Tabs' },
|
{ context: 'FormField' },
|
||||||
)
|
)
|
||||||
useKeybinding(
|
useKeybinding(
|
||||||
'tabs:previous',
|
'tabs:previous',
|
||||||
@@ -738,7 +739,7 @@ function OAuthStatusMessage({
|
|||||||
setInputCursorOffset((displayValues[FIELDS[idx - 1]!] ?? '').length)
|
setInputCursorOffset((displayValues[FIELDS[idx - 1]!] ?? '').length)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ context: 'Tabs' },
|
{ context: 'FormField' },
|
||||||
)
|
)
|
||||||
useKeybinding(
|
useKeybinding(
|
||||||
'confirm:no',
|
'confirm:no',
|
||||||
@@ -799,7 +800,7 @@ function OAuthStatusMessage({
|
|||||||
{renderRow('opus_model', 'Opus ')}
|
{renderRow('opus_model', 'Opus ')}
|
||||||
</Box>
|
</Box>
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
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
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
@@ -925,11 +926,12 @@ function OAuthStatusMessage({
|
|||||||
|
|
||||||
const handleOpenAIEnter = useCallback(() => {
|
const handleOpenAIEnter = useCallback(() => {
|
||||||
const idx = OPENAI_FIELDS.indexOf(activeField)
|
const idx = OPENAI_FIELDS.indexOf(activeField)
|
||||||
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue))
|
|
||||||
if (idx === OPENAI_FIELDS.length - 1) {
|
if (idx === OPENAI_FIELDS.length - 1) {
|
||||||
|
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue))
|
||||||
doOpenAISave()
|
doOpenAISave()
|
||||||
} else {
|
} else {
|
||||||
const next = OPENAI_FIELDS[idx + 1]!
|
const next = OPENAI_FIELDS[idx + 1]!
|
||||||
|
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue, next))
|
||||||
setOpenaiInputValue(openaiDisplayValues[next] ?? '')
|
setOpenaiInputValue(openaiDisplayValues[next] ?? '')
|
||||||
setOpenaiInputCursorOffset((openaiDisplayValues[next] ?? '').length)
|
setOpenaiInputCursorOffset((openaiDisplayValues[next] ?? '').length)
|
||||||
}
|
}
|
||||||
@@ -956,7 +958,7 @@ function OAuthStatusMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ context: 'Tabs' },
|
{ context: 'FormField' },
|
||||||
)
|
)
|
||||||
useKeybinding(
|
useKeybinding(
|
||||||
'tabs:previous',
|
'tabs:previous',
|
||||||
@@ -972,7 +974,7 @@ function OAuthStatusMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ context: 'Tabs' },
|
{ context: 'FormField' },
|
||||||
)
|
)
|
||||||
useKeybinding(
|
useKeybinding(
|
||||||
'confirm:no',
|
'confirm:no',
|
||||||
@@ -1037,7 +1039,7 @@ function OAuthStatusMessage({
|
|||||||
{renderOpenAIRow('opus_model', 'Opus ')}
|
{renderOpenAIRow('opus_model', 'Opus ')}
|
||||||
</Box>
|
</Box>
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
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
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
@@ -1157,11 +1159,12 @@ function OAuthStatusMessage({
|
|||||||
|
|
||||||
const handleGeminiEnter = useCallback(() => {
|
const handleGeminiEnter = useCallback(() => {
|
||||||
const idx = GEMINI_FIELDS.indexOf(activeField)
|
const idx = GEMINI_FIELDS.indexOf(activeField)
|
||||||
setOAuthStatus(buildGeminiState(activeField, geminiInputValue))
|
|
||||||
if (idx === GEMINI_FIELDS.length - 1) {
|
if (idx === GEMINI_FIELDS.length - 1) {
|
||||||
|
setOAuthStatus(buildGeminiState(activeField, geminiInputValue))
|
||||||
doGeminiSave()
|
doGeminiSave()
|
||||||
} else {
|
} else {
|
||||||
const next = GEMINI_FIELDS[idx + 1]!
|
const next = GEMINI_FIELDS[idx + 1]!
|
||||||
|
setOAuthStatus(buildGeminiState(activeField, geminiInputValue, next))
|
||||||
setGeminiInputValue(geminiDisplayValues[next] ?? '')
|
setGeminiInputValue(geminiDisplayValues[next] ?? '')
|
||||||
setGeminiInputCursorOffset((geminiDisplayValues[next] ?? '').length)
|
setGeminiInputCursorOffset((geminiDisplayValues[next] ?? '').length)
|
||||||
}
|
}
|
||||||
@@ -1188,7 +1191,7 @@ function OAuthStatusMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ context: 'Tabs' },
|
{ context: 'FormField' },
|
||||||
)
|
)
|
||||||
useKeybinding(
|
useKeybinding(
|
||||||
'tabs:previous',
|
'tabs:previous',
|
||||||
@@ -1204,7 +1207,7 @@ function OAuthStatusMessage({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ context: 'Tabs' },
|
{ context: 'FormField' },
|
||||||
)
|
)
|
||||||
useKeybinding(
|
useKeybinding(
|
||||||
'confirm:no',
|
'confirm:no',
|
||||||
@@ -1269,7 +1272,7 @@ function OAuthStatusMessage({
|
|||||||
{renderGeminiRow('opus_model', 'Opus ')}
|
{renderGeminiRow('opus_model', 'Opus ')}
|
||||||
</Box>
|
</Box>
|
||||||
<Text dimColor>
|
<Text dimColor>
|
||||||
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
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env bun
|
||||||
import { feature } from 'bun:bundle'
|
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
|
// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
|
||||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
||||||
process.env.COREPACK_ENABLE_AUTO_PIN = '0'
|
process.env.COREPACK_ENABLE_AUTO_PIN = '0'
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { isEnvTruthy } from 'src/utils/envUtils.js'
|
|||||||
import { useStartupNotification } from './useStartupNotification.js'
|
import { useStartupNotification } from './useStartupNotification.js'
|
||||||
|
|
||||||
const NPM_DEPRECATION_MESSAGE =
|
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 {
|
export function useNpmDeprecationNotification(): void {
|
||||||
useStartupNotification(async () => {
|
useStartupNotification(async () => {
|
||||||
|
|||||||
@@ -147,6 +147,16 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [
|
|||||||
'ctrl+d': 'permission:toggleDebug',
|
'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',
|
context: 'Tabs',
|
||||||
bindings: {
|
bindings: {
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -113,6 +113,26 @@ function convertUserContentBlockToGeminiParts(
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 将 Anthropic image 块转换为 Gemini inlineData
|
||||||
|
if (block.type === 'image') {
|
||||||
|
const source = block.source as Record<string, unknown> | 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 []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,18 @@ export type GeminiFunctionResponse = {
|
|||||||
response?: Record<string, unknown>
|
response?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GeminiInlineData = {
|
||||||
|
mimeType: string
|
||||||
|
data: string
|
||||||
|
}
|
||||||
|
|
||||||
export type GeminiPart = {
|
export type GeminiPart = {
|
||||||
text?: string
|
text?: string
|
||||||
thought?: boolean
|
thought?: boolean
|
||||||
thoughtSignature?: string
|
thoughtSignature?: string
|
||||||
functionCall?: GeminiFunctionCall
|
functionCall?: GeminiFunctionCall
|
||||||
functionResponse?: GeminiFunctionResponse
|
functionResponse?: GeminiFunctionResponse
|
||||||
|
inlineData?: GeminiInlineData
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GeminiContent = {
|
export type GeminiContent = {
|
||||||
|
|||||||
@@ -154,4 +154,98 @@ describe('anthropicMessagesToOpenAI', () => {
|
|||||||
expect((result[2] as any).tool_calls).toBeDefined()
|
expect((result[2] as any).tool_calls).toBeDefined()
|
||||||
expect(result[3].role).toBe('tool')
|
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',
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ function convertInternalUserMessage(
|
|||||||
} else if (Array.isArray(content)) {
|
} else if (Array.isArray(content)) {
|
||||||
const textParts: string[] = []
|
const textParts: string[] = []
|
||||||
const toolResults: BetaToolResultBlockParam[] = []
|
const toolResults: BetaToolResultBlockParam[] = []
|
||||||
|
const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> = []
|
||||||
|
|
||||||
for (const block of content) {
|
for (const block of content) {
|
||||||
if (typeof block === 'string') {
|
if (typeof block === 'string') {
|
||||||
@@ -83,11 +84,26 @@ function convertInternalUserMessage(
|
|||||||
textParts.push(block.text)
|
textParts.push(block.text)
|
||||||
} else if (block.type === 'tool_result') {
|
} else if (block.type === 'tool_result') {
|
||||||
toolResults.push(block as BetaToolResultBlockParam)
|
toolResults.push(block as BetaToolResultBlockParam)
|
||||||
|
} else if (block.type === 'image') {
|
||||||
|
const imagePart = convertImageBlockToOpenAI(block as Record<string, unknown>)
|
||||||
|
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({
|
result.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: textParts.join('\n'),
|
content: textParts.join('\n'),
|
||||||
@@ -182,3 +198,38 @@ function convertInternalAssistantMessage(
|
|||||||
|
|
||||||
return [result]
|
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<string, unknown>,
|
||||||
|
): { type: 'image_url'; image_url: { url: string } } | null {
|
||||||
|
const source = block.source as Record<string, unknown> | 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function isFullscreenEnvEnabled(): boolean {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return process.env.USER_TYPE === 'ant'
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user