mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-07 15:12:18 +08:00
### What this PR does
Before this PR:
`claude-agent-sdk` is patched via a static pnpm `.patch` file that
replaces entire minified lines. This breaks every time the SDK is
upgraded because obfuscated variable names change with each
minification.
After this PR:
A Node.js postinstall script (`scripts/patch-claude-agent-sdk.mjs`) uses
semantic regex patterns to apply the same 3 patches. Since it matches
structural patterns (not variable names), it survives SDK version bumps
as long as the code structure remains the same.
### Why we need it and why it was done in this way
The following tradeoffs were made:
- Regex-based patching is slightly less precise than a static `.patch`
file, but far more resilient to minified code changes.
- The script validates all 3 patches applied and exits with error if
patterns don't match, so SDK structure changes are caught immediately.
The following alternatives were considered:
- Keeping the pnpm patch approach — rejected because it requires manual
regeneration on every SDK upgrade.
- Using AST-based patching — rejected as overkill for 3 targeted
replacements in minified code.
### Breaking changes
None. The same 3 modifications are applied (spawn→fork, remove command
destructuring, IPC stdio), just via a different mechanism.
### Special notes for your reviewer
The 3 patches applied by the script:
1. `import{spawn as X}` → `import{fork as X}` — enables IPC channel
2. Remove `command:VAR,` from `spawnLocalProcess` destructuring
3. Rewrite spawn call to `fork(args[0], args.slice(1), ...)` with IPC
stdio, removing `windowsHide:!0`
43 unit tests cover: variable name variations, idempotency, partial
matches, and no-match detection.
### Checklist
- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: You have left the code cleaner than you found it (Boy
Scout Rule)
- [ ] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [ ] Documentation: Not required — internal build tooling change
- [x] Self-review: I have reviewed my own code before requesting review
from others
### Release note
```release-note
NONE
```
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
/**
|
|
* Postinstall script to patch @anthropic-ai/claude-agent-sdk
|
|
*
|
|
* The SDK is shipped as minified/obfuscated code, so we use semantic regex
|
|
* patterns (not variable names) to apply patches. This is more resilient
|
|
* to SDK version bumps than a static .patch file.
|
|
*
|
|
* Changes:
|
|
* 1. spawn → fork (child_process import) — enables IPC channel
|
|
* 2. Remove `command` from spawnLocalProcess destructuring
|
|
* 3. Rewrite spawn call to use fork(args[0], args.slice(1), ...) with IPC stdio
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
import { createRequire } from 'node:module'
|
|
import path from 'node:path'
|
|
|
|
interface PatchResult {
|
|
result: string
|
|
matched: boolean
|
|
}
|
|
|
|
interface ApplyAllResult {
|
|
result: string
|
|
patchCount: number
|
|
alreadyPatched: boolean
|
|
}
|
|
|
|
// 1. Replace `import{spawn as X}from"child_process"` with `import{fork as X}from"child_process"`
|
|
export function patchSpawnImport(content: string): PatchResult {
|
|
let matched = false
|
|
const result = content.replace(/import\{spawn as ([\w$]+)\}from"child_process"/, (_, alias) => {
|
|
matched = true
|
|
return `import{fork as ${alias}}from"child_process"`
|
|
})
|
|
return { result, matched }
|
|
}
|
|
|
|
// 2. Remove `command:X,` from spawnLocalProcess destructuring
|
|
// Before: spawnLocalProcess(Q){let{command:X,args:Y,cwd:$,env:W,signal:J}=Q
|
|
// After: spawnLocalProcess(Q){let{args:Y,cwd:$,env:W,signal:J}=Q
|
|
export function patchRemoveCommand(content: string): PatchResult {
|
|
let matched = false
|
|
const result = content.replace(
|
|
/spawnLocalProcess\(([\w$]+)\)\{let\{command:([\w$]+),args:([\w$]+)/,
|
|
(_, fnArg, _cmd, args) => {
|
|
matched = true
|
|
return `spawnLocalProcess(${fnArg}){let{args:${args}`
|
|
}
|
|
)
|
|
return { result, matched }
|
|
}
|
|
|
|
// 3. Rewrite the spawn/fork call:
|
|
// Before: =Sq(X,Y,{cwd:$,stdio:["pipe","pipe",G],signal:J,env:W,windowsHide:!0})
|
|
// After: =Sq(Y[0],Y.slice(1),{cwd:$,stdio:G==="pipe"?["pipe","pipe","pipe","ipc"]:["pipe","pipe","ignore","ipc"],signal:J,env:W})
|
|
export function patchSpawnCall(content: string): PatchResult {
|
|
let matched = false
|
|
const result = content.replace(
|
|
/([\w$]+)\(([\w$]+),([\w$]+),\{cwd:([\w$]+),stdio:\["pipe","pipe",([\w$]+)\],signal:([\w$]+),env:([\w$]+),windowsHide:!0\}/,
|
|
(_, fn, _cmd, args, cwd, stderr, signal, env) => {
|
|
matched = true
|
|
return `${fn}(${args}[0],${args}.slice(1),{cwd:${cwd},stdio:${stderr}==="pipe"?["pipe","pipe","pipe","ipc"]:["pipe","pipe","ignore","ipc"],signal:${signal},env:${env}}`
|
|
}
|
|
)
|
|
return { result, matched }
|
|
}
|
|
|
|
// Apply all patches and return summary
|
|
export function applyAllPatches(content: string): ApplyAllResult {
|
|
let patchCount = 0
|
|
|
|
const p1 = patchSpawnImport(content)
|
|
content = p1.result
|
|
if (p1.matched) patchCount++
|
|
|
|
const p2 = patchRemoveCommand(content)
|
|
content = p2.result
|
|
if (p2.matched) patchCount++
|
|
|
|
const p3 = patchSpawnCall(content)
|
|
content = p3.result
|
|
if (p3.matched) patchCount++
|
|
|
|
const alreadyPatched = patchCount === 0 && content.includes('import{fork as') && content.includes('"ipc"')
|
|
|
|
return { result: content, patchCount, alreadyPatched }
|
|
}
|
|
|
|
// --- CLI entry point (skipped when imported by tests) ---
|
|
|
|
function main() {
|
|
const require_ = createRequire(import.meta.url)
|
|
|
|
let sdkPath: string
|
|
try {
|
|
sdkPath = path.join(path.dirname(require_.resolve('@anthropic-ai/claude-agent-sdk')), 'sdk.mjs')
|
|
} catch {
|
|
console.log('[patch-claude-agent-sdk] Package not installed, skipping.')
|
|
process.exit(0)
|
|
}
|
|
|
|
let fileContent: string
|
|
try {
|
|
fileContent = readFileSync(sdkPath, 'utf-8')
|
|
} catch {
|
|
console.error(`[patch-claude-agent-sdk] Failed to read ${sdkPath}`)
|
|
process.exit(1)
|
|
}
|
|
|
|
const { result, patchCount, alreadyPatched } = applyAllPatches(fileContent)
|
|
|
|
if (patchCount === 0) {
|
|
if (alreadyPatched) {
|
|
console.log('[patch-claude-agent-sdk] Already patched, skipping.')
|
|
process.exit(0)
|
|
}
|
|
console.error('[patch-claude-agent-sdk] No patterns matched! The SDK structure may have changed.')
|
|
process.exit(1)
|
|
}
|
|
|
|
if (patchCount < 3) {
|
|
console.warn(`[patch-claude-agent-sdk] Warning: only ${patchCount}/3 patches applied.`)
|
|
}
|
|
|
|
writeFileSync(sdkPath, result, 'utf-8')
|
|
console.log(`[patch-claude-agent-sdk] Successfully applied ${patchCount}/3 patches to sdk.mjs`)
|
|
}
|
|
|
|
if (!process.env.VITEST) {
|
|
main()
|
|
}
|