mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
feat(desktop): inject CLAUDE_CODE_OAUTH_TOKEN from haha oauth.json
在 buildChildEnv 官方模式分支下,调 hahaOAuthService.ensureFreshAccessToken() (自动 refresh),成功则通过 env 注入给 CLI 子进程。 CLI 的 src/utils/auth.ts:1260 优先读 CLAUDE_CODE_OAUTH_TOKEN env,完全 不碰 Keychain —— DMG 装的 .app 在 macOS 下也能正常用官方 Claude,不再 403。 buildChildEnv 签名改为 async, 所有 caller 加 await。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
7421db2857
commit
94b445cd06
@ -10,6 +10,7 @@ describe('ConversationService', () => {
|
||||
let originalAuthToken: string | undefined
|
||||
let originalBaseUrl: string | undefined
|
||||
let originalModel: string | undefined
|
||||
let originalEntrypoint: string | undefined
|
||||
|
||||
beforeEach(async () => {
|
||||
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cc-haha-conversation-service-'))
|
||||
@ -17,11 +18,15 @@ describe('ConversationService', () => {
|
||||
originalAuthToken = process.env.ANTHROPIC_AUTH_TOKEN
|
||||
originalBaseUrl = process.env.ANTHROPIC_BASE_URL
|
||||
originalModel = process.env.ANTHROPIC_MODEL
|
||||
originalEntrypoint = process.env.CLAUDE_CODE_ENTRYPOINT
|
||||
|
||||
process.env.CLAUDE_CONFIG_DIR = tmpDir
|
||||
process.env.ANTHROPIC_AUTH_TOKEN = 'test-token'
|
||||
process.env.ANTHROPIC_BASE_URL = 'https://example.invalid/anthropic'
|
||||
process.env.ANTHROPIC_MODEL = 'test-model'
|
||||
// Clear inherited CLAUDE_CODE_ENTRYPOINT so tests can assert whether
|
||||
// buildChildEnv injects it or not without interference from the shell env.
|
||||
delete process.env.CLAUDE_CODE_ENTRYPOINT
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
@ -37,12 +42,15 @@ describe('ConversationService', () => {
|
||||
if (originalModel === undefined) delete process.env.ANTHROPIC_MODEL
|
||||
else process.env.ANTHROPIC_MODEL = originalModel
|
||||
|
||||
if (originalEntrypoint === undefined) delete process.env.CLAUDE_CODE_ENTRYPOINT
|
||||
else process.env.CLAUDE_CODE_ENTRYPOINT = originalEntrypoint
|
||||
|
||||
await fs.rm(tmpDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('keeps inherited provider env when no desktop provider config exists', () => {
|
||||
test('keeps inherited provider env when no desktop provider config exists', async () => {
|
||||
const service = new ConversationService() as any
|
||||
const env = service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha') as Record<string, string>
|
||||
const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha')) as Record<string, string>
|
||||
|
||||
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('test-token')
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('https://example.invalid/anthropic')
|
||||
@ -59,13 +67,63 @@ describe('ConversationService', () => {
|
||||
)
|
||||
|
||||
const service = new ConversationService() as any
|
||||
const env = service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha') as Record<string, string>
|
||||
const env = (await service.buildChildEnv('D:\\workspace\\code\\myself_code\\cc-haha')) as Record<string, string>
|
||||
|
||||
expect(env.ANTHROPIC_AUTH_TOKEN).toBeUndefined()
|
||||
expect(env.ANTHROPIC_BASE_URL).toBeUndefined()
|
||||
expect(env.ANTHROPIC_MODEL).toBeUndefined()
|
||||
})
|
||||
|
||||
test('buildChildEnv injects CLAUDE_CODE_OAUTH_TOKEN when official mode + haha oauth token exists', async () => {
|
||||
const ccHahaDir = path.join(tmpDir, 'cc-haha')
|
||||
await fs.mkdir(ccHahaDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(ccHahaDir, 'settings.json'),
|
||||
JSON.stringify({ env: {} }),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const { hahaOAuthService } = await import('../services/hahaOAuthService.js')
|
||||
await hahaOAuthService.saveTokens({
|
||||
accessToken: 'haha-fresh-token',
|
||||
refreshToken: 'haha-refresh-xxx',
|
||||
expiresAt: Date.now() + 30 * 60_000,
|
||||
scopes: ['user:inference'],
|
||||
subscriptionType: 'max',
|
||||
})
|
||||
|
||||
const service = new ConversationService() as any
|
||||
const env = (await service.buildChildEnv('/tmp')) as Record<string, string>
|
||||
|
||||
expect(env.CLAUDE_CODE_ENTRYPOINT).toBe('claude-desktop')
|
||||
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBe('haha-fresh-token')
|
||||
})
|
||||
|
||||
test('buildChildEnv does NOT inject CLAUDE_CODE_OAUTH_TOKEN when not official mode', async () => {
|
||||
const ccHahaDir = path.join(tmpDir, 'cc-haha')
|
||||
await fs.mkdir(ccHahaDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(ccHahaDir, 'settings.json'),
|
||||
JSON.stringify({ env: { ANTHROPIC_AUTH_TOKEN: 'custom-provider-token' } }),
|
||||
'utf-8',
|
||||
)
|
||||
|
||||
const { hahaOAuthService } = await import('../services/hahaOAuthService.js')
|
||||
await hahaOAuthService.saveTokens({
|
||||
accessToken: 'haha-token-should-not-be-used',
|
||||
refreshToken: null,
|
||||
expiresAt: null,
|
||||
scopes: [],
|
||||
subscriptionType: null,
|
||||
})
|
||||
|
||||
const service = new ConversationService() as any
|
||||
const env = (await service.buildChildEnv('/tmp')) as Record<string, string>
|
||||
|
||||
expect(env.CLAUDE_CODE_OAUTH_TOKEN).toBeUndefined()
|
||||
expect(env.CLAUDE_CODE_ENTRYPOINT).toBeUndefined()
|
||||
})
|
||||
|
||||
test('uses bun entrypoint fallback on Windows dev mode', () => {
|
||||
const service = new ConversationService() as any
|
||||
const args = service.resolveCliArgs(['--print'])
|
||||
|
||||
@ -119,7 +119,7 @@ export class ConversationService {
|
||||
// 工作目录就变成 `/`。把 CALLER_DIR / PWD 显式覆盖成 workDir,preload.ts
|
||||
// chdir 后落到正确目录。
|
||||
//
|
||||
const childEnv = this.buildChildEnv(workDir)
|
||||
const childEnv = await this.buildChildEnv(workDir)
|
||||
|
||||
let proc: ReturnType<typeof Bun.spawn>
|
||||
try {
|
||||
@ -471,7 +471,7 @@ export class ConversationService {
|
||||
return args
|
||||
}
|
||||
|
||||
private buildChildEnv(workDir: string): Record<string, string> {
|
||||
private async buildChildEnv(workDir: string): Promise<Record<string, string>> {
|
||||
// Provider isolation: when Desktop has its own provider config/index,
|
||||
// strip inherited provider env vars so the child CLI reads fresh values
|
||||
// from ~/.claude/cc-haha/settings.json instead of stale process.env.
|
||||
@ -511,11 +511,34 @@ export class ConversationService {
|
||||
// 否则 CLI 会忽略 provider 的 AUTH_TOKEN、错误地走 OAuth 打到第三方
|
||||
// endpoint。详见 src/utils/auth.ts isManagedOAuthContext()。
|
||||
...(this.shouldMarkManagedOAuth()
|
||||
? { CLAUDE_CODE_ENTRYPOINT: 'claude-desktop' }
|
||||
? await this.buildOfficialOAuthEnv()
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 官方模式下构造 CLI 子进程的 auth env:
|
||||
* - CLAUDE_CODE_ENTRYPOINT=claude-desktop 让 CLI 忽略外部残留 ANTHROPIC_* env
|
||||
* - 如果 haha 自管的 oauth.json 里有可用 token,注入 CLAUDE_CODE_OAUTH_TOKEN
|
||||
* 让 CLI 直接拿 env 里的 token,不碰 Keychain,绕开 macOS ACL 静默拒绝
|
||||
* (这是 DMG 安装 .app 后 403 "Request not allowed" 的唯一根治方案)
|
||||
*/
|
||||
private async buildOfficialOAuthEnv(): Promise<Record<string, string>> {
|
||||
const env: Record<string, string> = {
|
||||
CLAUDE_CODE_ENTRYPOINT: 'claude-desktop',
|
||||
}
|
||||
try {
|
||||
const { hahaOAuthService } = await import('./hahaOAuthService.js')
|
||||
const token = await hahaOAuthService.ensureFreshAccessToken()
|
||||
if (token) {
|
||||
env.CLAUDE_CODE_OAUTH_TOKEN = token
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[conversationService] ensureFreshAccessToken failed:', err)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
private shouldStripInheritedProviderEnv(): boolean {
|
||||
const configDir =
|
||||
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user