From 8bda11f683d5277ffb5f513841fe3a1fd6618814 Mon Sep 17 00:00:00 2001 From: caoqianming Date: Tue, 11 Aug 2026 08:58:30 +0800 Subject: [PATCH] =?UTF-8?q?feat(external-systems):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=AB=AF=E9=85=8D=E7=BD=AE=E8=A1=A8=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 ++ PROGRESS.md | 6 +- core/__init__.py | 2 +- web/static/admin.html | 27 ++++++++- web/static/js/admin.js | 132 +++++++++++++++++++++++++++++------------ 5 files changed, 131 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6eb539..8e24e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ > 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。 > 工程口径的完整记录见 `PROGRESS.md` / git log。 +## 0.63.5 — 2026-08-11 + +- 管理员配置外部系统时改用独立弹框,并会根据 OpenAPI、MCP 和认证方式只展示对应配置项;外部系统列表同步显示各连接类型真实的能力模式。 + ## 0.63.4 — 2026-08-10 - 外部系统管理统一为通用 OpenAPI 和 MCP 配置,不再需要选择特定 MES 类型;已有 MES 定义可通过随版本提供的一次性脚本转换,用户连接和加密凭据保持不变。 diff --git a/PROGRESS.md b/PROGRESS.md index fe0b8d0..8c3c6aa 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -2,7 +2,7 @@ > 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。 -最后更新:2026-08-10(外部系统统一为通用 OpenAPI/MCP,bump 0.63.4) +最后更新:2026-08-11(外部系统配置弹框与动态字段,bump 0.63.5) --- @@ -21,6 +21,10 @@ ## 已完成关键能力 +### 2026-08-11 + +- **08-11 / 0.63.5 / 外部系统配置弹框 + 动态字段**:管理后台外部系统定义由页面平铺表单改为新增/编辑弹框,按 OpenAPI/MCP connector 和认证方式只展示、提交当前适用字段;MCP URL 自动提供认证同源基址,新建时不再重复要求 Base URL,编辑存量定义继续保留既有基址,列表能力模式对 MCP 改为动态 `tools/list`。Web 无 DB 路由 23 项、JavaScript 语法、动态 DOM 引用及 diff 检查通过;当前运行时未发现可连接浏览器,真实点击冒烟待浏览器配置恢复后补验。无 schema、migration、HTTP API、依赖或运行方式变化,未连接生产 DB。 + ### 2026-08-10 - **08-10 / 0.63.4 / 移除 Factory preset + 通用 OpenAPI 契约收敛**:运行态和管理端删除 `factory_mes` provider、兼容包装类及业务默认文案,只保留通用 OpenAPI/MCP definition;新增显式 `ZCBOT_MIGRATION_DB_URL`、默认 dry-run 的一次性脚本,将存量 definition 原子转换为 `generic_openapi + query` 并同步 active connection revision,不解密或重写凭据。OpenAPI 参数直接执行规格中的数值、长度、数组和枚举约束,移除按 `page/page_size/pageoff` 名称猜测分页语义及无效 `max_page_size` 配置。完整 524 项 unittest 全绿(17 skip),外部系统/迁移/无 DB 路由专项 80 项、Python 编译、Ruff 致命规则、JavaScript 语法及 diff 检查通过;迁移脚本仅验证缺少显式 URL 时拒绝运行,未连接或写入任何数据库。 diff --git a/core/__init__.py b/core/__init__.py index 44da8dd..123e750 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,3 @@ # zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。 # 改版本只动这一行。 -__version__ = "0.63.4" +__version__ = "0.63.5" diff --git a/web/static/admin.html b/web/static/admin.html index 6f0a62b..7417b3c 100644 --- a/web/static/admin.html +++ b/web/static/admin.html @@ -109,10 +109,33 @@ width: 100%; padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--r-md); color: var(--text); background: #fff; } - #ext-admin-form button, #s-external td button { + #ext-admin-form button, #s-external button { font-size: 12px; padding: 4px 9px; border: 1px solid var(--border); border-radius: var(--r-md); background: #fff; cursor: pointer; } + #s-external button.primary, #ext-admin-form button.primary { + color: #fff; border-color: var(--accent); background: var(--accent); + } + #external-definition-modal .card { + width: min(820px, calc(100vw - 32px)); max-height: calc(100vh - 32px); + margin: 0; padding: 0; display: flex; flex-direction: column; + } + .ext-modal-head { + display: flex; align-items: center; gap: 12px; padding: 12px 16px; + border-bottom: 1px solid var(--border); + } + .ext-modal-head h3 { flex: 1; margin: 0; font-size: 14px; } + .ext-modal-head button { font-size: 18px !important; line-height: 1; border: 0 !important; } + .ext-form-body { padding: 16px; overflow-y: auto; } + .ext-form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 12px; } + .ext-form-section { + grid-column: 1 / -1; margin: 4px 0 0; padding-top: 10px; + border-top: 1px solid var(--border-soft); color: var(--text); font-weight: 600; + } + .ext-form-actions { + display: flex; justify-content: flex-end; gap: 8px; padding: 10px 16px; + border-top: 1px solid var(--border); background: #fff; + } /* 复用主控制台 dialog.js 的单例页内弹框。 */ .modal { @@ -156,6 +179,8 @@ .card-head { flex-wrap: wrap; } .ctrl { flex-wrap: wrap; } .ctrl select { min-width: 0; } + .ext-form-grid { grid-template-columns: 1fr; } + .ext-form-grid > label, .ext-form-section { grid-column: 1 !important; } /* 窄屏:目录变顶部横向 chip 条 */ #layout { grid-template-columns: 1fr; gap: 10px; } #toc { diff --git a/web/static/js/admin.js b/web/static/js/admin.js index 4db33b0..2dacdf2 100644 --- a/web/static/js/admin.js +++ b/web/static/js/admin.js @@ -161,32 +161,47 @@ function renderByDay(rows) { function renderExternalDefinitions() { const rows = externalDefinitions.map(r => { const cfg = r.config || {}; + const capability = r.provider === "generic_mcp" + ? "动态工具(tools/list)" + : (cfg.operation_mode === "upstream_managed" + ? "上游托管" + : `查询(${Object.keys(cfg.operation_policies || {}).length} 个 POST)`); return `` + `${escapeHtml(r.name)} ${escapeHtml(r.provider_title || r.provider)}${r.enabled ? "" : ' 停用'}` + ` ${r.visibility === "organization" ? "全部用户" : `指定 ${((r.selected_user_ids || []).length)} 人`}` + `${escapeHtml(r.host || cfg.base_url || "—")}` - + `${cfg.operation_mode === "upstream_managed" ? "上游托管" : `查询(${Object.keys(cfg.operation_policies || {}).length} 个 POST)`}` + + `${capability}` + ` ` + ``; }).join("") || `尚未配置外部系统`; $("s-external").innerHTML = `

外部系统目录

` - + `OpenAPI 与 Streamable HTTP MCP 系统均可配置;用户只提交该系统要求的凭据
` - + `
` + + `
OpenAPI 与 Streamable HTTP MCP 系统均可配置;用户只提交该系统要求的凭据 ` + + `
` + + `
` + + `${rows}
系统主机能力模式操作
` + + ``; $("ext-admin-form").onsubmit = saveExternalDefinition; $("exa-guidance").value = ""; @@ -214,7 +228,15 @@ function renderExternalDefinitions() { $("exa-access").onchange = () => { $("exa-users-wrap").hidden = $("exa-access").value !== "selected"; }; - $("exa-cancel").onclick = () => { externalEditingId = ""; renderExternalDefinitions(); }; + $("exa-add").onclick = () => openExternalDefinitionModal(); + $("exa-cancel").onclick = closeExternalDefinitionModal; + $("exa-close").onclick = closeExternalDefinitionModal; + $("external-definition-modal").onclick = event => { + if (event.target.id === "external-definition-modal") closeExternalDefinitionModal(); + }; + $("external-definition-modal").onkeydown = event => { + if (event.key === "Escape") closeExternalDefinitionModal(); + }; updateExternalAuthForm(); updateExternalOperationMode(); $("s-external").onclick = (e) => { @@ -222,7 +244,7 @@ function renderExternalDefinitions() { if (!tr) return; const row = externalDefinitions.find(x => x.definition_id === tr.dataset.definitionId); if (!row) return; - if (e.target.closest("[data-ext-edit]")) fillExternalDefinition(row); + if (e.target.closest("[data-ext-edit]")) openExternalDefinitionModal(row); if (e.target.closest("[data-ext-delete]")) deleteExternalDefinition(row); }; } @@ -237,11 +259,13 @@ function updateExternalAuthForm() { function updateExternalConnectorForm() { const mcp = $("exa-provider").value === "generic_mcp"; + $("exa-base-wrap").hidden = mcp; $("exa-spec-wrap").hidden = mcp; $("exa-mcp-wrap").hidden = !mcp; $("exa-mcp-server-wrap").hidden = !mcp; $("exa-operation-mode-wrap").hidden = mcp; $("exa-operations-wrap").hidden = mcp || $("exa-operation-mode").value === "upstream_managed"; + $("exa-base").required = !mcp; $("exa-spec").required = !mcp; $("exa-mcp").required = mcp; } @@ -298,8 +322,29 @@ async function editExternalGuidance() { updateExternalGuidanceSummary(); } +function openExternalDefinitionModal(row = null) { + externalEditingId = row ? row.definition_id : ""; + $("exa-dialog-title").textContent = row ? `编辑外部系统:${row.name}` : "新增外部系统"; + if (row) { + fillExternalDefinition(row); + } else { + $("ext-admin-form").reset(); + $("exa-provider").disabled = false; + $("exa-provider").value = "generic_openapi"; + $("exa-access").value = "selected"; + $("exa-users-wrap").hidden = false; + applyExternalProviderDefaults(); + } + $("external-definition-modal").classList.add("show"); + $("exa-name").focus(); +} + +function closeExternalDefinitionModal() { + externalEditingId = ""; + $("external-definition-modal").classList.remove("show"); +} + function fillExternalDefinition(row) { - externalEditingId = row.definition_id; const cfg = row.config || {}; $("exa-provider").value = row.provider || "generic_openapi"; $("exa-provider").disabled = true; @@ -331,31 +376,21 @@ function fillExternalDefinition(row) { const selected = new Set(row.selected_user_ids || []); Array.from($("exa-users").options).forEach(o => { o.selected = selected.has(o.value); }); $("exa-users-wrap").hidden = $("exa-access").value !== "selected"; - $("exa-cancel").hidden = false; - $("exa-name").focus(); } async function saveExternalDefinition(e) { e.preventDefault(); + const provider = $("exa-provider").value; + const authType = $("exa-auth").value; + const mcp = provider === "generic_mcp"; + const current = externalDefinitions.find(x => x.definition_id === externalEditingId); + const currentConfig = (current || {}).config || {}; const body = { - provider: $("exa-provider").value, + provider, name: $("exa-name").value.trim(), - base_url: $("exa-base").value.trim(), - openapi_url: $("exa-spec").value.trim(), - mcp_url: $("exa-mcp").value.trim(), - expected_server_name: $("exa-mcp-server").value.trim(), - login_path: $("exa-login").value.trim() || "/api/auth/token/", - auth_type: $("exa-auth").value, - username_field: $("exa-username-field").value.trim() || "username", - password_field: $("exa-password-field").value.trim() || "password", - token_field: $("exa-token-field").value.trim() || "access", + auth_type: authType, auth_header_name: $("exa-auth-header").value.trim() || "Authorization", auth_header_template: $("exa-auth-template").value || "Bearer {token}", - operation_mode: $("exa-operation-mode").value, - operation_policies: Object.fromEntries( - $("exa-operations").value.split(",").map(x => x.trim()).filter(Boolean) - .map(operationId => [operationId, "read"]), - ), recommended_operation_ids: $("exa-recommended").value.split(",").map(x => x.trim()).filter(Boolean), query_guidance: $("exa-guidance").value.trim(), verify_tls: $("exa-tls").checked, @@ -363,15 +398,38 @@ async function saveExternalDefinition(e) { visibility: $("exa-access").value, selected_user_ids: Array.from($("exa-users").selectedOptions).map(o => o.value), }; - const current = externalDefinitions.find(x => x.definition_id === externalEditingId); - body.timeout_seconds = current ? (current.config || {}).timeout_seconds || 15 : 15; - body.max_result_bytes = current ? (current.config || {}).max_result_bytes || 65536 : 65536; + if (authType === "password_jwt") { + Object.assign(body, { + login_path: $("exa-login").value.trim() || "/api/auth/token/", + username_field: $("exa-username-field").value.trim() || "username", + password_field: $("exa-password-field").value.trim() || "password", + token_field: $("exa-token-field").value.trim() || "access", + }); + } + if (mcp) { + Object.assign(body, { + // 编辑时保留既有同源认证基址;新建时由后端从 MCP URL 推导 origin。 + base_url: currentConfig.base_url || "", + mcp_url: $("exa-mcp").value.trim(), + expected_server_name: $("exa-mcp-server").value.trim(), + max_response_bytes: currentConfig.max_response_bytes || 10485760, + }); + } else { + Object.assign(body, { + base_url: $("exa-base").value.trim(), + openapi_url: $("exa-spec").value.trim(), + operation_mode: $("exa-operation-mode").value, + operation_policies: Object.fromEntries( + $("exa-operations").value.split(",").map(x => x.trim()).filter(Boolean) + .map(operationId => [operationId, "read"]), + ), + }); + } + body.timeout_seconds = currentConfig.timeout_seconds || 15; + body.max_result_bytes = currentConfig.max_result_bytes || 65536; body.max_total_result_bytes = current - ? (current.config || {}).max_total_result_bytes || 262144 + ? currentConfig.max_total_result_bytes || 262144 : 262144; - body.max_response_bytes = current - ? (current.config || {}).max_response_bytes || 10485760 - : 10485760; try { await apiSend( externalEditingId ? "PUT" : "POST",