Make prewarm resync MCP tools and rebuild frozen wire cache.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-08-12 21:34:45 +08:00
parent 25d09d78ce
commit 3785e78a72
10 changed files with 253 additions and 110 deletions

View file

@ -111,88 +111,22 @@ def _expand_mcp_entry_args(raw: list[Any] | None) -> list[str]:
def _mcp_health_and_sync_one(store: SqliteStore, row: dict[str, Any]) -> dict[str, Any] | None:
"""Run MCP initialize health + tools/list + persist tools (same semantics as check-all per row)."""
sid = str(row.get("server_id") or "").strip()
cmd = str(row.get("entry_command") or "").strip()
args = [str(x) for x in (row.get("entry_args") or []) if str(x).strip()]
if not sid:
return None
if not cmd:
item: dict[str, Any] = {
"server_id": sid,
"ok": False,
"error_code": "mcp_entry_missing",
"error": "entry_command_missing",
"health": {"ok": False, "error_code": "mcp_entry_missing", "error": "entry_command_missing"},
"tools_synced": 0,
}
store.set_mcp_server_health(server_id=sid, status="error", detail=item["health"])
return item
if _is_bailian_webparser_remote(entry_command=cmd, entry_args=args):
tools = _bailian_webparser_virtual_tools()
store.replace_mcp_server_tools(server_id=sid, tools=tools)
detail = {"synced_tools": len(tools), "compat_mode": "bailian_webparser"}
store.set_mcp_server_health(server_id=sid, status="ok", detail=detail)
return {"server_id": sid, "ok": True, "health": detail, "tools_synced": len(tools)}
rt = mcp_runtime_for_row(row, store=store)
try:
health = rt.health()
health_ok = bool(health.get("ok"))
if not health_ok:
item = {
"server_id": sid,
"ok": False,
"error_code": str(health.get("error_code") or "mcp_healthcheck_failed"),
"error": str(health.get("error") or "healthcheck_failed"),
"health": health,
"tools_synced": 0,
}
store.set_mcp_server_health(server_id=sid, status="error", detail=health)
return item
tools_res = rt.tools_list()
if not bool(tools_res.get("ok")):
item = {
"server_id": sid,
"ok": False,
"error_code": str(tools_res.get("error_code") or "mcp_tools_list_invalid"),
"error": str(tools_res.get("error") or "tools_list_failed"),
"health": health,
"tools_synced": 0,
}
store.set_mcp_server_health(server_id=sid, status="error", detail=tools_res)
return item
tools = tools_res.get("tools") if isinstance(tools_res.get("tools"), list) else []
store.replace_mcp_server_tools(server_id=sid, tools=tools if isinstance(tools, list) else [])
store.set_mcp_server_health(server_id=sid, status="ok", detail={"synced_tools": len(tools)})
return {"server_id": sid, "ok": True, "health": health, "tools_synced": len(tools)}
finally:
rt.stop()
"""Persist MCP tools/list for one server. Health is best-effort and does not gate sync."""
from runtime.tools.mcp.sync_tools import sync_mcp_server_tools
return sync_mcp_server_tools(store, row)
def _is_bailian_webparser_remote(*, entry_command: str, entry_args: list[str]) -> bool:
cmd = str(entry_command or "").strip().lower()
if cmd not in {"npx", "npx.cmd", "node"}:
return False
joined = " ".join(str(x or "").strip().lower() for x in (entry_args or []))
return "mcp-remote" in joined and "/api/v1/mcps/webparser/sse" in joined
from runtime.tools.mcp.sync_tools import is_bailian_webparser_remote
return is_bailian_webparser_remote(entry_command=entry_command, entry_args=entry_args)
def _bailian_webparser_virtual_tools() -> list[dict[str, Any]]:
return [
{
"tool_name": "bailian_webparser_parse",
"description": "Parse webpage via DashScope WebParser compatibility mode. Requires `url` (http/https).",
"parameters": {
"type": "object",
"properties": {
"url": {"type": "string", "description": "Target webpage URL (required). Example: https://example.com"},
"timeout": {"type": "integer", "default": 35, "minimum": 8, "maximum": 90},
},
"required": ["url"],
"additionalProperties": False,
},
}
]
from runtime.tools.mcp.sync_tools import bailian_webparser_virtual_tools
return bailian_webparser_virtual_tools()
def _http_get_json(url: str, *, timeout: float = 8.0) -> dict[str, Any]:

View file

@ -5,9 +5,9 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>oliver</title>
<!-- Avoid render-blocking external fonts (can stall first paint ~30s on blocked networks). -->
<link rel="stylesheet" href="/admin/assets/styles.css?v=20260812-21" />
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css?v=20260812-21" />
<link rel="stylesheet" href="/admin/assets/theme-presets.css?v=20260812-21" />
<link rel="stylesheet" href="/admin/assets/styles.css?v=20260812-22" />
<link rel="stylesheet" href="/admin/assets/theme-deepseek.css?v=20260812-22" />
<link rel="stylesheet" href="/admin/assets/theme-presets.css?v=20260812-22" />
<style>
/* First paint before app.js runs: keep admin from showing a blank dark screen. */
.admin-boot-splash {
@ -129,33 +129,33 @@
<script type="importmap">
{
"imports": {
"/admin/assets/app.js": "/admin/assets/app.js?v=20260812-21",
"/admin/assets/js/asset-v.js": "/admin/assets/js/asset-v.js?v=20260812-21",
"/admin/assets/js/core.js": "/admin/assets/js/core.js?v=20260812-21",
"/admin/assets/js/i18n/index.js": "/admin/assets/js/i18n/index.js?v=20260812-21",
"/admin/assets/js/i18n/messages-en.js": "/admin/assets/js/i18n/messages-en.js?v=20260812-21",
"/admin/assets/js/i18n/messages-zh.js": "/admin/assets/js/i18n/messages-zh.js?v=20260812-21",
"/admin/assets/js/main.js": "/admin/assets/js/main.js?v=20260812-21",
"/admin/assets/js/pages/attachments.js": "/admin/assets/js/pages/attachments.js?v=20260812-21",
"/admin/assets/js/pages/audit.js": "/admin/assets/js/pages/audit.js?v=20260812-21",
"/admin/assets/js/pages/authz.js": "/admin/assets/js/pages/authz.js?v=20260812-21",
"/admin/assets/js/pages/legacy-admin.js": "/admin/assets/js/pages/legacy-admin.js?v=20260812-21",
"/admin/assets/js/pages/legacy.js": "/admin/assets/js/pages/legacy.js?v=20260812-21",
"/admin/assets/js/pages/login.js": "/admin/assets/js/pages/login.js?v=20260812-21",
"/admin/assets/js/pages/memory.js": "/admin/assets/js/pages/memory.js?v=20260812-21",
"/admin/assets/js/pages/models.js": "/admin/assets/js/pages/models.js?v=20260812-21",
"/admin/assets/js/pages/plugins.js": "/admin/assets/js/pages/plugins.js?v=20260812-21",
"/admin/assets/js/pages/profile.js": "/admin/assets/js/pages/profile.js?v=20260812-21",
"/admin/assets/js/pages/scheduled-jobs.js": "/admin/assets/js/pages/scheduled-jobs.js?v=20260812-21",
"/admin/assets/js/pages/skills.js": "/admin/assets/js/pages/skills.js?v=20260812-21",
"/admin/assets/js/pages/stack.js": "/admin/assets/js/pages/stack.js?v=20260812-21",
"/admin/assets/js/pages/users.js": "/admin/assets/js/pages/users.js?v=20260812-21",
"/admin/assets/js/pages/workspace-paths.js": "/admin/assets/js/pages/workspace-paths.js?v=20260812-21",
"/admin/assets/js/router.js": "/admin/assets/js/router.js?v=20260812-21",
"/admin/assets/js/state.js": "/admin/assets/js/state.js?v=20260812-21"
"/admin/assets/app.js": "/admin/assets/app.js?v=20260812-22",
"/admin/assets/js/asset-v.js": "/admin/assets/js/asset-v.js?v=20260812-22",
"/admin/assets/js/core.js": "/admin/assets/js/core.js?v=20260812-22",
"/admin/assets/js/i18n/index.js": "/admin/assets/js/i18n/index.js?v=20260812-22",
"/admin/assets/js/i18n/messages-en.js": "/admin/assets/js/i18n/messages-en.js?v=20260812-22",
"/admin/assets/js/i18n/messages-zh.js": "/admin/assets/js/i18n/messages-zh.js?v=20260812-22",
"/admin/assets/js/main.js": "/admin/assets/js/main.js?v=20260812-22",
"/admin/assets/js/pages/attachments.js": "/admin/assets/js/pages/attachments.js?v=20260812-22",
"/admin/assets/js/pages/audit.js": "/admin/assets/js/pages/audit.js?v=20260812-22",
"/admin/assets/js/pages/authz.js": "/admin/assets/js/pages/authz.js?v=20260812-22",
"/admin/assets/js/pages/legacy-admin.js": "/admin/assets/js/pages/legacy-admin.js?v=20260812-22",
"/admin/assets/js/pages/legacy.js": "/admin/assets/js/pages/legacy.js?v=20260812-22",
"/admin/assets/js/pages/login.js": "/admin/assets/js/pages/login.js?v=20260812-22",
"/admin/assets/js/pages/memory.js": "/admin/assets/js/pages/memory.js?v=20260812-22",
"/admin/assets/js/pages/models.js": "/admin/assets/js/pages/models.js?v=20260812-22",
"/admin/assets/js/pages/plugins.js": "/admin/assets/js/pages/plugins.js?v=20260812-22",
"/admin/assets/js/pages/profile.js": "/admin/assets/js/pages/profile.js?v=20260812-22",
"/admin/assets/js/pages/scheduled-jobs.js": "/admin/assets/js/pages/scheduled-jobs.js?v=20260812-22",
"/admin/assets/js/pages/skills.js": "/admin/assets/js/pages/skills.js?v=20260812-22",
"/admin/assets/js/pages/stack.js": "/admin/assets/js/pages/stack.js?v=20260812-22",
"/admin/assets/js/pages/users.js": "/admin/assets/js/pages/users.js?v=20260812-22",
"/admin/assets/js/pages/workspace-paths.js": "/admin/assets/js/pages/workspace-paths.js?v=20260812-22",
"/admin/assets/js/router.js": "/admin/assets/js/router.js?v=20260812-22",
"/admin/assets/js/state.js": "/admin/assets/js/state.js?v=20260812-22"
}
}
</script>
<script type="module" src="/admin/assets/app.js?v=20260812-21" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load admin scripts',h:'Confirm /admin/assets/app.js and /admin/assets/js/ are reachable.'}:{t:'无法加载管理台脚本',h:'请确认 /admin/assets/app.js 与 /admin/assets/js/ 可访问。'};var s=document.getElementById('admin-boot-splash');if(s){var a=s.querySelector('.admin-boot-splash__title span:last-child');var b=s.querySelector('.admin-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
<script type="module" src="/admin/assets/app.js?v=20260812-22" onerror="(function(){var lang='zh';try{lang=String(localStorage.getItem('ops_admin_lang')||'zh').toLowerCase();}catch(e){}if(lang!=='en')lang='zh';var msg=lang==='en'?{t:'Failed to load admin scripts',h:'Confirm /admin/assets/app.js and /admin/assets/js/ are reachable.'}:{t:'无法加载管理台脚本',h:'请确认 /admin/assets/app.js 与 /admin/assets/js/ 可访问。'};var s=document.getElementById('admin-boot-splash');if(s){var a=s.querySelector('.admin-boot-splash__title span:last-child');var b=s.querySelector('.admin-boot-splash__muted');if(a)a.textContent=msg.t;if(b)b.textContent=msg.h;}})()"></script>
</body>
</html>

View file

@ -3,4 +3,4 @@
* - interfaces/admin/static/index.html importmap + CSS/JS ?v=
* - interfaces/admin/static/app.js entry query (optional if importmap covers it)
*/
export const ADMIN_ASSET_V = "20260812-21";
export const ADMIN_ASSET_V = "20260812-22";

View file

@ -76,7 +76,7 @@ export default {
"auth.logout": "Logout",
"auth.password": "Password",
"auth.username": "Username",
"auto.after_any_skill_tool_role_prompt_change_run_prewarm_imme": "After any skill/tool/role/prompt change, run prewarm immediately; restart for complex changes. System also auto-prewarms every 10 minutes.",
"auto.after_any_skill_tool_role_prompt_change_run_prewarm_imme": "After any skill/tool/role/prompt/MCP change, run prewarm immediately (syncs enabled MCP tools/list, then freezes the tool wire); restart for complex changes. System also auto-prewarms every 10 minutes.",
"auto.prewarm_running_in_background_refreshing_this_page_when_": "[prewarm] running in background; refreshing this page when finished…",
"boot.loadFailed": "Failed to load admin scripts",
"boot.loadFailedHint": "Confirm /admin/assets/app.js and /admin/assets/js/ are reachable.",

View file

@ -76,7 +76,7 @@ export default {
"auth.logout": "退出登录",
"auth.password": "密码",
"auth.username": "用户名",
"auto.after_any_skill_tool_role_prompt_change_run_prewarm_imme": "任何 skill/tool/角色/提示词变更后,请立即预热;复杂变更可直接重启。系统每10分钟自动异步预热一次。",
"auto.after_any_skill_tool_role_prompt_change_run_prewarm_imme": "任何 skill/tool/角色/提示词/MCP 变更后,请立即预热(会先同步已启用 MCP 的 tools/list,再冻结核线);复杂变更可直接重启。系统每10分钟自动异步预热一次。",
"auto.prewarm_running_in_background_refreshing_this_page_when_": "[预热] 后台执行中,完成后自动刷新本页数据(含提示词)…",
"boot.loadFailed": "无法加载管理台脚本",
"boot.loadFailedHint": "请确认 /admin/assets/app.js 与 /admin/assets/js/ 可访问。",