auto-save 2026-05-11 17:27 (~4)

This commit is contained in:
2026-05-11 17:27:50 +08:00
parent e870792f20
commit c4716b8c44
4 changed files with 103 additions and 42 deletions

View File

@@ -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
}
]
}

View File

@@ -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:

View File

@@ -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 || "(无回复)";

View File

@@ -1272,6 +1272,7 @@ git push # Gitea kangwan/hermes-glass-ui-personal
<div class="settings-field">
<label for="modelProfileApiKeyRef">API Key 引用</label>
<input type="text" id="modelProfileApiKeyRef" placeholder="OPENROUTER_API_KEY / 服务器环境变量" autocomplete="off">
<div class="settings-help">只填服务器环境变量名;前端不保存真实 Key。Agent 使用该 Profile 时会由桥接服务代理请求。</div>
</div>
<div class="settings-field model-profile-checks">
<label class="settings-checkline"><input type="checkbox" id="modelProfileEnabled" checked> 启用</label>