auto-save 2026-05-11 16:59 (~3)
This commit is contained in:
@@ -1,18 +1,5 @@
|
|||||||
{
|
{
|
||||||
"entries": [
|
"entries": [
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"hash": "ce1b8c3",
|
|
||||||
"message": "auto-save 2026-05-10 08:21 (~1)",
|
|
||||||
"ts": "2026-05-10T08:21:27+08:00",
|
|
||||||
"type": "commit"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files_changed": 1,
|
|
||||||
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 1 项未提交变更 · 最近提交:auto-save 2026-05-10 08:21 (~1)",
|
|
||||||
"ts": "2026-05-10T00:26:24Z",
|
|
||||||
"type": "session-heartbeat"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"files_changed": 1,
|
"files_changed": 1,
|
||||||
"hash": "3b0d2b2",
|
"hash": "3b0d2b2",
|
||||||
@@ -3263,6 +3250,19 @@
|
|||||||
"message": "auto-save 2026-05-11 16:48 (~1)",
|
"message": "auto-save 2026-05-11 16:48 (~1)",
|
||||||
"hash": "8b5eb7a",
|
"hash": "8b5eb7a",
|
||||||
"files_changed": 1
|
"files_changed": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-11T16:53:47+08:00",
|
||||||
|
"type": "commit",
|
||||||
|
"message": "auto-save 2026-05-11 16:53 (~2)",
|
||||||
|
"hash": "48474dc",
|
||||||
|
"files_changed": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ts": "2026-05-11T08:56:29Z",
|
||||||
|
"type": "session-heartbeat",
|
||||||
|
"message": "Codex 会话活跃 · 最近命令:codex · 分支 master · 3 项未提交变更 · 最近提交:auto-save 2026-05-11 16:53 (~2)",
|
||||||
|
"files_changed": 3
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from typing import Any
|
|||||||
|
|
||||||
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
FEISHU_API_BASE = "https://open.feishu.cn/open-apis"
|
||||||
APP_ID_RE = re.compile(r"^cli_[A-Za-z0-9]+$")
|
APP_ID_RE = re.compile(r"^cli_[A-Za-z0-9]+$")
|
||||||
|
UI_ID_RE = re.compile(r"^[A-Za-z0-9_-]{1,80}$")
|
||||||
|
|
||||||
|
|
||||||
def _env(name: str, default: str = "") -> str:
|
def _env(name: str, default: str = "") -> str:
|
||||||
@@ -616,12 +617,32 @@ def normalize_mcp_yaml(value: str) -> str:
|
|||||||
return "mcp_servers:\n" + indented
|
return "mcp_servers:\n" + indented
|
||||||
|
|
||||||
|
|
||||||
def validate_hermes_config_payload(body: dict[str, Any]) -> dict[str, str]:
|
def validate_hermes_config_payload(body: dict[str, Any], current: dict[str, Any] | None = None) -> dict[str, str]:
|
||||||
|
current_model = (current or {}).get("model") if isinstance((current or {}).get("model"), dict) else {}
|
||||||
model = body.get("model") if isinstance(body.get("model"), dict) else {}
|
model = body.get("model") if isinstance(body.get("model"), dict) else {}
|
||||||
default_model = str(model.get("default") or body.get("model_default") or "").strip()
|
default_model = str(
|
||||||
provider = str(model.get("provider") or body.get("provider") or "openrouter").strip()
|
model.get("default")
|
||||||
base_url = str(model.get("base_url") or body.get("base_url") or "").strip()
|
or body.get("model_default")
|
||||||
|
or current_model.get("default")
|
||||||
|
or current_model.get("model")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
provider = str(
|
||||||
|
model.get("provider")
|
||||||
|
or body.get("provider")
|
||||||
|
or current_model.get("provider")
|
||||||
|
or "openrouter"
|
||||||
|
).strip()
|
||||||
|
base_url = str(
|
||||||
|
model.get("base_url")
|
||||||
|
or body.get("base_url")
|
||||||
|
or current_model.get("base_url")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
if "mcp_servers_yaml" in body:
|
||||||
mcp_servers_yaml = normalize_mcp_yaml(str(body.get("mcp_servers_yaml") or ""))
|
mcp_servers_yaml = normalize_mcp_yaml(str(body.get("mcp_servers_yaml") or ""))
|
||||||
|
else:
|
||||||
|
mcp_servers_yaml = str((current or {}).get("mcp_servers_yaml") or "")
|
||||||
|
|
||||||
for label, value, limit in (
|
for label, value, limit in (
|
||||||
("model.default", default_model, 180),
|
("model.default", default_model, 180),
|
||||||
@@ -650,7 +671,8 @@ def validate_hermes_config_payload(body: dict[str, Any]) -> dict[str, str]:
|
|||||||
|
|
||||||
|
|
||||||
def write_hermes_runtime_config(body: dict[str, Any]) -> dict[str, Any]:
|
def write_hermes_runtime_config(body: dict[str, Any]) -> dict[str, Any]:
|
||||||
payload = validate_hermes_config_payload(body)
|
current = read_hermes_runtime_config()
|
||||||
|
payload = validate_hermes_config_payload(body, current)
|
||||||
script = r'''
|
script = r'''
|
||||||
import json
|
import json
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -761,6 +783,232 @@ def handle_hermes_config_post(headers: dict[str, str], body: dict[str, Any]) ->
|
|||||||
return 200, {"code": 0, "msg": "ok", "config": config}
|
return 200, {"code": 0, "msg": "ok", "config": config}
|
||||||
|
|
||||||
|
|
||||||
|
def one_line(value: Any, limit: int, default: str = "") -> str:
|
||||||
|
text = str(value if value is not None else default).strip()
|
||||||
|
text = text.replace("\r", " ").replace("\n", " ").strip()
|
||||||
|
if len(text) > limit:
|
||||||
|
text = text[:limit].rstrip()
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def string_list(value: Any, limit: int = 200) -> list[str]:
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return []
|
||||||
|
out: list[str] = []
|
||||||
|
for item in value[:limit]:
|
||||||
|
text = one_line(item, 180)
|
||||||
|
if text:
|
||||||
|
out.append(text)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def now_ms() -> int:
|
||||||
|
return int(time.time() * 1000)
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_model_profile(runtime: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
model = runtime.get("model") if isinstance(runtime.get("model"), dict) else {}
|
||||||
|
model_id = one_line(model.get("default") or Config.hermes_model or "gemini-3-pro-preview", 180)
|
||||||
|
provider = one_line(model.get("provider") or "openrouter", 80)
|
||||||
|
base_url = one_line(model.get("base_url") or "", 220)
|
||||||
|
created_at = now_ms()
|
||||||
|
return {
|
||||||
|
"id": "runtime-default",
|
||||||
|
"name": "线上默认模型",
|
||||||
|
"provider": provider,
|
||||||
|
"model": model_id,
|
||||||
|
"baseUrl": base_url,
|
||||||
|
"apiKeyRef": "服务器环境变量",
|
||||||
|
"enabled": True,
|
||||||
|
"isDefault": True,
|
||||||
|
"createdAt": created_at,
|
||||||
|
"updatedAt": created_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_model_profile(raw: Any, fallback: dict[str, Any], index: int) -> dict[str, Any] | None:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
profile_id = one_line(raw.get("id"), 80)
|
||||||
|
if not profile_id or not UI_ID_RE.match(profile_id):
|
||||||
|
profile_id = f"model_{index + 1}"
|
||||||
|
name = one_line(raw.get("name"), 80, fallback.get("name") or "模型接入")
|
||||||
|
model = one_line(raw.get("model"), 180, fallback.get("model") or "")
|
||||||
|
provider = one_line(raw.get("provider"), 80, fallback.get("provider") or "openrouter")
|
||||||
|
base_url = one_line(raw.get("baseUrl") or raw.get("base_url"), 220, fallback.get("baseUrl") or "")
|
||||||
|
api_key_ref = one_line(raw.get("apiKeyRef") or raw.get("api_key_ref"), 120, fallback.get("apiKeyRef") or "")
|
||||||
|
if not model:
|
||||||
|
return None
|
||||||
|
created_at = raw.get("createdAt") if isinstance(raw.get("createdAt"), (int, float)) else now_ms()
|
||||||
|
updated_at = raw.get("updatedAt") if isinstance(raw.get("updatedAt"), (int, float)) else now_ms()
|
||||||
|
return {
|
||||||
|
"id": profile_id,
|
||||||
|
"name": name or model,
|
||||||
|
"provider": provider,
|
||||||
|
"model": model,
|
||||||
|
"baseUrl": base_url,
|
||||||
|
"apiKeyRef": api_key_ref,
|
||||||
|
"enabled": bool(raw.get("enabled", True)),
|
||||||
|
"isDefault": bool(raw.get("isDefault", False)),
|
||||||
|
"createdAt": int(created_at),
|
||||||
|
"updatedAt": int(updated_at),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_stages(value: Any) -> dict[str, list[str]]:
|
||||||
|
value = value if isinstance(value, dict) else {}
|
||||||
|
return {
|
||||||
|
"pre": string_list(value.get("pre")),
|
||||||
|
"exec": string_list(value.get("exec")),
|
||||||
|
"post": string_list(value.get("post")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_agent(raw: Any, index: int) -> dict[str, Any] | None:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
return None
|
||||||
|
agent_id = one_line(raw.get("id"), 80)
|
||||||
|
if not agent_id or not UI_ID_RE.match(agent_id):
|
||||||
|
agent_id = f"agent_{index + 1}"
|
||||||
|
name = one_line(raw.get("name"), 80)
|
||||||
|
system_prompt = str(raw.get("systemPrompt") or raw.get("system_prompt") or "").strip()
|
||||||
|
if not name or not system_prompt:
|
||||||
|
return None
|
||||||
|
created_at = raw.get("createdAt") if isinstance(raw.get("createdAt"), (int, float)) else now_ms()
|
||||||
|
updated_at = raw.get("updatedAt") if isinstance(raw.get("updatedAt"), (int, float)) else now_ms()
|
||||||
|
return {
|
||||||
|
"id": agent_id,
|
||||||
|
"emoji": one_line(raw.get("emoji"), 16, "🤖"),
|
||||||
|
"name": name,
|
||||||
|
"desc": one_line(raw.get("desc"), 180),
|
||||||
|
"model": one_line(raw.get("model"), 180),
|
||||||
|
"modelProfileId": one_line(raw.get("modelProfileId") or raw.get("model_profile_id"), 80),
|
||||||
|
"systemPrompt": system_prompt[:20000],
|
||||||
|
"skills": string_list(raw.get("skills")),
|
||||||
|
"stages": normalize_stages(raw.get("stages")),
|
||||||
|
"createdAt": int(created_at),
|
||||||
|
"updatedAt": int(updated_at),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_ui_config(raw: dict[str, Any], runtime: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||||
|
runtime_profile = runtime_model_profile(runtime or {})
|
||||||
|
model_profiles_raw = raw.get("modelProfiles") if isinstance(raw.get("modelProfiles"), list) else []
|
||||||
|
profiles: list[dict[str, Any]] = []
|
||||||
|
seen_profile_ids: set[str] = set()
|
||||||
|
for idx, item in enumerate(model_profiles_raw[:80]):
|
||||||
|
profile = normalize_model_profile(item, runtime_profile, idx)
|
||||||
|
if not profile or profile["id"] in seen_profile_ids:
|
||||||
|
continue
|
||||||
|
seen_profile_ids.add(profile["id"])
|
||||||
|
profiles.append(profile)
|
||||||
|
if not profiles:
|
||||||
|
profiles.append(runtime_profile)
|
||||||
|
if not any(profile.get("isDefault") for profile in profiles):
|
||||||
|
profiles[0]["isDefault"] = True
|
||||||
|
default_seen = False
|
||||||
|
for profile in profiles:
|
||||||
|
if profile.get("isDefault") and not default_seen:
|
||||||
|
default_seen = True
|
||||||
|
else:
|
||||||
|
profile["isDefault"] = False
|
||||||
|
|
||||||
|
agents_raw = raw.get("agents") if isinstance(raw.get("agents"), list) else []
|
||||||
|
agents: list[dict[str, Any]] = []
|
||||||
|
seen_agent_ids: set[str] = set()
|
||||||
|
for idx, item in enumerate(agents_raw[:200]):
|
||||||
|
agent = normalize_agent(item, idx)
|
||||||
|
if not agent or agent["id"] in seen_agent_ids:
|
||||||
|
continue
|
||||||
|
seen_agent_ids.add(agent["id"])
|
||||||
|
agents.append(agent)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"version": 1,
|
||||||
|
"modelProfiles": profiles,
|
||||||
|
"agents": agents,
|
||||||
|
"updatedAt": now_ms(),
|
||||||
|
"configPath": Config.hermes_ui_config_path,
|
||||||
|
"lxc": Config.hermes_agent_lxc,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def read_ui_config_file() -> dict[str, Any]:
|
||||||
|
script = r'''
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = pathlib.Path(sys.argv[1])
|
||||||
|
if not path.exists():
|
||||||
|
print("{}")
|
||||||
|
else:
|
||||||
|
print(path.read_text(encoding="utf-8"))
|
||||||
|
'''
|
||||||
|
raw = run_lxc_command(["python3", "-c", script, Config.hermes_ui_config_path], timeout=20)
|
||||||
|
if not raw.strip():
|
||||||
|
return {}
|
||||||
|
data = json.loads(raw)
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def write_ui_config_file(config: dict[str, Any]) -> None:
|
||||||
|
script = r'''
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
|
||||||
|
path = pathlib.Path(sys.argv[1])
|
||||||
|
payload = json.loads(sys.stdin.read())
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp = path.with_name(path.name + f".tmp-{os.getpid()}")
|
||||||
|
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||||
|
tmp.chmod(0o600)
|
||||||
|
tmp.replace(path)
|
||||||
|
'''
|
||||||
|
run_lxc_command(
|
||||||
|
["python3", "-c", script, Config.hermes_ui_config_path],
|
||||||
|
input_text=json.dumps(config, ensure_ascii=False),
|
||||||
|
timeout=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_ui_config_get(headers: dict[str, str]) -> tuple[int, dict[str, Any]]:
|
||||||
|
ok, status, message = is_admin_request(headers)
|
||||||
|
if not ok:
|
||||||
|
return status, {"code": status, "msg": message}
|
||||||
|
try:
|
||||||
|
runtime = read_hermes_runtime_config()
|
||||||
|
config = normalize_ui_config(read_ui_config_file(), runtime)
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error("failed to read Hermes UI config:\n%s", traceback.format_exc())
|
||||||
|
return 500, {"code": 500, "msg": str(exc)}
|
||||||
|
return 200, {"code": 0, "msg": "ok", "config": config}
|
||||||
|
|
||||||
|
|
||||||
|
def handle_ui_config_post(headers: dict[str, str], body: dict[str, Any]) -> tuple[int, dict[str, Any]]:
|
||||||
|
ok, status, message = is_admin_request(headers)
|
||||||
|
if not ok:
|
||||||
|
return status, {"code": status, "msg": message}
|
||||||
|
try:
|
||||||
|
runtime = read_hermes_runtime_config()
|
||||||
|
raw_config = body.get("config") if isinstance(body.get("config"), dict) else body
|
||||||
|
config = normalize_ui_config(raw_config, runtime)
|
||||||
|
write_ui_config_file(config)
|
||||||
|
except ValueError as exc:
|
||||||
|
return 400, {"code": 400, "msg": str(exc)}
|
||||||
|
except Exception as exc:
|
||||||
|
logging.error("failed to write Hermes UI config:\n%s", traceback.format_exc())
|
||||||
|
return 500, {"code": 500, "msg": str(exc)}
|
||||||
|
logging.info(
|
||||||
|
"updated Hermes UI config model_profiles=%s agents=%s",
|
||||||
|
len(config.get("modelProfiles", [])),
|
||||||
|
len(config.get("agents", [])),
|
||||||
|
)
|
||||||
|
return 200, {"code": 0, "msg": "ok", "config": config}
|
||||||
|
|
||||||
|
|
||||||
def is_admin_request(headers: dict[str, str]) -> tuple[bool, int, str]:
|
def is_admin_request(headers: dict[str, str]) -> tuple[bool, int, str]:
|
||||||
cookie = headers.get("cookie", "")
|
cookie = headers.get("cookie", "")
|
||||||
if "hermes_auth=ok" not in cookie:
|
if "hermes_auth=ok" not in cookie:
|
||||||
@@ -947,6 +1195,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
status, payload = handle_hermes_config_get(headers)
|
status, payload = handle_hermes_config_get(headers)
|
||||||
self.send_json(status, payload)
|
self.send_json(status, payload)
|
||||||
return
|
return
|
||||||
|
if self.path == "/feishu/ui-config":
|
||||||
|
status, payload = handle_ui_config_get(headers)
|
||||||
|
self.send_json(status, payload)
|
||||||
|
return
|
||||||
self.send_json(404, {"code": 404, "msg": "not found"})
|
self.send_json(404, {"code": 404, "msg": "not found"})
|
||||||
|
|
||||||
def do_POST(self) -> None:
|
def do_POST(self) -> None:
|
||||||
@@ -959,6 +1211,8 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
status, payload = handle_apps_admin(headers, body)
|
status, payload = handle_apps_admin(headers, body)
|
||||||
elif self.path == "/feishu/hermes-config":
|
elif self.path == "/feishu/hermes-config":
|
||||||
status, payload = handle_hermes_config_post(headers, body)
|
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/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:
|
||||||
|
|||||||
383
src/app.js
383
src/app.js
@@ -11,6 +11,7 @@ const LS_CUSTOM_SKILLS = "hermes-ui-custom-skills-v1";
|
|||||||
const LS_FLOWS = "hermes-ui-flows-v1";
|
const LS_FLOWS = "hermes-ui-flows-v1";
|
||||||
const LS_TAB = "hermes-ui-active-tab-v1";
|
const LS_TAB = "hermes-ui-active-tab-v1";
|
||||||
const LS_WEEKLY_REPORTS = "hermes-ui-weekly-reports-v1";
|
const LS_WEEKLY_REPORTS = "hermes-ui-weekly-reports-v1";
|
||||||
|
const UI_CONFIG_ENDPOINT = "/feishu/ui-config";
|
||||||
const DEFAULT_MODEL_ID = "google/gemini-3.1-pro-preview";
|
const DEFAULT_MODEL_ID = "google/gemini-3.1-pro-preview";
|
||||||
const LEGACY_DEFAULT_MODEL_ID = "gemini-3-pro-preview";
|
const LEGACY_DEFAULT_MODEL_ID = "gemini-3-pro-preview";
|
||||||
|
|
||||||
@@ -30,6 +31,10 @@ const state = {
|
|||||||
// 智能体
|
// 智能体
|
||||||
agents: {}, // {id: {id, emoji, name, desc, model, systemPrompt, createdAt}}
|
agents: {}, // {id: {id, emoji, name, desc, model, systemPrompt, createdAt}}
|
||||||
editingAgentId: null,
|
editingAgentId: null,
|
||||||
|
modelProfiles: [],
|
||||||
|
editingModelProfileId: null,
|
||||||
|
sharedConfigLoaded: false,
|
||||||
|
sharedConfigAvailable: false,
|
||||||
|
|
||||||
// 自定义 skill
|
// 自定义 skill
|
||||||
customSkills: {}, // {id: {id, emoji, name, prompt, custom: true}}
|
customSkills: {}, // {id: {id, emoji, name, prompt, custom: true}}
|
||||||
@@ -54,22 +59,65 @@ const state = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---------- 入口 ----------
|
// ---------- 入口 ----------
|
||||||
|
function showBootIssue(error, source = "运行错误") {
|
||||||
|
const message = error?.message || String(error || "未知错误");
|
||||||
|
const detail = error?.stack || message;
|
||||||
|
const render = () => {
|
||||||
|
if (!document.body) return;
|
||||||
|
let box = document.getElementById("bootIssue");
|
||||||
|
if (!box) {
|
||||||
|
box = document.createElement("div");
|
||||||
|
box.id = "bootIssue";
|
||||||
|
box.className = "boot-issue";
|
||||||
|
document.body.appendChild(box);
|
||||||
|
}
|
||||||
|
box.innerHTML = `
|
||||||
|
<div class="boot-issue-title">${escapeHTML(source)}</div>
|
||||||
|
<div class="boot-issue-msg">${escapeHTML(message)}</div>
|
||||||
|
<button type="button" onclick="this.closest('.boot-issue')?.remove()">关闭</button>
|
||||||
|
<details><summary>详情</summary><pre>${escapeHTML(detail)}</pre></details>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", render, { once: true });
|
||||||
|
} else {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeBoot(label, fn) {
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[boot]", label, error);
|
||||||
|
showBootIssue(error, label);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("error", (event) => showBootIssue(event.error || event.message, "页面脚本错误"));
|
||||||
|
window.addEventListener("unhandledrejection", (event) => showBootIssue(event.reason, "异步请求错误"));
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
loadTheme();
|
safeBoot("加载主题", loadTheme);
|
||||||
loadSettings();
|
safeBoot("加载本地设置", loadSettings);
|
||||||
loadCustomSkills();
|
safeBoot("加载自定义技能", loadCustomSkills);
|
||||||
loadFlows();
|
safeBoot("加载本地编排", loadFlows);
|
||||||
loadAgents();
|
safeBoot("加载本地智能体", loadAgents);
|
||||||
loadConversations();
|
safeBoot("加载本地对话", loadConversations);
|
||||||
loadWeeklyReports();
|
safeBoot("加载周报记录", loadWeeklyReports);
|
||||||
bindTabs();
|
safeBoot("绑定导航", bindTabs);
|
||||||
bindChat();
|
safeBoot("绑定对话", bindChat);
|
||||||
bindSearch();
|
safeBoot("绑定搜索", bindSearch);
|
||||||
bindStudio();
|
safeBoot("绑定 Skill Studio", bindStudio);
|
||||||
renderSidebar();
|
safeBoot("渲染侧栏", renderSidebar);
|
||||||
renderChat();
|
safeBoot("渲染对话", renderChat);
|
||||||
renderAgents();
|
safeBoot("渲染智能体", renderAgents);
|
||||||
restoreActiveTab();
|
safeBoot("恢复页面", restoreActiveTab);
|
||||||
|
refreshUiConfig({ migrateLocalAgents: true }).catch((error) => {
|
||||||
|
console.warn("[ui-config] fallback to local config", error);
|
||||||
|
setSharedConfigStatus("共享配置读取失败,本机仍可临时使用: " + (error.message || error), true);
|
||||||
|
});
|
||||||
pingBackend();
|
pingBackend();
|
||||||
fetchIP();
|
fetchIP();
|
||||||
setInterval(pingBackend, 30000);
|
setInterval(pingBackend, 30000);
|
||||||
@@ -178,6 +226,135 @@ function syncModelPick(modelValue, providerValue = "") {
|
|||||||
updateModelDisplay(model, providerValue);
|
updateModelDisplay(model, providerValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeId(prefix) {
|
||||||
|
return prefix + "_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeModelProfile(raw = {}) {
|
||||||
|
const model = (raw.model || raw.default || "").trim();
|
||||||
|
return {
|
||||||
|
id: raw.id || makeId("model"),
|
||||||
|
name: (raw.name || model || "模型接入").trim(),
|
||||||
|
provider: (raw.provider || "openrouter").trim(),
|
||||||
|
model,
|
||||||
|
baseUrl: (raw.baseUrl || raw.base_url || "").trim(),
|
||||||
|
apiKeyRef: (raw.apiKeyRef || raw.api_key_ref || "").trim(),
|
||||||
|
enabled: raw.enabled !== false,
|
||||||
|
isDefault: !!raw.isDefault,
|
||||||
|
createdAt: Number(raw.createdAt || Date.now()),
|
||||||
|
updatedAt: Number(raw.updatedAt || Date.now()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function defaultModelProfile() {
|
||||||
|
const model = state.model || DEFAULT_MODEL_ID;
|
||||||
|
return normalizeModelProfile({
|
||||||
|
id: "runtime-default",
|
||||||
|
name: "线上默认模型",
|
||||||
|
provider: "openrouter",
|
||||||
|
model,
|
||||||
|
apiKeyRef: "服务器环境变量",
|
||||||
|
enabled: true,
|
||||||
|
isDefault: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeModelProfiles(list) {
|
||||||
|
const seen = new Set();
|
||||||
|
const profiles = (Array.isArray(list) ? list : [])
|
||||||
|
.map(normalizeModelProfile)
|
||||||
|
.filter(profile => profile.model && !seen.has(profile.id) && seen.add(profile.id));
|
||||||
|
if (!profiles.length) profiles.push(defaultModelProfile());
|
||||||
|
if (!profiles.some(profile => profile.isDefault)) profiles[0].isDefault = true;
|
||||||
|
let defaultSeen = false;
|
||||||
|
for (const profile of profiles) {
|
||||||
|
if (profile.isDefault && !defaultSeen) defaultSeen = true;
|
||||||
|
else profile.isDefault = false;
|
||||||
|
}
|
||||||
|
return profiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeModelProfile() {
|
||||||
|
return state.modelProfiles.find(profile => profile.isDefault && profile.enabled)
|
||||||
|
|| state.modelProfiles.find(profile => profile.enabled)
|
||||||
|
|| state.modelProfiles[0]
|
||||||
|
|| defaultModelProfile();
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelProfileById(id) {
|
||||||
|
return state.modelProfiles.find(profile => profile.id === id) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelForAgent(agent) {
|
||||||
|
const profile = modelProfileById(agent?.modelProfileId);
|
||||||
|
return profile?.model || agent?.model || state.model || DEFAULT_MODEL_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function modelLabelForAgent(agent) {
|
||||||
|
const profile = modelProfileById(agent?.modelProfileId);
|
||||||
|
if (profile) return profile.name + " · " + profile.model;
|
||||||
|
return agent?.model || state.model || DEFAULT_MODEL_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncModelOptionsFromProfiles() {
|
||||||
|
for (const profile of state.modelProfiles) {
|
||||||
|
ensureModelChoice(profile.model, profile.name || profile.model);
|
||||||
|
}
|
||||||
|
const active = activeModelProfile();
|
||||||
|
if (active?.model) {
|
||||||
|
syncModelPick(active.model, active.provider);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function upsertRuntimeModelProfile(model) {
|
||||||
|
if (!model?.default && !model?.model) return;
|
||||||
|
const modelId = model.default || model.model;
|
||||||
|
const existing = state.modelProfiles.find(profile => profile.isDefault)
|
||||||
|
|| state.modelProfiles.find(profile => profile.id === "runtime-default");
|
||||||
|
const next = normalizeModelProfile({
|
||||||
|
...(existing || {}),
|
||||||
|
id: existing?.id || "runtime-default",
|
||||||
|
name: existing?.name || "线上默认模型",
|
||||||
|
provider: model.provider || existing?.provider || "openrouter",
|
||||||
|
model: modelId,
|
||||||
|
baseUrl: model.base_url || model.baseUrl || existing?.baseUrl || "",
|
||||||
|
apiKeyRef: existing?.apiKeyRef || "服务器环境变量",
|
||||||
|
enabled: true,
|
||||||
|
isDefault: true,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
|
state.modelProfiles = normalizeModelProfiles([
|
||||||
|
next,
|
||||||
|
...state.modelProfiles.filter(profile => profile.id !== next.id).map(profile => ({ ...profile, isDefault: false })),
|
||||||
|
]);
|
||||||
|
syncModelOptionsFromProfiles();
|
||||||
|
renderModelProfiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderAgentModelProfileOptions(selectedId = "") {
|
||||||
|
const select = document.getElementById("agentModelProfile");
|
||||||
|
if (!select) return;
|
||||||
|
const profiles = normalizeModelProfiles(state.modelProfiles);
|
||||||
|
select.innerHTML = '<option value="">跟随对话默认 / 仅使用模型 ID</option>';
|
||||||
|
for (const profile of profiles) {
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = profile.id;
|
||||||
|
option.textContent = `${profile.name} · ${profile.model}`;
|
||||||
|
select.appendChild(option);
|
||||||
|
}
|
||||||
|
select.value = selectedId && profiles.some(profile => profile.id === selectedId) ? selectedId : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyAgentModelProfileSelection() {
|
||||||
|
const select = document.getElementById("agentModelProfile");
|
||||||
|
const input = document.getElementById("agentModel");
|
||||||
|
const profile = modelProfileById(select?.value || "");
|
||||||
|
if (profile?.model && input) {
|
||||||
|
input.value = profile.model;
|
||||||
|
ensureModelChoice(profile.model, profile.name || profile.model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 会话持久化 ----------
|
// ---------- 会话持久化 ----------
|
||||||
function loadConversations() {
|
function loadConversations() {
|
||||||
try {
|
try {
|
||||||
@@ -679,8 +856,12 @@ async function refreshHermesConfig(force = false) {
|
|||||||
},
|
},
|
||||||
mcp_servers_yaml: config.mcp_servers_yaml || "",
|
mcp_servers_yaml: config.mcp_servers_yaml || "",
|
||||||
};
|
};
|
||||||
if (model.default) syncModelPick(model.default, model.provider || "");
|
if (model.default) {
|
||||||
else updateModelDisplay(state.model, model.provider || "");
|
syncModelPick(model.default, model.provider || "");
|
||||||
|
if (!state.modelProfiles.length) upsertRuntimeModelProfile(model);
|
||||||
|
} else {
|
||||||
|
updateModelDisplay(state.model, model.provider || "");
|
||||||
|
}
|
||||||
_hermesConfigLoaded = true;
|
_hermesConfigLoaded = true;
|
||||||
const suffix = config.lxc ? " · " + config.lxc : "";
|
const suffix = config.lxc ? " · " + config.lxc : "";
|
||||||
setHermesModelStatus("已读取模型配置" + suffix);
|
setHermesModelStatus("已读取模型配置" + suffix);
|
||||||
@@ -723,6 +904,128 @@ async function postHermesRuntimeConfig(payload) {
|
|||||||
return data.config || {};
|
return data.config || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setSharedConfigStatus(text, isError = false) {
|
||||||
|
setSettingsStatus("sharedConfigStatus", text, isError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function agentsForSharedConfig() {
|
||||||
|
return sortedAgents().map(agent => ({
|
||||||
|
id: agent.id,
|
||||||
|
emoji: agent.emoji || "🤖",
|
||||||
|
name: agent.name || "",
|
||||||
|
desc: agent.desc || "",
|
||||||
|
model: agent.model || "",
|
||||||
|
modelProfileId: agent.modelProfileId || "",
|
||||||
|
systemPrompt: agent.systemPrompt || "",
|
||||||
|
skills: Array.isArray(agent.skills) ? agent.skills : [],
|
||||||
|
stages: agent.stages || null,
|
||||||
|
createdAt: agent.createdAt || Date.now(),
|
||||||
|
updatedAt: agent.updatedAt || Date.now(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySharedAgents(agents) {
|
||||||
|
if (!Array.isArray(agents) || !agents.length) return false;
|
||||||
|
const next = {};
|
||||||
|
for (const agent of agents) {
|
||||||
|
if (!agent?.id || !agent?.name || !agent?.systemPrompt) continue;
|
||||||
|
next[agent.id] = {
|
||||||
|
id: agent.id,
|
||||||
|
emoji: agent.emoji || "🤖",
|
||||||
|
name: agent.name,
|
||||||
|
desc: agent.desc || "",
|
||||||
|
model: agent.model || "",
|
||||||
|
modelProfileId: agent.modelProfileId || "",
|
||||||
|
systemPrompt: agent.systemPrompt,
|
||||||
|
skills: Array.isArray(agent.skills) ? agent.skills : [],
|
||||||
|
stages: agent.stages || null,
|
||||||
|
createdAt: agent.createdAt || Date.now(),
|
||||||
|
updatedAt: agent.updatedAt || Date.now(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (!Object.keys(next).length) return false;
|
||||||
|
state.agents = next;
|
||||||
|
saveAgents();
|
||||||
|
renderAgents();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUiConfig() {
|
||||||
|
const res = await fetch(UI_CONFIG_ENDPOINT, {
|
||||||
|
credentials: "same-origin",
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status));
|
||||||
|
return data.config || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSharedConfig(options = {}) {
|
||||||
|
const config = {
|
||||||
|
version: 1,
|
||||||
|
modelProfiles: normalizeModelProfiles(state.modelProfiles),
|
||||||
|
agents: agentsForSharedConfig(),
|
||||||
|
};
|
||||||
|
const res = await fetch(UI_CONFIG_ENDPOINT, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ config }),
|
||||||
|
});
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
if (!res.ok || data.code !== 0) throw new Error(data.msg || ("HTTP " + res.status));
|
||||||
|
const saved = data.config || config;
|
||||||
|
state.modelProfiles = normalizeModelProfiles(saved.modelProfiles);
|
||||||
|
state.sharedConfigLoaded = true;
|
||||||
|
state.sharedConfigAvailable = true;
|
||||||
|
syncModelOptionsFromProfiles();
|
||||||
|
renderModelProfiles();
|
||||||
|
renderAgentModelProfileOptions(document.getElementById("agentModelProfile")?.value || "");
|
||||||
|
if (!options.silent) toast("共享配置已保存");
|
||||||
|
setSharedConfigStatus("共享配置已保存到服务器");
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistAgents(options = {}) {
|
||||||
|
saveAgents();
|
||||||
|
renderAgents();
|
||||||
|
if (!state.sharedConfigAvailable) {
|
||||||
|
if (!options.silent) toast("已保存到本机;共享配置暂不可用");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await saveSharedConfig({ silent: true });
|
||||||
|
if (!options.silent) toast("智能体已保存到服务器");
|
||||||
|
} catch (error) {
|
||||||
|
setSharedConfigStatus("智能体共享保存失败: " + (error.message || error), true);
|
||||||
|
if (!options.silent) toast("共享保存失败,已保留在本机");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshUiConfig(options = {}) {
|
||||||
|
try {
|
||||||
|
const config = await fetchUiConfig();
|
||||||
|
const profiles = normalizeModelProfiles(config.modelProfiles);
|
||||||
|
state.modelProfiles = profiles;
|
||||||
|
state.sharedConfigLoaded = true;
|
||||||
|
state.sharedConfigAvailable = true;
|
||||||
|
syncModelOptionsFromProfiles();
|
||||||
|
renderModelProfiles();
|
||||||
|
renderAgentModelProfileOptions(document.getElementById("agentModelProfile")?.value || "");
|
||||||
|
|
||||||
|
const hadServerAgents = applySharedAgents(config.agents);
|
||||||
|
if (!hadServerAgents && options.migrateLocalAgents && Object.keys(state.agents || {}).length) {
|
||||||
|
await saveSharedConfig({ silent: true });
|
||||||
|
setSharedConfigStatus("已把本机智能体迁移到服务器共享配置");
|
||||||
|
} else {
|
||||||
|
setSharedConfigStatus("已读取服务器共享配置" + (config.lxc ? " · " + config.lxc : ""));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
state.sharedConfigAvailable = false;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveModelConfig() {
|
async function saveModelConfig() {
|
||||||
const model = readModelConfigFields();
|
const model = readModelConfigFields();
|
||||||
if (!model.default) {
|
if (!model.default) {
|
||||||
@@ -754,6 +1057,10 @@ async function saveModelConfig() {
|
|||||||
mcp_servers_yaml: _hermesConfigSnapshot ? _hermesConfigSnapshot.mcp_servers_yaml : (saved.mcp_servers_yaml || ""),
|
mcp_servers_yaml: _hermesConfigSnapshot ? _hermesConfigSnapshot.mcp_servers_yaml : (saved.mcp_servers_yaml || ""),
|
||||||
};
|
};
|
||||||
_hermesConfigLoaded = false;
|
_hermesConfigLoaded = false;
|
||||||
|
upsertRuntimeModelProfile(savedModel.default ? savedModel : model);
|
||||||
|
saveSharedConfig({ silent: true }).catch((error) => {
|
||||||
|
setSharedConfigStatus("模型 Profile 共享保存失败: " + (error.message || error), true);
|
||||||
|
});
|
||||||
setHermesModelStatus("模型配置已保存并重启 · 备份 " + (saved.backup || "已创建"));
|
setHermesModelStatus("模型配置已保存并重启 · 备份 " + (saved.backup || "已创建"));
|
||||||
toast("AI 模型接入配置已生效");
|
toast("AI 模型接入配置已生效");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -773,11 +1080,6 @@ async function saveModelConfig() {
|
|||||||
|
|
||||||
async function saveMcpConfig() {
|
async function saveMcpConfig() {
|
||||||
const mcpServersYaml = document.getElementById("mcpServersYaml")?.value || "";
|
const mcpServersYaml = document.getElementById("mcpServersYaml")?.value || "";
|
||||||
const model = snapshotModelOrFields();
|
|
||||||
if (!model.default) {
|
|
||||||
toast("先读取或填写默认模型");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!confirm("保存 MCP 工具接入配置后会重启线上 Hermes agent,当前正在生成的任务可能中断。继续吗?")) return;
|
if (!confirm("保存 MCP 工具接入配置后会重启线上 Hermes agent,当前正在生成的任务可能中断。继续吗?")) return;
|
||||||
const btn = document.getElementById("hermesMcpSaveBtn");
|
const btn = document.getElementById("hermesMcpSaveBtn");
|
||||||
const oldHTML = btn?.innerHTML;
|
const oldHTML = btn?.innerHTML;
|
||||||
@@ -788,11 +1090,10 @@ async function saveMcpConfig() {
|
|||||||
setHermesMcpStatus("正在写入 mcp_servers 配置并重启 Hermes agent...");
|
setHermesMcpStatus("正在写入 mcp_servers 配置并重启 Hermes agent...");
|
||||||
try {
|
try {
|
||||||
const saved = await postHermesRuntimeConfig({
|
const saved = await postHermesRuntimeConfig({
|
||||||
model,
|
|
||||||
mcp_servers_yaml: mcpServersYaml,
|
mcp_servers_yaml: mcpServersYaml,
|
||||||
restart: true,
|
restart: true,
|
||||||
});
|
});
|
||||||
const savedModel = saved.model || model;
|
const savedModel = saved.model || snapshotModelOrFields();
|
||||||
if (savedModel.default) syncModelPick(savedModel.default, savedModel.provider || model.provider || "");
|
if (savedModel.default) syncModelPick(savedModel.default, savedModel.provider || model.provider || "");
|
||||||
document.getElementById("mcpServersYaml").value = saved.mcp_servers_yaml || "";
|
document.getElementById("mcpServersYaml").value = saved.mcp_servers_yaml || "";
|
||||||
_hermesConfigSnapshot = {
|
_hermesConfigSnapshot = {
|
||||||
@@ -905,7 +1206,7 @@ async function sendMessage(text) {
|
|||||||
if (useAgent) {
|
if (useAgent) {
|
||||||
const sys = composeSystemPrompt(useAgent);
|
const sys = composeSystemPrompt(useAgent);
|
||||||
if (sys) msgsForApi = [{ role: "system", content: sys }, ...msgsForApi];
|
if (sys) msgsForApi = [{ role: "system", content: sys }, ...msgsForApi];
|
||||||
modelForApi = useAgent.model || state.model;
|
modelForApi = modelForAgent(useAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 本次使用完清掉 pendingAgent
|
// 本次使用完清掉 pendingAgent
|
||||||
@@ -2072,6 +2373,7 @@ function loadAgents() {
|
|||||||
migrated = true;
|
migrated = true;
|
||||||
}
|
}
|
||||||
ensureModelChoice(agent.model || DEFAULT_MODEL_ID);
|
ensureModelChoice(agent.model || DEFAULT_MODEL_ID);
|
||||||
|
if (!("modelProfileId" in agent)) agent.modelProfileId = "";
|
||||||
}
|
}
|
||||||
if (migrated) saveAgents();
|
if (migrated) saveAgents();
|
||||||
}
|
}
|
||||||
@@ -2112,7 +2414,7 @@ function renderAgents() {
|
|||||||
`;
|
`;
|
||||||
card.querySelector(".agent-avatar").textContent = a.emoji || "🤖";
|
card.querySelector(".agent-avatar").textContent = a.emoji || "🤖";
|
||||||
card.querySelector(".agent-name").textContent = a.name;
|
card.querySelector(".agent-name").textContent = a.name;
|
||||||
card.querySelector(".agent-model").textContent = a.model;
|
card.querySelector(".agent-model").textContent = modelLabelForAgent(a);
|
||||||
card.querySelector(".agent-desc").textContent = a.desc || "(无简介)";
|
card.querySelector(".agent-desc").textContent = a.desc || "(无简介)";
|
||||||
const sEl = card.querySelector(".agent-skills");
|
const sEl = card.querySelector(".agent-skills");
|
||||||
const skills = (a.skills || []).map(skillById).filter(Boolean);
|
const skills = (a.skills || []).map(skillById).filter(Boolean);
|
||||||
@@ -2321,6 +2623,7 @@ function openAgentModal(id) {
|
|||||||
document.getElementById("agentDesc").value = a.desc || "";
|
document.getElementById("agentDesc").value = a.desc || "";
|
||||||
ensureModelChoice(a.model || DEFAULT_MODEL_ID);
|
ensureModelChoice(a.model || DEFAULT_MODEL_ID);
|
||||||
document.getElementById("agentModel").value = a.model || DEFAULT_MODEL_ID;
|
document.getElementById("agentModel").value = a.model || DEFAULT_MODEL_ID;
|
||||||
|
renderAgentModelProfileOptions(a.modelProfileId || "");
|
||||||
document.getElementById("agentPrompt").value = a.systemPrompt || "";
|
document.getElementById("agentPrompt").value = a.systemPrompt || "";
|
||||||
renderSkillsPicker(a.skills || []);
|
renderSkillsPicker(a.skills || []);
|
||||||
} else {
|
} else {
|
||||||
@@ -2329,6 +2632,7 @@ function openAgentModal(id) {
|
|||||||
document.getElementById("agentName").value = "";
|
document.getElementById("agentName").value = "";
|
||||||
document.getElementById("agentDesc").value = "";
|
document.getElementById("agentDesc").value = "";
|
||||||
document.getElementById("agentModel").value = state.model || DEFAULT_MODEL_ID;
|
document.getElementById("agentModel").value = state.model || DEFAULT_MODEL_ID;
|
||||||
|
renderAgentModelProfileOptions(activeModelProfile()?.id || "");
|
||||||
document.getElementById("agentPrompt").value = "";
|
document.getElementById("agentPrompt").value = "";
|
||||||
renderSkillsPicker([]);
|
renderSkillsPicker([]);
|
||||||
}
|
}
|
||||||
@@ -2344,6 +2648,7 @@ function saveAgent() {
|
|||||||
const name = document.getElementById("agentName").value.trim();
|
const name = document.getElementById("agentName").value.trim();
|
||||||
const desc = document.getElementById("agentDesc").value.trim();
|
const desc = document.getElementById("agentDesc").value.trim();
|
||||||
const model = document.getElementById("agentModel").value.trim();
|
const model = document.getElementById("agentModel").value.trim();
|
||||||
|
const modelProfileId = document.getElementById("agentModelProfile")?.value || "";
|
||||||
const systemPrompt = document.getElementById("agentPrompt").value.trim();
|
const systemPrompt = document.getElementById("agentPrompt").value.trim();
|
||||||
if (!name) { toast("请填写名称"); return; }
|
if (!name) { toast("请填写名称"); return; }
|
||||||
if (!model) { toast("请填写模型 ID"); return; }
|
if (!model) { toast("请填写模型 ID"); return; }
|
||||||
@@ -2353,21 +2658,29 @@ function saveAgent() {
|
|||||||
const skills = readSkillsPicker();
|
const skills = readSkillsPicker();
|
||||||
|
|
||||||
if (state.editingAgentId && state.agents[state.editingAgentId]) {
|
if (state.editingAgentId && state.agents[state.editingAgentId]) {
|
||||||
Object.assign(state.agents[state.editingAgentId], { emoji, name, desc, model, systemPrompt, skills });
|
Object.assign(state.agents[state.editingAgentId], {
|
||||||
|
emoji,
|
||||||
|
name,
|
||||||
|
desc,
|
||||||
|
model,
|
||||||
|
modelProfileId,
|
||||||
|
systemPrompt,
|
||||||
|
skills,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const id = "a_" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
const id = makeId("a");
|
||||||
state.agents[id] = { id, emoji, name, desc, model, systemPrompt, skills, createdAt: Date.now() };
|
state.agents[id] = { id, emoji, name, desc, model, modelProfileId, systemPrompt, skills, createdAt: Date.now(), updatedAt: Date.now() };
|
||||||
}
|
}
|
||||||
saveAgents();
|
persistAgents({ silent: true });
|
||||||
renderAgents();
|
|
||||||
closeAgentModal();
|
closeAgentModal();
|
||||||
toast("已保存");
|
toast(state.sharedConfigAvailable ? "已保存到服务器" : "已保存到本机");
|
||||||
}
|
}
|
||||||
function deleteAgent(id) {
|
function deleteAgent(id) {
|
||||||
if (!confirm("删除这个智能体?已有的对话不受影响。")) return;
|
if (!confirm("删除这个智能体?已有的对话不受影响。")) return;
|
||||||
delete state.agents[id];
|
delete state.agents[id];
|
||||||
saveAgents();
|
persistAgents({ silent: true });
|
||||||
renderAgents();
|
toast(state.sharedConfigAvailable ? "已从服务器删除" : "已从本机删除");
|
||||||
}
|
}
|
||||||
function chatWithAgent(id) {
|
function chatWithAgent(id) {
|
||||||
const a = state.agents[id];
|
const a = state.agents[id];
|
||||||
@@ -2456,7 +2769,7 @@ async function runClusterOne(agent, prompt, col) {
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Authorization": "Bearer " + state.apiKey,
|
"Authorization": "Bearer " + state.apiKey,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ model: agent.model, messages, stream: false }),
|
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();
|
||||||
|
|||||||
Reference in New Issue
Block a user