auto-save 2026-05-11 17:27 (~4)
This commit is contained in:
@@ -1,18 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"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,
|
"files_changed": 1,
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 08:44 (~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)",
|
"message": "auto-save 2026-05-11 17:16 (~1)",
|
||||||
"hash": "5fb59ac",
|
"hash": "5fb59ac",
|
||||||
"files_changed": 1
|
"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):
|
class Handler(BaseHTTPRequestHandler):
|
||||||
server_version = "HermesFeishuBridge/1.0"
|
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:
|
def do_GET(self) -> None:
|
||||||
headers = {key.lower(): value for key, value in self.headers.items()}
|
headers = {key.lower(): value for key, value in self.headers.items()}
|
||||||
if self.path == "/health":
|
if self.path == "/health":
|
||||||
@@ -1318,6 +1350,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
status, payload = handle_hermes_config_post(headers, body)
|
status, payload = handle_hermes_config_post(headers, body)
|
||||||
elif self.path == "/feishu/ui-config":
|
elif self.path == "/feishu/ui-config":
|
||||||
status, payload = handle_ui_config_post(headers, body)
|
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/"):
|
elif self.path == "/feishu/notify" or self.path.startswith("/feishu/notify/"):
|
||||||
status, payload = handle_notify(self.path, headers, body)
|
status, payload = handle_notify(self.path, headers, body)
|
||||||
else:
|
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;
|
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) {
|
function modelForAgent(agent) {
|
||||||
const profile = modelProfileById(agent?.modelProfileId);
|
const profile = profileForAgent(agent);
|
||||||
return profile?.model || agent?.model || state.model || DEFAULT_MODEL_ID;
|
return profile?.model || agent?.model || state.model || DEFAULT_MODEL_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
function modelLabelForAgent(agent) {
|
function modelLabelForAgent(agent) {
|
||||||
const profile = modelProfileById(agent?.modelProfileId);
|
const profile = profileForAgent(agent);
|
||||||
if (profile) return profile.name + " · " + profile.model;
|
if (profile) return profile.name + " · " + profile.model;
|
||||||
return agent?.model || state.model || DEFAULT_MODEL_ID;
|
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() {
|
function syncModelOptionsFromProfiles() {
|
||||||
for (const profile of state.modelProfiles) {
|
for (const profile of state.modelProfiles) {
|
||||||
ensureModelChoice(profile.model, profile.name || profile.model);
|
ensureModelChoice(profile.model, profile.name || profile.model);
|
||||||
@@ -1413,6 +1451,7 @@ async function sendMessage(text) {
|
|||||||
if (sys) msgsForApi = [{ role: "system", content: sys }, ...msgsForApi];
|
if (sys) msgsForApi = [{ role: "system", content: sys }, ...msgsForApi];
|
||||||
modelForApi = modelForAgent(useAgent);
|
modelForApi = modelForAgent(useAgent);
|
||||||
}
|
}
|
||||||
|
const route = chatRouteForAgent(useAgent);
|
||||||
|
|
||||||
// 本次使用完清掉 pendingAgent
|
// 本次使用完清掉 pendingAgent
|
||||||
if (pendingId) {
|
if (pendingId) {
|
||||||
@@ -1424,19 +1463,13 @@ async function sendMessage(text) {
|
|||||||
messages: msgsForApi,
|
messages: msgsForApi,
|
||||||
stream: state.stream,
|
stream: state.stream,
|
||||||
};
|
};
|
||||||
|
if (route.modelProfileId) body.modelProfileId = route.modelProfileId;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (state.stream) {
|
if (state.stream) {
|
||||||
await streamChat(body, assistantMsg);
|
await streamChat(body, assistantMsg, route);
|
||||||
} else {
|
} else {
|
||||||
const res = await apiFetch(state.apiBase + "/chat/completions", {
|
const res = await apiFetch(route.url, chatFetchOptions(route, body));
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": "Bearer " + state.apiKey,
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status + ": " + await res.text());
|
if (!res.ok) throw new Error("HTTP " + res.status + ": " + await res.text());
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
assistantMsg.content = data?.choices?.[0]?.message?.content || "(无回复)";
|
assistantMsg.content = data?.choices?.[0]?.message?.content || "(无回复)";
|
||||||
@@ -1459,16 +1492,8 @@ async function sendMessage(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function streamChat(body, assistantMsg) {
|
async function streamChat(body, assistantMsg, route = chatRouteForAgent(null)) {
|
||||||
const res = await apiFetch(state.apiBase + "/chat/completions", {
|
const res = await apiFetch(route.url, chatFetchOptions(route, body, true));
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": "Bearer " + state.apiKey,
|
|
||||||
"Accept": "text/event-stream",
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status + ": " + await res.text());
|
if (!res.ok) throw new Error("HTTP " + res.status + ": " + await res.text());
|
||||||
|
|
||||||
const reader = res.body.getReader();
|
const reader = res.body.getReader();
|
||||||
@@ -2968,14 +2993,10 @@ async function runClusterOne(agent, prompt, col) {
|
|||||||
{ role: "system", content: composeSystemPrompt(agent) },
|
{ role: "system", content: composeSystemPrompt(agent) },
|
||||||
{ role: "user", content: prompt },
|
{ role: "user", content: prompt },
|
||||||
];
|
];
|
||||||
const res = await apiFetch(state.apiBase + "/chat/completions", {
|
const route = chatRouteForAgent(agent);
|
||||||
method: "POST",
|
const requestBody = { model: modelForAgent(agent), messages, stream: false };
|
||||||
headers: {
|
if (route.modelProfileId) requestBody.modelProfileId = route.modelProfileId;
|
||||||
"Content-Type": "application/json",
|
const res = await apiFetch(route.url, chatFetchOptions(route, requestBody));
|
||||||
"Authorization": "Bearer " + state.apiKey,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ model: modelForAgent(agent), messages, stream: false }),
|
|
||||||
});
|
|
||||||
if (!res.ok) throw new Error("HTTP " + res.status);
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const text = data?.choices?.[0]?.message?.content || "(无回复)";
|
const text = data?.choices?.[0]?.message?.content || "(无回复)";
|
||||||
|
|||||||
@@ -1272,6 +1272,7 @@ git push # Gitea kangwan/hermes-glass-ui-personal
|
|||||||
<div class="settings-field">
|
<div class="settings-field">
|
||||||
<label for="modelProfileApiKeyRef">API Key 引用</label>
|
<label for="modelProfileApiKeyRef">API Key 引用</label>
|
||||||
<input type="text" id="modelProfileApiKeyRef" placeholder="OPENROUTER_API_KEY / 服务器环境变量" autocomplete="off">
|
<input type="text" id="modelProfileApiKeyRef" placeholder="OPENROUTER_API_KEY / 服务器环境变量" autocomplete="off">
|
||||||
|
<div class="settings-help">只填服务器环境变量名;前端不保存真实 Key。Agent 使用该 Profile 时会由桥接服务代理请求。</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-field model-profile-checks">
|
<div class="settings-field model-profile-checks">
|
||||||
<label class="settings-checkline"><input type="checkbox" id="modelProfileEnabled" checked> 启用</label>
|
<label class="settings-checkline"><input type="checkbox" id="modelProfileEnabled" checked> 启用</label>
|
||||||
|
|||||||
Reference in New Issue
Block a user