Skip to content

Commit 6199925

Browse files
zyszys
authored andcommitted
feat: single console with dual bridge ports (CN 8963 + Global 8964)
The Global web console (3589) was unused while its bridge path was still required. Merge both bridges into one console instance: - add --global-port flag (default 8964, 0 disables); --instance=cn now starts console 3588 + bridge 8963 + bridge 8964 in one process - 8964 runs a Global-only account pool so international models never land on a CN account; its cooldown state file is region-isolated (pool_state_global.json) so it cannot overwrite the main pool - 8963 keeps the full pool and relies on existing per-model region routing for cross-region requests - keep --instance=global as a standalone escape hatch (3589 + 8964 only) - StopBridge now shuts down both servers Verified: 8963 exposes 14 models incl. CN-only q37fmodel and no ultimate; 8964 exposes 15 models incl. ultimate and no q37fmodel; one console lists all three accounts (2 cn + 1 global) and one-click check-in covers both.
1 parent 5e2ac82 commit 6199925

2 files changed

Lines changed: 107 additions & 12 deletions

File tree

‎main.go‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,26 @@ var svc *Service
3131

3232
func main() {
3333
webPort := flag.Int("web-port", 3588, "web console port")
34-
bridgePort := flag.Int("bridge-port", 8963, "bridge api port")
34+
bridgePort := flag.Int("bridge-port", 8963, "bridge api port (CN 优先,内部按模型 region 自动路由)")
35+
globalPort := flag.Int("global-port", 8964, "global bridge api port (强制 Global 账号池);0=不启动")
3536
bind := flag.String("bind", "0.0.0.0", "console bind address")
3637
dataDir := flag.String("data-dir", "", "data root (default: $QODER2API_HOME or ~/.qoder2api)")
37-
instance := flag.String("instance", "", "preset: cn (3588/8963) or global (3589/8964); sets ports if not overridden")
38+
instance := flag.String("instance", "", "preset: cn (单控制台 3588 + 双桥 8963/8964) 或 global (旧版独立实例 3589/8964)")
3839
flag.Parse()
3940

40-
// 实例预设:方便 CN / Global 双开
41+
// 实例预设:默认 cn = 单控制台 + 双桥(CN 8963 / Global 8964)
42+
// global 预设保留给「只跑海外」的场景(独立控制台 3589 + 单桥 8964)
4143
switch strings.ToLower(strings.TrimSpace(*instance)) {
42-
case "cn", "china":
44+
case "cn", "china", "":
4345
if !isFlagPassed("web-port") {
4446
*webPort = 3588
4547
}
4648
if !isFlagPassed("bridge-port") {
4749
*bridgePort = 8963
4850
}
51+
if !isFlagPassed("global-port") {
52+
*globalPort = 8964
53+
}
4954
if *dataDir == "" && os.Getenv("QODER2API_HOME") == "" {
5055
if home, err := os.UserHomeDir(); err == nil {
5156
*dataDir = filepath.Join(home, ".qoder2api-cn")
@@ -58,6 +63,9 @@ func main() {
5863
if !isFlagPassed("bridge-port") {
5964
*bridgePort = 8964
6065
}
66+
if !isFlagPassed("global-port") {
67+
*globalPort = 0 // 该模式只保留一个桥,8963 不启动
68+
}
6169
if *dataDir == "" && os.Getenv("QODER2API_HOME") == "" {
6270
if home, err := os.UserHomeDir(); err == nil {
6371
*dataDir = filepath.Join(home, ".qoder2api-global")
@@ -91,6 +99,9 @@ func main() {
9199
if *bridgePort > 0 {
92100
svc.bridgePort = *bridgePort
93101
}
102+
if *globalPort > 0 {
103+
svc.globalPort = *globalPort
104+
}
94105

95106
// cookie 按控制台端口隔离,避免 3588/3589 同 host 登录互踢
96107
consoleWebPort = *webPort

‎service.go‎

Lines changed: 92 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ type Service struct {
3232
bridge *bridge.Bridge
3333
pool *bridge.Pool
3434
bridgeSrv *http.Server
35+
globalSrv *http.Server // 8964:强制 Global 账号池
3536
bridgeMu sync.Mutex
3637
bridgePort int
38+
globalPort int
3739
bridgeToken string
3840
basePrompt []byte
3941
}
@@ -176,14 +178,24 @@ func (s *Service) EnsureBridgeRunning() error {
176178
func (s *Service) StopBridge() error {
177179
s.bridgeMu.Lock()
178180
defer s.bridgeMu.Unlock()
181+
var firstErr error
182+
if s.globalSrv != nil {
183+
if err := s.globalSrv.Close(); err != nil && firstErr == nil {
184+
firstErr = err
185+
}
186+
s.globalSrv = nil
187+
}
179188
if s.bridgeSrv == nil {
180-
return nil
189+
return firstErr
181190
}
182191
err := s.bridgeSrv.Close()
183192
s.bridgeSrv = nil
184193
s.bridge = nil
185194
s.pool = nil
186-
return err
195+
if err != nil {
196+
return err
197+
}
198+
return firstErr
187199
}
188200

189201
func (s *Service) restartBridge(acct *account.Account) error {
@@ -249,21 +261,60 @@ func (s *Service) startBridgeWithAccount(acct *account.Account) error {
249261
// mux 外包鉴权中间件:同网络客户端必须携带有效 token 才能消耗账号配额
250262
handler := s.authMiddleware(mux)
251263

252-
srv := &http.Server{
264+
s.bridgeMu.Lock()
265+
s.bridge = b
266+
s.pool = pool
267+
s.bridgeSrv = &http.Server{
253268
Addr: fmt.Sprintf("0.0.0.0:%d", s.bridgePort),
254269
Handler: handler,
255270
}
271+
s.bridgeMu.Unlock()
272+
273+
logger.Info("Bridge started on port %d", s.bridgePort)
274+
go func() {
275+
if err := s.bridgeSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
276+
logger.Error("bridge serve error: %v", err)
277+
}
278+
}()
279+
280+
// 8964:Global 专用桥,强制只走 Global 账号(跨区模型不能误打到国内号)
281+
if s.globalPort > 0 && s.globalPort != s.bridgePort {
282+
if err := s.startGlobalBridge(templateBase); err != nil {
283+
// Global 池为空不应影响主桥,仅告警
284+
logger.Info("global bridge (port %d) not started: %v", s.globalPort, err)
285+
}
286+
}
287+
return nil
288+
}
289+
290+
// startGlobalBridge 启动 8964 端口的 Global 专用桥。
291+
// 只纳入 region=global 的账号,避免国内账号被误用于国际模型请求。
292+
func (s *Service) startGlobalBridge(templateBase map[string]interface{}) error {
293+
pool := s.buildRegionPool(account.RegionGlobal)
294+
if pool.Size() == 0 {
295+
return fmt.Errorf("no global account with usable secret")
296+
}
297+
b := bridge.NewPoolBridge(pool, templateBase)
298+
299+
mux := http.NewServeMux()
300+
mux.HandleFunc("/v1/chat/completions", b.HandleChatCompletions)
301+
mux.HandleFunc("/v1/messages", b.HandleClaudeMessages)
302+
mux.HandleFunc("/v1/models", b.HandleListModels)
303+
mux.HandleFunc("/v1/responses", b.HandleCodexResponses)
304+
305+
srv := &http.Server{
306+
Addr: fmt.Sprintf("0.0.0.0:%d", s.globalPort),
307+
Handler: s.authMiddleware(mux),
308+
}
256309

257310
s.bridgeMu.Lock()
258-
s.bridge = b
259-
s.pool = pool
260-
s.bridgeSrv = srv
311+
s.globalSrv = srv
261312
s.bridgeMu.Unlock()
262313

263-
logger.Info("Bridge started on port %d", s.bridgePort)
314+
logger.Info("Global bridge started on port %d (pool=%d global accounts)", s.globalPort, pool.Size())
264315
go func() {
265316
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
266-
logger.Error("bridge serve error: %v", err)
317+
logger.Error("global bridge serve error: %v", err)
267318
}
268319
}()
269320
return nil
@@ -300,6 +351,39 @@ func (s *Service) buildAccountPool() *bridge.Pool {
300351
return pool
301352
}
302353

354+
// buildRegionPool 只把指定区域的账号建成池(用于 8964 Global 专用桥)。
355+
// 状态文件按区域隔离,避免与主桥互相覆盖冷却记录。
356+
func (s *Service) buildRegionPool(region account.Region) *bridge.Pool {
357+
pool := bridge.NewPoolWithState(filepath.Join(account.DataRoot(), "pool_state_"+string(region)+".json"))
358+
accounts, err := account.List()
359+
if err != nil {
360+
logger.Error("buildRegionPool(%s): list accounts: %v", region, err)
361+
return pool
362+
}
363+
for _, a := range accounts {
364+
if a.Region != region {
365+
continue
366+
}
367+
if !account.HasSecret(a.ID) {
368+
continue
369+
}
370+
pat, err := account.GetSecret(a.ID)
371+
if err != nil {
372+
logger.Error("buildRegionPool(%s): get secret for %s: %v", region, a.Name, err)
373+
continue
374+
}
375+
slot, err := bridge.NewSlotFromSecret(a.ID, a.Name, a.Region, pat)
376+
if err != nil {
377+
logger.Error("buildRegionPool(%s): account %s unusable, skipped: %v", region, a.Name, err)
378+
continue
379+
}
380+
pool.Add(slot)
381+
logger.Info("buildRegionPool(%s): added account %s", region, a.Name)
382+
}
383+
pool.LoadState()
384+
return pool
385+
}
386+
303387
// PoolStatus 返回账号池运行时状态(桥未启动时为空)。
304388
func (s *Service) PoolStatus() []bridge.SlotStatus {
305389
s.bridgeMu.Lock()

0 commit comments

Comments
 (0)