@@ -686,11 +686,13 @@ async def admin_status(request: Request):
686686 "mode" : pool .get ("mode" ),
687687 "total" : pool .get ("total" ),
688688 "live" : pool .get ("live" ),
689+ "rotatable" : pool .get ("rotatable" ) if pool .get ("rotatable" ) is not None else pool .get ("live" ),
689690 "enabled" : pool .get ("enabled" ),
690691 "in_cooldown" : pool .get ("in_cooldown" ),
691692 "quota_disabled" : pool .get ("quota_disabled" ),
692693 "model_blocked" : pool .get ("model_blocked" ),
693694 "expired" : pool .get ("expired" ),
695+ "disabled" : pool .get ("disabled" ),
694696 "source" : pool .get ("source" ) or "postgres" ,
695697 },
696698 "keys" : key_stats ,
@@ -3647,6 +3649,121 @@ async def get_sub2api_settings(
36473649 return {"ok" : True , "config" : public_sub2api_config ()}
36483650
36493651
3652+ @router .get ("/settings/cliproxyapi" )
3653+ async def get_cliproxyapi_settings (
3654+ request : Request ,
3655+ x_admin_token : str | None = Header (default = None , alias = "X-Admin-Token" ),
3656+ ) -> dict [str , Any ]:
3657+ """Return redacted CLIProxyAPI push config for admin UI."""
3658+ require_admin (request , x_admin_token )
3659+ from grok2api .upstream .cliproxyapi_client import public_cliproxyapi_config
3660+
3661+ return {"ok" : True , "config" : public_cliproxyapi_config ()}
3662+
3663+
3664+ class CliproxyapiConfigBody (BaseModel ):
3665+ enabled : bool | None = None
3666+ base_url : str | None = None
3667+ management_key : str | None = None
3668+ concurrency : int | None = Field (default = None , ge = 1 , le = 16 )
3669+ auto_push_on_register : bool | None = None
3670+ auth_type : str | None = None
3671+ base_upstream : str | None = None
3672+ notes_prefix : str | None = None
3673+ test : bool | None = None
3674+
3675+
3676+ @router .put ("/settings/cliproxyapi" )
3677+ async def put_cliproxyapi_settings (
3678+ body : CliproxyapiConfigBody ,
3679+ request : Request ,
3680+ x_admin_token : str | None = Header (default = None , alias = "X-Admin-Token" ),
3681+ ) -> dict [str , Any ]:
3682+ """Save CLIProxyAPI URL + management key. Blank key keeps previous."""
3683+ require_admin (request , x_admin_token )
3684+ from grok2api .upstream .cliproxyapi_client import (
3685+ public_cliproxyapi_config ,
3686+ set_cliproxyapi_config ,
3687+ test_connection ,
3688+ )
3689+
3690+ patch = body .model_dump (exclude_none = True )
3691+ do_test = bool (patch .pop ("test" , False ))
3692+ try :
3693+ set_cliproxyapi_config (patch , replace = False )
3694+ except Exception as e : # noqa: BLE001
3695+ raise HTTPException (status_code = 400 , detail = str (e )) from e
3696+ out : dict [str , Any ] = {"ok" : True , "config" : public_cliproxyapi_config ()}
3697+ if do_test :
3698+ out ["test" ] = test_connection ()
3699+ out ["ok" ] = bool (out ["test" ].get ("ok" ))
3700+ return out
3701+
3702+
3703+ @router .post ("/settings/cliproxyapi/test" )
3704+ async def test_cliproxyapi_settings (
3705+ request : Request ,
3706+ x_admin_token : str | None = Header (default = None , alias = "X-Admin-Token" ),
3707+ ) -> dict [str , Any ]:
3708+ """Login/list smoke test against CPA management API."""
3709+ require_admin (request , x_admin_token )
3710+ from grok2api .upstream .cliproxyapi_client import test_connection
3711+
3712+ result = test_connection ()
3713+ return {"ok" : bool (result .get ("ok" )), "test" : result }
3714+
3715+
3716+ @router .post ("/accounts/push-cliproxyapi" )
3717+ async def push_accounts_to_cliproxyapi (
3718+ body : Sub2ApiPushBody ,
3719+ request : Request ,
3720+ x_admin_token : str | None = Header (default = None , alias = "X-Admin-Token" ),
3721+ ) -> dict [str , Any ]:
3722+ """Batch push local accounts into CLIProxyAPI auth dir (selected or all)."""
3723+ require_admin (request , x_admin_token )
3724+ from grok2api .upstream .cliproxyapi_client import (
3725+ public_cliproxyapi_config ,
3726+ push_accounts ,
3727+ )
3728+
3729+ cfg = public_cliproxyapi_config ()
3730+ if not cfg .get ("base_url" ):
3731+ raise HTTPException (
3732+ status_code = 400 ,
3733+ detail = "请先在设置页填写 CLIProxyAPI URL 与 management key" ,
3734+ )
3735+ if not cfg .get ("has_management_key" ):
3736+ raise HTTPException (
3737+ status_code = 400 ,
3738+ detail = "请先在设置页填写 CLIProxyAPI management key" ,
3739+ )
3740+ ids = body .account_ids
3741+ push_all = bool (body .all ) or ids is None
3742+ if not push_all and isinstance (ids , list ) and len (ids ) == 0 :
3743+ raise HTTPException (status_code = 400 , detail = "未选择账号" )
3744+ try :
3745+ result = push_accounts (
3746+ None if push_all else list (ids or []),
3747+ concurrency = body .concurrency ,
3748+ )
3749+ except Exception as e : # noqa: BLE001
3750+ raise HTTPException (status_code = 400 , detail = str (e )) from e
3751+ audit_log (
3752+ request ,
3753+ action = "accounts.push_cliproxyapi" ,
3754+ summary = result .get ("message" ) or "推送账号到 CLIProxyAPI" ,
3755+ target_type = "pool" ,
3756+ detail = {
3757+ "success" : result .get ("success" ),
3758+ "failed" : result .get ("failed" ),
3759+ "total" : result .get ("total" ),
3760+ "all" : push_all ,
3761+ },
3762+ ok = bool (result .get ("ok" )),
3763+ )
3764+ return result
3765+
3766+
36503767class Sub2ApiConfigBody (BaseModel ):
36513768 enabled : bool | None = None
36523769 base_url : str | None = None
@@ -3968,6 +4085,62 @@ async def export_sub2api_format(
39684085 return payload
39694086
39704087
4088+ @router .post ("/accounts/export-cliproxyapi-format" )
4089+ async def export_cliproxyapi_format (
4090+ body : Sub2ApiPushBody ,
4091+ request : Request ,
4092+ x_admin_token : str | None = Header (default = None , alias = "X-Admin-Token" ),
4093+ ) -> dict [str , Any ]:
4094+ """Export local accounts as CLIProxyAPI auth-bundle JSON.
4095+
4096+ Shape::
4097+
4098+ {
4099+ "type": "cliproxyapi-auth-bundle",
4100+ "version": 1,
4101+ "exported_at": "...",
4102+ "accounts": [
4103+ {
4104+ "type": "xai",
4105+ "email": "...",
4106+ "access_token": "...",
4107+ "refresh_token": "...",
4108+ "expired": "2026-...",
4109+ "base_url": "https://cli-chat-proxy.grok.com/v1",
4110+ "headers": { "X-XAI-Token-Auth": "xai-grok-cli", ... },
4111+ ...
4112+ }
4113+ ]
4114+ }
4115+
4116+ Each ``accounts[]`` item is a single CPA auth-file body (same as
4117+ ``xai-<email>.json`` under CLIProxyAPI's auth dir). Import back via the
4118+ generic JSON 导入 (auto-detects CPA) or drop files into CPA auth dir.
4119+ """
4120+ require_admin (request , x_admin_token )
4121+ push_all = bool (body .all ) or body .account_ids is None
4122+ ids = None if push_all else [str (x ).strip () for x in (body .account_ids or []) if str (x ).strip ()]
4123+ if not push_all and not ids :
4124+ raise HTTPException (status_code = 400 , detail = "未选择账号" )
4125+ payload = accounts .export_cliproxyapi_payload (
4126+ account_ids = ids ,
4127+ push_all = push_all ,
4128+ )
4129+ audit_log (
4130+ request ,
4131+ action = "accounts.export_cliproxyapi" ,
4132+ summary = f"导出 CLIProxyAPI 格式 { len (payload .get ('accounts' ) or [])} 个账号" ,
4133+ target_type = "pool" ,
4134+ detail = {
4135+ "count" : len (payload .get ("accounts" ) or []),
4136+ "all" : push_all ,
4137+ "selected" : 0 if push_all else len (ids or []),
4138+ },
4139+ ok = True ,
4140+ )
4141+ return payload
4142+
4143+
39714144@router .post ("/accounts/logout" )
39724145async def account_logout (
39734146 request : Request ,
0 commit comments