auto-save 2026-05-11 17:27 (~4)
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
79
src/app.js
79
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 || "(无回复)";
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user