#!/usr/bin/env bash
set -euo pipefail

# OCI Generative AI -> OpenAI-compatible API bridge with interactive account manager.
# Run:
#   sudo bash /root/install_oci_openai_bridge.sh
# Then use the menu:
#   1 当前账号
#   2 增加账号  (paste PEM, then paste Oracle API config)
#   3 删除账号
#
# After service starts:
#   Base URL: http://127.0.0.1:8321/v1
#   API key:  shown in the menu, default oci-local-key
#   Model:    xai.grok-4-fast-non-reasoning or any model from /v1/models

INSTALL_DIR="${OCI_BRIDGE_INSTALL_DIR:-/opt/oci-openai-bridge}"
PORT="${OCI_BRIDGE_PORT:-8321}"
BRIDGE_KEY="${OCI_OPENAI_BRIDGE_KEY:-oci-local-key}"
SERVICE_NAME="oci-openai-bridge"
VENV_DIR="$INSTALL_DIR/venv"
APP_PATH="$INSTALL_DIR/oci_openai_bridge.py"
MANAGER_PATH="$INSTALL_DIR/manager.py"
ACCOUNTS_DIR="$INSTALL_DIR/accounts"
ACCOUNTS_JSON="$INSTALL_DIR/accounts.json"

if [[ $EUID -ne 0 ]]; then
  echo "建议用 root/sudo 运行，否则 systemd 服务和 /opt 目录可能没有权限。"
fi

need_pkg() {
  command -v "$1" >/dev/null 2>&1
}

run_as_root_note() {
  if [[ $EUID -ne 0 ]]; then
    echo "当前不是 root，安装系统依赖可能失败；建议用 sudo/root 运行。" >&2
  fi
}

install_system_deps() {
  # This script is meant to run on fresh VPS images too.  Some images do not
  # even have python3, so do not call python before this function succeeds.
  if need_pkg apt-get; then
    export DEBIAN_FRONTEND=noninteractive
    apt-get update
    apt-get install -y ca-certificates curl python3 python3-pip python3-venv || true
    # Debian/Ubuntu sometimes split venv into the exact minor version package.
    if need_pkg python3; then
      local pyver
      pyver="$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
      apt-get install -y "python${pyver}-venv" || true
    fi
    apt-get install -y python3-venv python3-pip || true
  elif need_pkg dnf; then
    dnf install -y ca-certificates curl python3 python3-pip python3-virtualenv || dnf install -y ca-certificates curl python3 python3-pip
  elif need_pkg yum; then
    yum install -y ca-certificates curl python3 python3-pip python3-virtualenv || yum install -y ca-certificates curl python3 python3-pip
  elif need_pkg apk; then
    apk add --no-cache ca-certificates curl python3 py3-pip py3-virtualenv
  elif need_pkg pacman; then
    pacman -Sy --noconfirm ca-certificates curl python python-pip
  else
    echo "未识别包管理器。请先手动安装: python3 python3-pip python3-venv curl ca-certificates" >&2
  fi

  if ! need_pkg python3; then
    echo "python3 仍然不可用，请检查系统包管理器/软件源。" >&2
    exit 1
  fi
}

ensure_python_venv() {
  run_as_root_note
  install_system_deps
  if ! python3 -m venv --help >/dev/null 2>&1; then
    echo "python3 venv 仍然不可用。请安装 python3-venv 后重试。" >&2
    exit 1
  fi
}

install_base() {
  ensure_python_venv
  mkdir -p "$INSTALL_DIR" "$ACCOUNTS_DIR"
  chmod 700 "$INSTALL_DIR" "$ACCOUNTS_DIR" || true
  if [[ ! -x "$VENV_DIR/bin/python" || ! -x "$VENV_DIR/bin/pip" ]]; then
    rm -rf "$VENV_DIR"
    python3 -m venv "$VENV_DIR"
  fi
  if [[ ! -x "$VENV_DIR/bin/pip" ]]; then
    echo "venv 创建失败：$VENV_DIR/bin/pip 不存在。请确认 python3-venv/ensurepip 可用。" >&2
    exit 1
  fi
  "$VENV_DIR/bin/pip" install --upgrade pip >/dev/null
  "$VENV_DIR/bin/pip" install oci fastapi hypercorn httpx pydantic >/dev/null
  if [[ ! -f "$ACCOUNTS_JSON" ]]; then
    printf '{"default": null, "accounts": {}}\n' > "$ACCOUNTS_JSON"
    chmod 600 "$ACCOUNTS_JSON"
  fi
}

write_bridge_app() {
cat > "$APP_PATH" <<'PYAPP'
#!/usr/bin/env python3
import json
import os
import tempfile
import time
import uuid
from pathlib import Path
from typing import Any, Dict, Optional

import oci
from fastapi import FastAPI, Header, HTTPException, Request
from fastapi.responses import JSONResponse, StreamingResponse
from oci.generative_ai import GenerativeAiClient
from oci.generative_ai_inference import GenerativeAiInferenceClient
from oci.generative_ai_inference import models as gim

INSTALL_DIR = Path(os.getenv("OCI_BRIDGE_INSTALL_DIR", "/opt/oci-openai-bridge"))
ACCOUNTS_JSON = Path(os.getenv("OCI_BRIDGE_ACCOUNTS_JSON", str(INSTALL_DIR / "accounts.json")))
BRIDGE_API_KEY = os.getenv("OCI_OPENAI_BRIDGE_KEY", "oci-local-key")
DEFAULT_MODEL = os.getenv("OCI_BRIDGE_DEFAULT_MODEL", "xai.grok-4-fast-non-reasoning")

app = FastAPI(title="OCI Generative AI OpenAI-compatible Bridge")
_client_cache: Dict[str, Any] = {}


def require_auth(auth: Optional[str]):
    if not BRIDGE_API_KEY:
        return
    if not auth or not auth.lower().startswith("bearer ") or auth.split(" ", 1)[1] != BRIDGE_API_KEY:
        raise HTTPException(status_code=401, detail="Invalid API key")


def load_accounts() -> Dict[str, Any]:
    if not ACCOUNTS_JSON.exists():
        return {"default": None, "accounts": {}}
    return json.loads(ACCOUNTS_JSON.read_text())


def get_account(name: Optional[str] = None) -> Dict[str, str]:
    data = load_accounts()
    account_name = name or data.get("default")
    if not account_name:
        raise HTTPException(status_code=400, detail="No OCI account configured. Run the script and choose 增加账号.")
    acc = data.get("accounts", {}).get(account_name)
    if not acc:
        raise HTTPException(status_code=404, detail=f"OCI account not found: {account_name}")
    return {**acc, "name": account_name}


def clients_for(account_name: Optional[str] = None):
    acc = get_account(account_name)
    cache_key = f"{acc['name']}:{acc['region']}:{acc['fingerprint']}"
    if cache_key in _client_cache:
        return _client_cache[cache_key]
    cfg = {
        "user": acc["user"],
        "fingerprint": acc["fingerprint"],
        "tenancy": acc["tenancy"],
        "region": acc["region"],
        "key_file": acc["key_file"],
    }
    ga = GenerativeAiClient(cfg)
    inf = GenerativeAiInferenceClient(cfg)
    _client_cache[cache_key] = (acc, ga, inf)
    return _client_cache[cache_key]


def text_content(text: str):
    return gim.TextContent(text=text or "")


def normalize_content(content: Any):
    if content is None:
        return []
    if isinstance(content, str):
        return [text_content(content)]
    if isinstance(content, list):
        out = []
        for part in content:
            if isinstance(part, str):
                out.append(text_content(part))
            elif isinstance(part, dict):
                typ = part.get("type")
                if typ in ("text", "input_text"):
                    out.append(text_content(part.get("text", "")))
                elif typ in ("image_url", "input_image"):
                    try:
                        out.append(gim.ImageContent(image_url=part.get("image_url") or part.get("image")))
                    except Exception:
                        out.append(text_content(f"[image omitted: {part.get('image_url') or part.get('image')}]"))
                else:
                    out.append(text_content(json.dumps(part, ensure_ascii=False)))
            else:
                out.append(text_content(str(part)))
        return out
    return [text_content(str(content))]


def to_oci_message(msg: Dict[str, Any]):
    role = (msg.get("role") or "user").lower()
    content = normalize_content(msg.get("content"))
    name = msg.get("name")
    if role == "system":
        return gim.SystemMessage(content=content, name=name)
    if role == "developer":
        return gim.DeveloperMessage(content=content, name=name)
    if role == "assistant":
        kwargs = {"content": content, "name": name}
        if msg.get("tool_calls"):
            kwargs["tool_calls"] = [to_oci_tool_call(tc) for tc in msg.get("tool_calls")]
        return gim.AssistantMessage(**kwargs)
    if role == "tool":
        return gim.ToolMessage(content=content, tool_call_id=msg.get("tool_call_id"))
    return gim.UserMessage(content=content, name=name)


def to_oci_tool_definition(tool: Dict[str, Any]):
    fn = tool.get("function", {}) if tool.get("type") == "function" and isinstance(tool.get("function"), dict) else tool
    return gim.FunctionDefinition(
        name=fn.get("name"),
        description=fn.get("description"),
        parameters=fn.get("parameters") or {"type": "object", "properties": {}},
    )


def to_oci_tool_call(tc: Dict[str, Any]):
    fn = tc.get("function", {}) if isinstance(tc.get("function"), dict) else {}
    return gim.FunctionCall(
        id=tc.get("id"),
        name=fn.get("name") or tc.get("name"),
        arguments=fn.get("arguments") or tc.get("arguments") or "{}",
    )


def content_to_text(content: Any) -> str:
    if content is None:
        return ""
    if isinstance(content, str):
        return content
    if isinstance(content, list):
        parts = []
        for p in content:
            txt = getattr(p, "text", None)
            if txt is not None:
                parts.append(txt)
            elif isinstance(p, dict):
                parts.append(p.get("text") or json.dumps(p, ensure_ascii=False))
            else:
                parts.append(str(p))
        return "".join(parts)
    return str(content)


def tool_call_to_openai(tc: Any) -> Dict[str, Any]:
    fn_obj = getattr(tc, "function", None)
    if fn_obj is not None:
        name = getattr(fn_obj, "name", None) or (fn_obj.get("name") if isinstance(fn_obj, dict) else None)
        args = getattr(fn_obj, "arguments", None) or (fn_obj.get("arguments") if isinstance(fn_obj, dict) else None)
    else:
        name = getattr(tc, "name", None)
        args = getattr(tc, "arguments", None)
    return {
        "id": getattr(tc, "id", None) or f"call_{uuid.uuid4().hex[:24]}",
        "type": "function",
        "function": {"name": name, "arguments": args or "{}"},
    }


def finish_reason(fr: Optional[str]) -> str:
    if not fr:
        return "stop"
    f = str(fr).lower()
    if "tool" in f:
        return "tool_calls"
    if "length" in f or "token" in f:
        return "length"
    return "stop" if f in ("stop", "complete") else f


def usage_to_openai(usage: Any):
    if not usage:
        return None
    return {
        "prompt_tokens": int(getattr(usage, "prompt_tokens", 0) or 0),
        "completion_tokens": int(getattr(usage, "completion_tokens", 0) or 0),
        "total_tokens": int(getattr(usage, "total_tokens", 0) or 0),
    }


@app.get("/health")
def health():
    data = load_accounts()
    return {"ok": True, "default_account": data.get("default"), "account_count": len(data.get("accounts", {}))}


@app.get("/v1/models")
def list_models(authorization: Optional[str] = Header(None), x_oci_account: Optional[str] = Header(None)):
    require_auth(authorization)
    acc, ga_client, _ = clients_for(x_oci_account)
    resp = ga_client.list_models(compartment_id=acc["tenancy"], limit=100)
    data = []
    for m in resp.data.items:
        data.append({
            "id": m.display_name,
            "object": "model",
            "created": 0,
            "owned_by": getattr(m, "vendor", None) or "oci",
            "oci_account": acc["name"],
            "oci_id": m.id,
            "type": getattr(m, "type", None),
            "lifecycle_state": getattr(m, "lifecycle_state", None),
        })
    return {"object": "list", "data": data}


@app.post("/v1/chat/completions")
async def chat_completions(request: Request, authorization: Optional[str] = Header(None), x_oci_account: Optional[str] = Header(None)):
    require_auth(authorization)
    body = await request.json()
    acc, _, inf_client = clients_for(x_oci_account or body.get("oci_account"))
    model = body.get("model") or DEFAULT_MODEL
    messages = [to_oci_message(m) for m in body.get("messages", [])]
    tools = [to_oci_tool_definition(t) for t in body.get("tools", [])] if body.get("tools") else None
    kwargs = {
        "api_format": "GENERIC",
        "messages": messages,
        "temperature": body.get("temperature"),
        "top_p": body.get("top_p"),
        "max_tokens": body.get("max_tokens") or body.get("max_completion_tokens"),
        "frequency_penalty": body.get("frequency_penalty"),
        "presence_penalty": body.get("presence_penalty"),
        "stop": body.get("stop"),
        "is_stream": False,
    }
    if tools:
        kwargs["tools"] = tools
        if body.get("tool_choice"):
            kwargs["tool_choice"] = body.get("tool_choice")
    kwargs = {k: v for k, v in kwargs.items() if v is not None}
    details = gim.ChatDetails(
        compartment_id=acc["tenancy"],
        serving_mode=gim.OnDemandServingMode(model_id=model),
        chat_request=gim.GenericChatRequest(**kwargs),
    )
    try:
        result = inf_client.chat(details).data
    except Exception as e:
        raise HTTPException(status_code=getattr(e, "status", 500) or 500, detail=getattr(e, "message", str(e)))

    cr = result.chat_response
    choice_objs = []
    for ch in cr.choices:
        msg = ch.message
        content = content_to_text(getattr(msg, "content", None))
        tool_calls = getattr(msg, "tool_calls", None)
        out_msg = {"role": "assistant", "content": content}
        if tool_calls:
            out_msg["tool_calls"] = [tool_call_to_openai(tc) for tc in tool_calls]
            if not content:
                out_msg["content"] = None
        choice_objs.append({"index": ch.index, "message": out_msg, "finish_reason": finish_reason(getattr(ch, "finish_reason", None))})

    response = {
        "id": f"chatcmpl-{uuid.uuid4().hex}",
        "object": "chat.completion",
        "created": int(time.time()),
        "model": result.model_id,
        "oci_account": acc["name"],
        "choices": choice_objs,
        "usage": usage_to_openai(getattr(cr, "usage", None)),
    }
    if body.get("stream"):
        def gen():
            rid = response["id"]
            for c in choice_objs:
                delta = {"role": "assistant"}
                if c["message"].get("tool_calls"):
                    delta["tool_calls"] = c["message"]["tool_calls"]
                else:
                    delta["content"] = c["message"].get("content") or ""
                chunk = {"id": rid, "object": "chat.completion.chunk", "created": int(time.time()), "model": result.model_id, "choices": [{"index": c["index"], "delta": delta, "finish_reason": None}]}
                yield f"data: {json.dumps(chunk, ensure_ascii=False)}\n\n"
                done = {"id": rid, "object": "chat.completion.chunk", "created": int(time.time()), "model": result.model_id, "choices": [{"index": c["index"], "delta": {}, "finish_reason": c["finish_reason"]}]}
                yield f"data: {json.dumps(done, ensure_ascii=False)}\n\n"
            yield "data: [DONE]\n\n"
        return StreamingResponse(gen(), media_type="text/event-stream")
    return JSONResponse(response)


if __name__ == "__main__":
    import asyncio
    import hypercorn.asyncio
    import hypercorn.config
    port = int(os.getenv("PORT", "8321"))
    hc = hypercorn.config.Config()
    hc.bind = [f"0.0.0.0:{port}"]
    asyncio.run(hypercorn.asyncio.serve(app, hc))
PYAPP
chmod +x "$APP_PATH"
}

write_manager() {
cat > "$MANAGER_PATH" <<'PYMAN'
#!/usr/bin/env python3
import configparser
import json
import os
import re
import subprocess
import sys
from pathlib import Path

INSTALL_DIR = Path(os.environ.get("OCI_BRIDGE_INSTALL_DIR", "/opt/oci-openai-bridge"))
ACCOUNTS_DIR = INSTALL_DIR / "accounts"
ACCOUNTS_JSON = INSTALL_DIR / "accounts.json"
SERVICE_NAME = os.environ.get("OCI_BRIDGE_SERVICE_NAME", "oci-openai-bridge")
PORT = os.environ.get("OCI_BRIDGE_PORT", "8321")
BRIDGE_KEY = os.environ.get("OCI_OPENAI_BRIDGE_KEY", "oci-local-key")


def load_data():
    if not ACCOUNTS_JSON.exists():
        return {"default": None, "accounts": {}}
    try:
        return json.loads(ACCOUNTS_JSON.read_text())
    except Exception:
        return {"default": None, "accounts": {}}


def save_data(data):
    ACCOUNTS_DIR.mkdir(parents=True, exist_ok=True)
    ACCOUNTS_JSON.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n")
    os.chmod(ACCOUNTS_JSON, 0o600)


def safe_name(name):
    # Keep Chinese / unicode account names; only replace characters unsafe for filenames/headers.
    name = re.sub(r"[^\w_.-]+", "-", name.strip(), flags=re.UNICODE)
    return name.strip(".-_") or "default"


def input_block(title, end_hint, end_predicate):
    print("\n" + title)
    print(end_hint)
    lines = []
    while True:
        try:
            line = input()
        except EOFError:
            break
        lines.append(line)
        if end_predicate(line, lines):
            break
    return "\n".join(lines).strip() + "\n"


def parse_oci_config(text):
    # Accept all of these:
    #   1) normal ini with one key=value per line
    #   2) no [DEFAULT] header
    #   3) one-line paste: [DEFAULT] user=... fingerprint=... tenancy=... region=... key_file=...
    # Comments after # are ignored.
    no_comments = []
    for raw in text.splitlines():
        line = raw.strip()
        if not line:
            continue
        if "#" in line:
            line = line.split("#", 1)[0].strip()
        if line:
            no_comments.append(line)
    clean = "\n".join(no_comments).strip()
    if not clean:
        raise ValueError("配置为空")

    # Regex parser first: robust for single-line paste and normal multi-line paste.
    out = {}
    keys = ["user", "fingerprint", "tenancy", "region"]
    for key in keys:
        m = re.search(rf"(?:^|\s){key}\s*=\s*(.*?)(?=\s+(?:user|fingerprint|tenancy|region|key_file)\s*=|\s*\[|$)", clean, flags=re.I | re.S)
        if m:
            out[key] = m.group(1).strip().strip('"\'')

    # Fallback to configparser for strict INI input.
    if any(not out.get(k) for k in keys):
        ini = clean
        if not ini.lstrip().startswith("["):
            ini = "[DEFAULT]\n" + ini
        # If user pasted "[DEFAULT] user=..." on one line, split after section header.
        ini = re.sub(r"^(\[[^\]]+\])\s+(?=\w+\s*=)", r"\1\n", ini)
        try:
            cp = configparser.ConfigParser()
            cp.read_string(ini)
            sec = cp["DEFAULT"]
            for key in keys:
                out.setdefault(key, sec.get(key, "").strip())
        except Exception:
            pass

    missing = [k for k in keys if not out.get(k)]
    if missing:
        raise ValueError("配置缺少字段: " + ", ".join(missing))
    return {k: out[k] for k in keys}


def validate_pem(pem):
    # Accept normal multi-line PEM and also one-line paste where spaces replaced newlines.
    text = pem.strip()
    m = re.search(r"-----BEGIN ([A-Z ]*PRIVATE KEY)-----(.*?)-----END \1-----", text, flags=re.S)
    if not m:
        raise ValueError("没有识别到完整 PRIVATE KEY PEM")
    label = m.group(1)
    body = re.sub(r"\s+", "", m.group(2))
    if not body:
        raise ValueError("PEM 内容为空")
    lines = [f"-----BEGIN {label}-----"]
    lines += [body[i:i+64] for i in range(0, len(body), 64)]
    lines += [f"-----END {label}-----"]
    return "\n".join(lines) + "\n"


def list_accounts():
    data = load_data()
    accounts = data.get("accounts", {})
    print("\n当前账号：")
    if not accounts:
        print("  暂无账号，请选择 2 增加账号。")
    for i, (name, acc) in enumerate(accounts.items(), 1):
        mark = "*" if name == data.get("default") else " "
        print(f" {mark} {i}. {name}")
        print(f"      user: {acc.get('user')}")
        print(f"      tenancy: {acc.get('tenancy')}")
        print(f"      region: {acc.get('region')}")
        print(f"      fingerprint: {acc.get('fingerprint')}")
    print("\n接口信息：")
    print(f"  Base URL: http://127.0.0.1:{PORT}/v1")
    print(f"  API Key:  {BRIDGE_KEY}")
    print("  默认模型: xai.grok-4-fast-non-reasoning")
    print("  指定账号: 请求头加 X-OCI-Account: 账号名")


def add_account():
    data = load_data()
    suggested = f"account{len(data.get('accounts', {})) + 1}"
    name = input(f"账号名称 [{suggested}]: ").strip() or suggested
    name = safe_name(name)
    pem = input_block(
        "请粘贴 PEM 私钥：",
        "支持多行或单行粘贴；识别到 -----END ... PRIVATE KEY----- 后会自动结束。",
        lambda line, lines: "-----END " in line and "PRIVATE KEY-----" in line,
    )
    pem = validate_pem(pem)
    cfg_text = input_block(
        "请粘贴 Oracle API 配置：",
        "例如 [DEFAULT] / user= / fingerprint= / tenancy= / region=。粘贴完后输入单独一行 END 结束。",
        lambda line, lines: line.strip().upper() == "END",
    )
    cfg_text = "\n".join([x for x in cfg_text.splitlines() if x.strip().upper() != "END"])
    acc = parse_oci_config(cfg_text)
    pem_path = ACCOUNTS_DIR / f"{name}.pem"
    pem_path.write_text(pem)
    os.chmod(pem_path, 0o600)
    acc["key_file"] = str(pem_path)
    data.setdefault("accounts", {})[name] = acc
    if not data.get("default") or input("设为默认账号？[Y/n]: ").strip().lower() not in ("n", "no"):
        data["default"] = name
    save_data(data)
    print(f"\n已保存账号: {name}")
    restart_hint()


def delete_account():
    data = load_data()
    names = list(data.get("accounts", {}).keys())
    if not names:
        print("没有可删除的账号。")
        return
    print("\n选择要删除的账号：")
    for i, name in enumerate(names, 1):
        print(f"  {i}. {name}")
    s = input("编号或账号名，直接回车取消: ").strip()
    if not s:
        return
    if s.isdigit() and 1 <= int(s) <= len(names):
        name = names[int(s) - 1]
    else:
        name = s
    if name not in data.get("accounts", {}):
        print("账号不存在。")
        return
    if input(f"确认删除 {name}？输入 YES 确认: ") != "YES":
        print("已取消。")
        return
    pem = data["accounts"][name].get("key_file")
    del data["accounts"][name]
    if data.get("default") == name:
        data["default"] = next(iter(data["accounts"]), None)
    save_data(data)
    if pem:
        try:
            Path(pem).unlink(missing_ok=True)
        except Exception:
            pass
    print(f"已删除账号: {name}")
    restart_hint()


def restart_hint():
    if shutil_systemctl():
        print("如果服务正在运行，已尝试重启服务使账号变更生效。")
        subprocess.run(["systemctl", "restart", f"{SERVICE_NAME}.service"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    else:
        print("如服务正在运行，请手动重启。")


def shutil_systemctl():
    from shutil import which
    return which("systemctl") is not None


def service_menu():
    print("\n服务操作：")
    print("  1. 安装/重启 systemd 服务")
    print("  2. 查看服务状态")
    print("  3. 测试 /v1/models")
    s = input("选择: ").strip()
    if s == "1":
        install_service()
    elif s == "2":
        subprocess.run(["systemctl", "status", f"{SERVICE_NAME}.service", "--no-pager", "-l"])
    elif s == "3":
        test_models()


def install_service():
    service = f"""[Unit]
Description=OCI Generative AI OpenAI-compatible Bridge
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
Environment=OCI_BRIDGE_INSTALL_DIR={INSTALL_DIR}
Environment=OCI_BRIDGE_ACCOUNTS_JSON={ACCOUNTS_JSON}
Environment=OCI_OPENAI_BRIDGE_KEY={BRIDGE_KEY}
Environment=PORT={PORT}
WorkingDirectory={INSTALL_DIR}
ExecStart={INSTALL_DIR}/venv/bin/python {INSTALL_DIR}/oci_openai_bridge.py
Restart=always
RestartSec=5
User=root

[Install]
WantedBy=multi-user.target
"""
    path = Path(f"/etc/systemd/system/{SERVICE_NAME}.service")
    path.write_text(service)
    subprocess.check_call(["systemctl", "daemon-reload"])
    subprocess.check_call(["systemctl", "enable", "--now", f"{SERVICE_NAME}.service"])
    subprocess.check_call(["systemctl", "restart", f"{SERVICE_NAME}.service"])
    print("服务已安装/重启。")


def test_models():
    import urllib.request
    req = urllib.request.Request(f"http://127.0.0.1:{PORT}/v1/models", headers={"Authorization": f"Bearer {BRIDGE_KEY}"})
    try:
        j = json.loads(urllib.request.urlopen(req, timeout=60).read().decode())
        print("model_count:", len(j.get("data", [])))
        print("first_models:", [m.get("id") for m in j.get("data", [])[:10]])
    except Exception as e:
        print("测试失败:", e)


def main():
    ACCOUNTS_DIR.mkdir(parents=True, exist_ok=True)
    if not ACCOUNTS_JSON.exists():
        save_data({"default": None, "accounts": {}})
    while True:
        print("\n==============================")
        print("OCI OpenAI Bridge 账号管理")
        print("==============================")
        print("1、目前账号")
        print("2、增加账号")
        print("3、删除账号")
        print("4、服务/测试")
        print("0、退出")
        choice = input("请选择: ").strip()
        if choice == "1":
            list_accounts()
        elif choice == "2":
            try:
                add_account()
            except Exception as e:
                print("增加失败:", e)
        elif choice == "3":
            delete_account()
        elif choice == "4":
            service_menu()
        elif choice == "0":
            break
        else:
            print("无效选择。")


if __name__ == "__main__":
    main()
PYMAN
chmod +x "$MANAGER_PATH"
}

install_base
write_bridge_app
write_manager
"$VENV_DIR/bin/python" -m py_compile "$APP_PATH" "$MANAGER_PATH"

cat <<EOF
已准备完成。
安装目录: $INSTALL_DIR
账号数据: $ACCOUNTS_JSON
接口地址: http://127.0.0.1:$PORT/v1
本地 API Key: $BRIDGE_KEY

现在进入交互菜单。
EOF

exec "$VENV_DIR/bin/python" "$MANAGER_PATH"
