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 系统均可配置;用户只提交该系统要求的凭据`
- + `
`
+ + ``
+ + ``
+ + `
`
+ + ``
+ + `
`;
$("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",