diff --git a/.memory/worklog.json b/.memory/worklog.json index ef65267..66563e8 100644 --- a/.memory/worklog.json +++ b/.memory/worklog.json @@ -1,18 +1,5 @@ { "entries": [ - { - "files_changed": 1, - "hash": "d1741e4", - "message": "auto-save 2026-05-10 08:44 (~1)", - "ts": "2026-05-10T08:44:59+08:00", - "type": "commit" - }, - { - "files_changed": 1, - "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 08:44 (~1)", - "ts": "2026-05-10T00:46:24Z", - "type": "session-heartbeat" - }, { "files_changed": 1, "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 08:44 (~1)", @@ -3264,6 +3251,19 @@ "message": "auto-save 2026-05-11 17:16 (~1)", "hash": "5fb59ac", "files_changed": 1 + }, + { + "ts": "2026-05-11T17:22:16+08:00", + "type": "commit", + "message": "auto-save 2026-05-11 17:22 (~2)", + "hash": "e870792", + "files_changed": 2 + }, + { + "ts": "2026-05-11T09:26:29Z", + "type": "session-heartbeat", + "message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 4 项未提交变更 · 最近提交:auto-save 2026-05-11 17:22 (~2)", + "files_changed": 4 } ] } diff --git a/server/feishu_bridge.py b/server/feishu_bridge.py index dff0669..9ca5954 100644 --- a/server/feishu_bridge.py +++ b/server/feishu_bridge.py @@ -1287,6 +1287,38 @@ def handle_notify(path: str, headers: dict[str, str], body: dict[str, Any]) -> t class Handler(BaseHTTPRequestHandler): server_version = "HermesFeishuBridge/1.0" + def proxy_chat_completions(self, body: dict[str, Any]) -> None: + req, stream, status, error = build_profile_chat_request(body) + if error is not None or req is None: + self.send_json(status, error or {"code": status, "msg": "invalid chat request"}) + return + 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" + ) + self.send_response(resp.status) + self.send_header("Content-Type", content_type) + self.send_header("Cache-Control", "no-cache") + self.end_headers() + while True: + chunk = resp.read(8192) + if not chunk: + break + self.wfile.write(chunk) + self.wfile.flush() + except urllib.error.HTTPError as exc: + raw = exc.read() + self.send_response(exc.code) + self.send_header("Content-Type", exc.headers.get("Content-Type") or "application/json") + self.end_headers() + self.wfile.write(raw or json.dumps({"code": exc.code, "msg": exc.reason}).encode("utf-8")) + except BrokenPipeError: + logging.info("chat proxy client disconnected") + except Exception as exc: + logging.error("chat proxy failed:\n%s", traceback.format_exc()) + self.send_json(500, {"code": 500, "msg": str(exc)}) + def do_GET(self) -> None: headers = {key.lower(): value for key, value in self.headers.items()} if self.path == "/health": @@ -1318,6 +1350,13 @@ class Handler(BaseHTTPRequestHandler): status, payload = handle_hermes_config_post(headers, body) elif self.path == "/feishu/ui-config": status, payload = handle_ui_config_post(headers, body) + elif self.path == "/feishu/chat/completions": + ok, status, message = is_admin_request(headers) + if not ok: + self.send_json(status, {"code": status, "msg": message}) + else: + self.proxy_chat_completions(body) + return elif self.path == "/feishu/notify" or self.path.startswith("/feishu/notify/"): status, payload = handle_notify(self.path, headers, body) else: diff --git a/src/app.js b/src/app.js index 48b53f8..09a5048 100644 --- a/src/app.js +++ b/src/app.js @@ -300,17 +300,55 @@ function modelProfileById(id) { return state.modelProfiles.find(profile => profile.id === id) || null; } +function profileForAgent(agent) { + if (!agent?.modelProfileId) return null; + const profile = modelProfileById(agent.modelProfileId); + return profile && profile.enabled !== false ? profile : null; +} + function modelForAgent(agent) { - const profile = modelProfileById(agent?.modelProfileId); + const profile = profileForAgent(agent); return profile?.model || agent?.model || state.model || DEFAULT_MODEL_ID; } function modelLabelForAgent(agent) { - const profile = modelProfileById(agent?.modelProfileId); + const profile = profileForAgent(agent); if (profile) return profile.name + " · " + profile.model; return agent?.model || state.model || DEFAULT_MODEL_ID; } +function profileNeedsProxy(profile) { + return !!(profile && profile.id !== "runtime-default" && (profile.baseUrl || profile.apiKeyRef)); +} + +function chatRouteForAgent(agent) { + const profile = profileForAgent(agent); + if (profileNeedsProxy(profile)) { + return { + url: "/feishu/chat/completions", + proxy: true, + modelProfileId: profile.id, + }; + } + return { + url: state.apiBase + "/chat/completions", + proxy: false, + modelProfileId: "", + }; +} + +function chatFetchOptions(route, body, stream = false) { + const headers = { "Content-Type": "application/json" }; + if (stream) headers.Accept = "text/event-stream"; + if (!route.proxy) headers.Authorization = "Bearer " + state.apiKey; + return { + method: "POST", + credentials: "same-origin", + headers, + body: JSON.stringify(body), + }; +} + function syncModelOptionsFromProfiles() { for (const profile of state.modelProfiles) { ensureModelChoice(profile.model, profile.name || profile.model); @@ -1413,6 +1451,7 @@ async function sendMessage(text) { if (sys) msgsForApi = [{ role: "system", content: sys }, ...msgsForApi]; modelForApi = modelForAgent(useAgent); } + const route = chatRouteForAgent(useAgent); // 本次使用完清掉 pendingAgent if (pendingId) { @@ -1424,19 +1463,13 @@ async function sendMessage(text) { messages: msgsForApi, stream: state.stream, }; + if (route.modelProfileId) body.modelProfileId = route.modelProfileId; try { if (state.stream) { - await streamChat(body, assistantMsg); + await streamChat(body, assistantMsg, route); } else { - const res = await apiFetch(state.apiBase + "/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer " + state.apiKey, - }, - body: JSON.stringify(body), - }); + const res = await apiFetch(route.url, chatFetchOptions(route, body)); if (!res.ok) throw new Error("HTTP " + res.status + ": " + await res.text()); const data = await res.json(); assistantMsg.content = data?.choices?.[0]?.message?.content || "(无回复)"; @@ -1459,16 +1492,8 @@ async function sendMessage(text) { } } -async function streamChat(body, assistantMsg) { - const res = await apiFetch(state.apiBase + "/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer " + state.apiKey, - "Accept": "text/event-stream", - }, - body: JSON.stringify(body), - }); +async function streamChat(body, assistantMsg, route = chatRouteForAgent(null)) { + const res = await apiFetch(route.url, chatFetchOptions(route, body, true)); if (!res.ok) throw new Error("HTTP " + res.status + ": " + await res.text()); const reader = res.body.getReader(); @@ -2968,14 +2993,10 @@ async function runClusterOne(agent, prompt, col) { { role: "system", content: composeSystemPrompt(agent) }, { role: "user", content: prompt }, ]; - const res = await apiFetch(state.apiBase + "/chat/completions", { - method: "POST", - headers: { - "Content-Type": "application/json", - "Authorization": "Bearer " + state.apiKey, - }, - body: JSON.stringify({ model: modelForAgent(agent), messages, stream: false }), - }); + const route = chatRouteForAgent(agent); + const requestBody = { model: modelForAgent(agent), messages, stream: false }; + if (route.modelProfileId) requestBody.modelProfileId = route.modelProfileId; + const res = await apiFetch(route.url, chatFetchOptions(route, requestBody)); if (!res.ok) throw new Error("HTTP " + res.status); const data = await res.json(); const text = data?.choices?.[0]?.message?.content || "(无回复)"; diff --git a/src/index.html b/src/index.html index ecefdbb..4909b45 100644 --- a/src/index.html +++ b/src/index.html @@ -1272,6 +1272,7 @@ git push # Gitea kangwan/hermes-glass-ui-personal