Skip to content

Commit f5b1b7c

Browse files
authored
feat: Codex OAuth 按需启动 + Custom Provider Claude 伪装配置 (#182)
1 parent 61f3cb1 commit f5b1b7c

File tree

12 files changed

+233
-27
lines changed

12 files changed

+233
-27
lines changed

cmd/maxx/main.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,11 +376,9 @@ func main() {
376376
Handler: loggedMux,
377377
}
378378

379-
// Start Codex OAuth callback server (listens on localhost:1455)
379+
// Initialize Codex OAuth callback server (start on-demand)
380380
codexOAuthServer := core.NewCodexOAuthServer(codexHandler)
381-
if err := codexOAuthServer.Start(context.Background()); err != nil {
382-
log.Printf("Warning: Failed to start Codex OAuth server: %v", err)
383-
}
381+
codexHandler.SetOAuthServer(codexOAuthServer)
384382

385383
// Start server in goroutine
386384
log.Printf("Starting Maxx server %s on %s", version.Info(), *addr)

internal/core/database.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ func InitializeServerComponents(
358358
kiroHandler := handler.NewKiroHandler(adminService)
359359
codexHandler := handler.NewCodexHandler(adminService, repos.CodexQuotaRepo, wailsBroadcaster)
360360
codexOAuthServer := NewCodexOAuthServer(codexHandler)
361+
codexHandler.SetOAuthServer(codexOAuthServer)
361362
projectProxyHandler := handler.NewProjectProxyHandler(proxyHandler, modelsHandler, repos.CachedProjectRepo)
362363

363364
log.Printf("[Core] Creating request tracker for graceful shutdown")

internal/core/server.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,6 @@ func (s *ManagedServer) Start(ctx context.Context) error {
135135
}
136136
}
137137

138-
// 启动 Codex OAuth 回调服务器
139-
if s.config.Components != nil && s.config.Components.CodexOAuthServer != nil {
140-
if err := s.config.Components.CodexOAuthServer.Start(s.ctx); err != nil {
141-
log.Printf("[Server] Failed to start Codex OAuth server: %v", err)
142-
}
143-
}
144-
145138
s.isRunning = true
146139
log.Printf("[Server] Server started successfully")
147140
return nil

internal/handler/codex.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ type CodexHandler struct {
2222
quotaRepo repository.CodexQuotaRepository
2323
oauthManager *codex.OAuthManager
2424
taskSvc *service.CodexTaskService
25+
oauthServer OAuthServer
26+
}
27+
28+
// OAuthServer is a minimal interface for the local OAuth callback server.
29+
type OAuthServer interface {
30+
Start(ctx context.Context) error
31+
Stop(ctx context.Context) error
32+
IsRunning() bool
2533
}
2634

2735
// NewCodexHandler creates a new Codex handler
@@ -38,6 +46,11 @@ func (h *CodexHandler) SetTaskService(taskSvc *service.CodexTaskService) {
3846
h.taskSvc = taskSvc
3947
}
4048

49+
// SetOAuthServer injects the local OAuth callback server.
50+
func (h *CodexHandler) SetOAuthServer(server OAuthServer) {
51+
h.oauthServer = server
52+
}
53+
4154
// ServeHTTP routes Codex requests
4255
// Routes:
4356
//
@@ -183,6 +196,16 @@ func (h *CodexHandler) handleValidateToken(w http.ResponseWriter, r *http.Reques
183196

184197
// handleOAuthStart starts the OAuth authorization flow
185198
func (h *CodexHandler) handleOAuthStart(w http.ResponseWriter, r *http.Request) {
199+
if h.oauthServer != nil && !h.oauthServer.IsRunning() {
200+
startCtx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
201+
if err := h.oauthServer.Start(startCtx); err != nil {
202+
cancel()
203+
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
204+
return
205+
}
206+
cancel()
207+
}
208+
186209
result, err := h.StartOAuth()
187210
if err != nil {
188211
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -260,6 +283,8 @@ func (h *CodexHandler) handleOAuthCallback(w http.ResponseWriter, r *http.Reques
260283
w.Header().Set("Content-Type", "text/html; charset=utf-8")
261284
w.WriteHeader(http.StatusOK)
262285
w.Write([]byte(codexOAuthSuccessHTML))
286+
287+
h.stopOAuthServerAsync()
263288
}
264289

265290
// handleOAuthExchange handles POST /codex/oauth/exchange
@@ -351,6 +376,19 @@ func (h *CodexHandler) sendOAuthErrorResult(w http.ResponseWriter, state, errorM
351376
w.Header().Set("Content-Type", "text/html; charset=utf-8")
352377
w.WriteHeader(http.StatusBadRequest)
353378
w.Write([]byte(codexOAuthErrorHTML))
379+
380+
h.stopOAuthServerAsync()
381+
}
382+
383+
func (h *CodexHandler) stopOAuthServerAsync() {
384+
if h.oauthServer == nil || !h.oauthServer.IsRunning() {
385+
return
386+
}
387+
go func() {
388+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
389+
defer cancel()
390+
_ = h.oauthServer.Stop(ctx)
391+
}()
354392
}
355393

356394
// RefreshProviderInfo refreshes the Codex provider info by re-validating the refresh token

web/src/components/language-toggle.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import * as React from 'react';
21
import { Languages, Check } from 'lucide-react';
32
import { useTranslation } from 'react-i18next';
43
import {

web/src/locales/en.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,17 @@
891891
"editDescription": "Update your custom provider settings",
892892
"basicInfo": "1. Basic Information",
893893
"clientConfig": "2. Client Configuration",
894+
"cloakTitle": "3. Claude Cloaking",
895+
"cloakMode": "Cloak Mode",
896+
"cloakModeDesc": "Controls whether the Claude Code system prompt and fake user_id are injected.",
897+
"cloakModeAuto": "Auto (only non-claude-cli clients)",
898+
"cloakModeAlways": "Always (force cloaking)",
899+
"cloakModeNever": "Never (disable cloaking)",
900+
"cloakStrictMode": "Strict Mode",
901+
"cloakStrictModeDesc": "Replace all system messages with the Claude Code prompt.",
902+
"cloakSensitiveWords": "Sensitive Words",
903+
"cloakSensitiveWordsDesc": "Words will be obfuscated with zero-width characters (comma or newline separated).",
904+
"cloakSensitiveWordsPlaceholder": "e.g. secret, internal-project",
894905
"displayName": "Display Name",
895906
"apiEndpoint": "API Endpoint",
896907
"apiKey": "API Key",

web/src/locales/zh.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -891,6 +891,17 @@
891891
"editDescription": "更新您的自定义提供商设置",
892892
"basicInfo": "1. 基本信息",
893893
"clientConfig": "2. 客户端配置",
894+
"cloakTitle": "3. Claude 伪装",
895+
"cloakMode": "伪装模式",
896+
"cloakModeDesc": "控制是否注入 Claude Code system 提示词与伪造 user_id。",
897+
"cloakModeAuto": "自动(仅非 claude-cli 客户端)",
898+
"cloakModeAlways": "始终(强制伪装)",
899+
"cloakModeNever": "从不(禁用伪装)",
900+
"cloakStrictMode": "严格模式",
901+
"cloakStrictModeDesc": "用 Claude Code 提示词替换所有 system 消息。",
902+
"cloakSensitiveWords": "敏感词",
903+
"cloakSensitiveWordsDesc": "将以零宽字符混淆(逗号或换行分隔)。",
904+
"cloakSensitiveWordsPlaceholder": "例如:secret, internal-project",
894905
"displayName": "显示名称",
895906
"apiEndpoint": "API 端点",
896907
"apiKey": "API 密钥",

web/src/pages/providers/components/clients-config-section.tsx

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { useState, useEffect } from 'react';
22
import { Switch } from '@/components/ui';
33
import { Input } from '@/components/ui/input';
4+
import { Textarea } from '@/components/ui/textarea';
5+
import {
6+
Select,
7+
SelectContent,
8+
SelectItem,
9+
SelectTrigger,
10+
SelectValue,
11+
} from '@/components/ui/select';
412
import { ClientIcon } from '@/components/icons/client-icons';
513
import type { ClientType } from '@/lib/transport';
614
import type { ClientConfig } from '../types';
@@ -9,6 +17,12 @@ import { useTranslation } from 'react-i18next';
917
interface ClientsConfigSectionProps {
1018
clients: ClientConfig[];
1119
onUpdateClient: (clientId: ClientType, updates: Partial<ClientConfig>) => void;
20+
cloak?: {
21+
mode: 'auto' | 'always' | 'never';
22+
strictMode: boolean;
23+
sensitiveWords: string;
24+
};
25+
onUpdateCloak?: (updates: Partial<ClientsConfigSectionProps['cloak']>) => void;
1226
}
1327

1428
// Separate component for multiplier input to manage local state
@@ -57,21 +71,24 @@ function MultiplierInput({
5771
);
5872
}
5973

60-
export function ClientsConfigSection({ clients, onUpdateClient }: ClientsConfigSectionProps) {
74+
export function ClientsConfigSection({
75+
clients,
76+
onUpdateClient,
77+
cloak,
78+
onUpdateCloak,
79+
}: ClientsConfigSectionProps) {
6180
const { t } = useTranslation();
6281
return (
6382
<div>
64-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
65-
{clients.map((client) => (
83+
<div className="rounded-xl border border-border overflow-hidden bg-card">
84+
{clients.map((client, index) => (
6685
<div
6786
key={client.id}
68-
className={`rounded-xl border transition-all duration-200 flex flex-col ${
69-
client.enabled
70-
? 'bg-card border-border shadow-sm'
71-
: 'bg-muted/30 border-transparent opacity-80 hover:opacity-100 hover:bg-muted/50'
72-
}`}
87+
className={`px-4 py-4 transition-colors duration-200 ${
88+
client.enabled ? 'bg-card' : 'bg-muted/30'
89+
} ${index > 0 ? 'border-t border-border' : ''}`}
7390
>
74-
<div className="flex items-center justify-between p-4 border-b border-transparent">
91+
<div className="flex items-center justify-between">
7592
<div className="flex items-center gap-3">
7693
<ClientIcon type={client.id} size={32} />
7794
<span
@@ -90,10 +107,12 @@ export function ClientsConfigSection({ clients, onUpdateClient }: ClientsConfigS
90107

91108
{/* Expandable/Visible Content */}
92109
<div
93-
className={`px-4 pb-4 transition-all duration-200 ${client.enabled ? 'opacity-100' : 'opacity-50 grayscale pointer-events-none'}`}
110+
className={`pt-4 transition-all duration-200 ${
111+
client.enabled ? 'opacity-100' : 'opacity-50 grayscale pointer-events-none'
112+
}`}
94113
>
95-
<div className="space-y-3">
96-
<div className="bg-muted/50 rounded-lg p-3 border border-border/50">
114+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
115+
<div>
97116
<label className="text-xs font-medium text-muted-foreground block mb-1.5 uppercase tracking-wide">
98117
{t('provider.endpointOverride')}
99118
</label>
@@ -106,7 +125,7 @@ export function ClientsConfigSection({ clients, onUpdateClient }: ClientsConfigS
106125
className="text-sm w-full bg-card h-9"
107126
/>
108127
</div>
109-
<div className="bg-muted/50 rounded-lg p-3 border border-border/50">
128+
<div>
110129
<label className="text-xs font-medium text-muted-foreground block mb-1.5 uppercase tracking-wide">
111130
{t('provider.multiplier', 'Price Multiplier')}
112131
</label>
@@ -123,6 +142,65 @@ export function ClientsConfigSection({ clients, onUpdateClient }: ClientsConfigS
123142
</div>
124143
</div>
125144
</div>
145+
146+
{client.id === 'claude' && cloak && onUpdateCloak && (
147+
<div className="mt-5 space-y-4">
148+
<div className="border-t border-border/60" />
149+
<div>
150+
<label className="text-xs font-medium text-muted-foreground block mb-1.5 uppercase tracking-wide">
151+
{t('provider.cloakMode')}
152+
</label>
153+
<Select
154+
value={cloak.mode}
155+
onValueChange={(value) =>
156+
onUpdateCloak({ mode: value as 'auto' | 'always' | 'never' })
157+
}
158+
>
159+
<SelectTrigger className="w-full">
160+
<SelectValue placeholder={t('provider.cloakModeAuto')} />
161+
</SelectTrigger>
162+
<SelectContent>
163+
<SelectItem value="auto">{t('provider.cloakModeAuto')}</SelectItem>
164+
<SelectItem value="always">{t('provider.cloakModeAlways')}</SelectItem>
165+
<SelectItem value="never">{t('provider.cloakModeNever')}</SelectItem>
166+
</SelectContent>
167+
</Select>
168+
<p className="text-xs text-muted-foreground mt-1">
169+
{t('provider.cloakModeDesc')}
170+
</p>
171+
</div>
172+
173+
<div className="flex items-center justify-between gap-3">
174+
<div className="space-y-1">
175+
<p className="text-sm font-medium text-foreground">
176+
{t('provider.cloakStrictMode')}
177+
</p>
178+
<p className="text-xs text-muted-foreground">
179+
{t('provider.cloakStrictModeDesc')}
180+
</p>
181+
</div>
182+
<Switch
183+
checked={cloak.strictMode}
184+
onCheckedChange={(checked) => onUpdateCloak({ strictMode: checked })}
185+
/>
186+
</div>
187+
188+
<div>
189+
<label className="text-xs font-medium text-muted-foreground block mb-1.5 uppercase tracking-wide">
190+
{t('provider.cloakSensitiveWords')}
191+
</label>
192+
<Textarea
193+
value={cloak.sensitiveWords}
194+
onChange={(e) => onUpdateCloak({ sensitiveWords: e.target.value })}
195+
placeholder={t('provider.cloakSensitiveWordsPlaceholder')}
196+
className="min-h-[88px]"
197+
/>
198+
<p className="text-xs text-muted-foreground mt-1">
199+
{t('provider.cloakSensitiveWordsDesc')}
200+
</p>
201+
</div>
202+
</div>
203+
)}
126204
</div>
127205
</div>
128206
))}

web/src/pages/providers/components/custom-config-step.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ export function CustomConfigStep() {
2626
const createProvider = useCreateProvider();
2727
const createModelMapping = useCreateModelMapping();
2828

29+
const parseSensitiveWords = (value: string): string[] => {
30+
return value
31+
.split(/[\n,]/)
32+
.map((item) => item.trim())
33+
.filter(Boolean);
34+
};
35+
2936
const handleSave = async () => {
3037
if (!isValid()) return;
3138

@@ -55,6 +62,16 @@ export function CustomConfigStep() {
5562
apiKey: formData.apiKey,
5663
clientBaseURL: Object.keys(clientBaseURL).length > 0 ? clientBaseURL : undefined,
5764
clientMultiplier: Object.keys(clientMultiplier).length > 0 ? clientMultiplier : undefined,
65+
cloak:
66+
formData.cloakMode !== 'auto' ||
67+
formData.cloakStrictMode ||
68+
parseSensitiveWords(formData.cloakSensitiveWords || '').length > 0
69+
? {
70+
mode: formData.cloakMode,
71+
strictMode: formData.cloakStrictMode,
72+
sensitiveWords: parseSensitiveWords(formData.cloakSensitiveWords || ''),
73+
}
74+
: undefined,
5875
},
5976
},
6077
supportedClientTypes,
@@ -170,7 +187,22 @@ export function CustomConfigStep() {
170187
<h3 className="text-lg font-semibold text-text-primary border-b border-border pb-2">
171188
{t('provider.clientConfig')}
172189
</h3>
173-
<ClientsConfigSection clients={formData.clients} onUpdateClient={updateClient} />
190+
<ClientsConfigSection
191+
clients={formData.clients}
192+
onUpdateClient={updateClient}
193+
cloak={{
194+
mode: formData.cloakMode || 'auto',
195+
strictMode: !!formData.cloakStrictMode,
196+
sensitiveWords: formData.cloakSensitiveWords || '',
197+
}}
198+
onUpdateCloak={(updates) =>
199+
updateFormData({
200+
cloakMode: updates?.mode ?? formData.cloakMode,
201+
cloakStrictMode: updates?.strictMode ?? formData.cloakStrictMode,
202+
cloakSensitiveWords: updates?.sensitiveWords ?? formData.cloakSensitiveWords,
203+
})
204+
}
205+
/>
174206
</div>
175207

176208
{/* Model Mapping Section */}

0 commit comments

Comments
 (0)