From e870792f205d8c3eea29db537a5bf7d436e0644e Mon Sep 17 00:00:00 2001 From: kang Date: Mon, 11 May 2026 17:22:16 +0800 Subject: [PATCH] auto-save 2026-05-11 17:22 (~2) --- .memory/worklog.json | 14 +++--- server/feishu_bridge.py | 104 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+), 7 deletions(-) diff --git a/.memory/worklog.json b/.memory/worklog.json index 8598e7e..ef65267 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,12 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "d042b1f", - "message": "auto-save 2026-05-10 08:39 (~1)", - "ts": "2026-05-10T08:39:07+08:00", - "type": "commit" - }, { "files_changed": 1, "hash": "d1741e4", @@ -3264,6 +3257,13 @@ "type": "session-heartbeat", "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-11 17:10 (~3)", "files_changed": 1 + }, + { + "ts": "2026-05-11T17:16:35+08:00", + "type": "commit", + "message": "auto-save 2026-05-11 17:16 (~1)", + "hash": "5fb59ac", + "files_changed": 1 } ] } diff --git a/server/feishu_bridge.py b/server/feishu_bridge.py index 6660b09..dff0669 100644 --- a/server/feishu_bridge.py +++ b/server/feishu_bridge.py @@ -282,6 +282,110 @@ def http_json( return json.loads(raw) +def openai_completion_url(base_url: str) -> str: + base = base_url.rstrip("/") + if base.endswith("/chat/completions"): + return base + return f"{base}/chat/completions" + + +def read_env_value(key: str) -> str: + key = key.strip() + if not key: + return "" + return os.environ.get(key, "") or parse_env_file(Config.env_file).get(key, "") + + +def resolve_profile_api_key(profile: dict[str, Any]) -> str: + ref = str(profile.get("apiKeyRef") or "").strip() + if not ref or ref in {"服务器环境变量", "server env", "server-env"}: + return Config.hermes_api_key + if ref.startswith("env:"): + ref = ref[4:].strip() + if re.match(r"^[A-Z][A-Z0-9_]{1,120}$", ref): + return read_env_value(ref) + return "" + + +def profile_for_chat(profile_id: str) -> dict[str, Any]: + runtime = read_hermes_runtime_config() + config = normalize_ui_config(read_ui_config_file(), runtime) + profiles = config.get("modelProfiles") if isinstance(config.get("modelProfiles"), list) else [] + if profile_id: + for profile in profiles: + if profile.get("id") == profile_id: + return profile + raise ValueError(f"unknown modelProfileId: {profile_id}") + for profile in profiles: + if profile.get("isDefault"): + return profile + if profiles: + return profiles[0] + return runtime_model_profile(runtime) + + +def build_profile_chat_request(body: dict[str, Any]) -> tuple[urllib.request.Request | None, bool, int, dict[str, Any] | None]: + profile_id = str(body.get("modelProfileId") or body.get("model_profile_id") or "").strip() + profile = profile_for_chat(profile_id) + if profile.get("enabled") is False: + return None, False, 400, {"code": 400, "msg": "model profile is disabled"} + + base_url = str(profile.get("baseUrl") or "").strip() or Config.hermes_api_base + if not base_url: + return None, False, 400, {"code": 400, "msg": "model profile baseUrl is required"} + model = str(profile.get("model") or body.get("model") or Config.hermes_model).strip() + if not model: + return None, False, 400, {"code": 400, "msg": "model is required"} + if not re.match(r"^https?://", base_url): + return None, False, 400, {"code": 400, "msg": "model profile baseUrl must start with http:// or https://"} + + payload = dict(body) + payload.pop("modelProfileId", None) + payload.pop("model_profile_id", None) + payload["model"] = model + stream = bool(payload.get("stream")) + headers = {"Accept": "text/event-stream" if stream else "application/json"} + api_key = resolve_profile_api_key(profile) + api_key_ref = str(profile.get("apiKeyRef") or "").strip() + if api_key: + headers["Authorization"] = f"Bearer {api_key}" + elif api_key_ref and api_key_ref not in {"服务器环境变量", "server env", "server-env"}: + return None, stream, 400, {"code": 400, "msg": f"API key env not found: {api_key_ref}"} + + url = openai_completion_url(base_url) + raw_body = json.dumps(payload, ensure_ascii=False).encode("utf-8") + req = urllib.request.Request( + url, + data=raw_body, + method="POST", + headers={ + "Content-Type": "application/json; charset=utf-8", + **headers, + }, + ) + return req, stream, 200, None + + +def proxy_chat_completion(body: dict[str, Any]) -> tuple[int, dict[str, Any] | bytes, dict[str, str]]: + req, stream, status, error = build_profile_chat_request(body) + if error is not None or req is None: + return status, error or {"code": status, "msg": "invalid chat request"}, {} + try: + with urllib.request.urlopen(req, timeout=Config.request_timeout) as resp: + content_type = resp.headers.get("Content-Type") or ( + "text/event-stream" if stream else "application/json" + ) + if stream: + return resp.status, resp.read(), {"Content-Type": content_type} + raw = resp.read() + except urllib.error.HTTPError as exc: + raw = exc.read() + return exc.code, raw or json.dumps({"code": exc.code, "msg": exc.reason}).encode("utf-8"), { + "Content-Type": exc.headers.get("Content-Type") or "application/json" + } + return 200, raw, {"Content-Type": "application/json"} + + def json_text(value: Any) -> str: if isinstance(value, str): try: