feat(external-systems): 使用弹框管理连接并标识类型
This commit is contained in:
parent
729c4caafb
commit
e53e91372d
|
|
@ -5,6 +5,10 @@
|
||||||
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
> 所以不是每个版本号都有条目。条目格式 `## <版本> — <日期>`,新条目加在最上面。
|
||||||
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
> 工程口径的完整记录见 `PROGRESS.md` / git log。
|
||||||
|
|
||||||
|
## 0.63.8 — 2026-08-11
|
||||||
|
|
||||||
|
- 外部系统的新建与凭据更新改用独立弹框;点击已有连接即可编辑,新建时不再列出已连接系统,连接卡片同时用图标和文字区分 OpenAPI 与 MCP。
|
||||||
|
|
||||||
## 0.63.7 — 2026-08-11
|
## 0.63.7 — 2026-08-11
|
||||||
|
|
||||||
- 已连接的外部系统现在可直接在凭据表单中重新连接并验证,不再被“该外部系统已连接”的重复创建提示拦住。
|
- 已连接的外部系统现在可直接在凭据表单中重新连接并验证,不再被“该外部系统已连接”的重复创建提示拦住。
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
> 配合 `DESIGN.md`。本文件只记 phase 状态、决策偏差、文件量、下一步。每条 1-2 句:做了啥 + 关键判断;细节查 `git log` / `git diff` / `DESIGN §7.9`。
|
||||||
|
|
||||||
最后更新:2026-08-11(外部系统已有连接直接重连修复,bump 0.63.7)
|
最后更新:2026-08-11(外部系统连接弹框与类型标识,bump 0.63.8)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -23,6 +23,8 @@
|
||||||
|
|
||||||
### 2026-08-11
|
### 2026-08-11
|
||||||
|
|
||||||
|
- **08-11 / 0.63.8 / 外部系统连接弹框 + connector 类型标识**:用户外部系统主页移除状态耦合的内联凭据表单,新建连接与更新凭据统一改为独立弹框;点击已有卡片直接进入编辑,新建弹框只列出尚未连接的 definition,避免重复创建。连接卡片读取既有 `connector` 字段,以不同 SVG、颜色和文字区分 OpenAPI 与 MCP。Web 无 DB 路由与静态资源共 36 项 unittest、JavaScript 语法及 diff 检查通过;本地无安全的 Web 实例可做真实点击,未启动会读取生产 `.env` 的应用,未连接生产 DB。无 schema、migration、HTTP API、依赖或运行方式变化。
|
||||||
|
|
||||||
- **08-11 / 0.63.7 / 外部系统已有连接直接重连**:用户凭据表单按所选 definition 识别已有连接,自动切换为“重新连接并验证”并调用现有凭据更新接口,验证成功后保存新凭据和 active 状态,不再先按新连接提交后收到重复连接提示;卡片上的显式“更新凭据”入口保持不变。Web 无 DB 路由与静态资源共 35 项 unittest、JavaScript 语法及 diff 检查通过;无 schema、migration、HTTP API、依赖或运行方式变化,未连接生产 DB。
|
- **08-11 / 0.63.7 / 外部系统已有连接直接重连**:用户凭据表单按所选 definition 识别已有连接,自动切换为“重新连接并验证”并调用现有凭据更新接口,验证成功后保存新凭据和 active 状态,不再先按新连接提交后收到重复连接提示;卡片上的显式“更新凭据”入口保持不变。Web 无 DB 路由与静态资源共 35 项 unittest、JavaScript 语法及 diff 检查通过;无 schema、migration、HTTP API、依赖或运行方式变化,未连接生产 DB。
|
||||||
|
|
||||||
- **08-11 / 0.63.6 / 外部系统弹框动态字段 + 滚动隔离修复**:修复表单 label 样式覆盖 HTML `hidden` 语义,OpenAPI/MCP connector、认证方式和接口执行模式切换后只显示适用配置项;弹框改为独立可收缩滚动区,并在打开期间锁定根页面滚动,滚动到边界不再带动后台页面。Node 前端 19 项、Web 公共逻辑 unittest 3 项、JavaScript 语法及 diff 检查通过;无 schema、migration、HTTP API、依赖或运行方式变化,未连接生产 DB。
|
- **08-11 / 0.63.6 / 外部系统弹框动态字段 + 滚动隔离修复**:修复表单 label 样式覆盖 HTML `hidden` 语义,OpenAPI/MCP connector、认证方式和接口执行模式切换后只显示适用配置项;弹框改为独立可收缩滚动区,并在打开期间锁定根页面滚动,滚动到边界不再带动后台页面。Node 前端 19 项、Web 公共逻辑 unittest 3 项、JavaScript 语法及 diff 检查通过;无 schema、migration、HTTP API、依赖或运行方式变化,未连接生产 DB。
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
# zcbot 版本号单一事实源:web/app.py 的 FastAPI version、/healthz 返回、前端展示都引这里。
|
||||||
# 改版本只动这一行。
|
# 改版本只动这一行。
|
||||||
__version__ = "0.63.7"
|
__version__ = "0.63.8"
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,22 @@ class StaticVendorTests(unittest.TestCase):
|
||||||
embed_js,
|
embed_js,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_external_system_connections_use_separate_create_and_edit_modal(self) -> None:
|
||||||
|
html = DEV_HTML.read_text(encoding="utf-8")
|
||||||
|
external_js = (JS_DIR / "external_systems.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn('id="ext-add" class="primary"', html)
|
||||||
|
self.assertIn('id="external-connection-modal" class="modal"', html)
|
||||||
|
self.assertLess(html.index('id="external-modal"'), html.index('id="external-connection-modal"'))
|
||||||
|
self.assertIn("function unconnectedDefinitions()", external_js)
|
||||||
|
self.assertIn("function connectorBadgeHtml(connector)", external_js)
|
||||||
|
self.assertIn('class="ext-connector ext-connector-openapi"', external_js)
|
||||||
|
self.assertIn('class="ext-connector ext-connector-mcp"', external_js)
|
||||||
|
self.assertIn('$("ext-add").onclick = () => openConnectionModal();', external_js)
|
||||||
|
self.assertIn("openConnectionModal(systemsById.get(id));", external_js)
|
||||||
|
self.assertIn('await api("PUT", `/v1/external-systems/${editingId}/credentials`', external_js)
|
||||||
|
self.assertNotIn("function connectionForDefinition(", external_js)
|
||||||
|
|
||||||
def test_static_js_is_served_with_revalidation_header(self) -> None:
|
def test_static_js_is_served_with_revalidation_header(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
static_dir = Path(tmp)
|
static_dir = Path(tmp)
|
||||||
|
|
|
||||||
|
|
@ -743,23 +743,33 @@
|
||||||
|
|
||||||
/* ───── 外部系统 modal(用户凭据只写不回显)───── */
|
/* ───── 外部系统 modal(用户凭据只写不回显)───── */
|
||||||
#external-modal { z-index: 112; }
|
#external-modal { z-index: 112; }
|
||||||
|
#external-connection-modal { z-index: 114; }
|
||||||
#external-modal .card { width:620px; max-width:94vw; max-height:84vh; display:flex; flex-direction:column; }
|
#external-modal .card { width:620px; max-width:94vw; max-height:84vh; display:flex; flex-direction:column; }
|
||||||
#external-modal h3 { margin:0; padding:12px 16px; font-size:16px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:8px; }
|
#external-connection-modal .card { width:480px; max-width:94vw; max-height:84vh; display:flex; flex-direction:column; }
|
||||||
#external-modal h3 .spacer { flex:1; }
|
#external-modal h3, #external-connection-modal h3 { margin:0; padding:12px 16px; font-size:16px; border-bottom:1px solid var(--border); display:flex; align-items:center; gap:8px; }
|
||||||
#external-modal .sk-x { border:none; background:transparent; font-size:16px; cursor:pointer; color:var(--muted); padding:2px 6px; }
|
#external-modal h3 .spacer, #external-connection-modal h3 .spacer { flex:1; }
|
||||||
|
#external-modal .sk-x, #external-connection-modal .sk-x { border:none; background:transparent; font-size:16px; cursor:pointer; color:var(--muted); padding:2px 6px; }
|
||||||
#ext-body { padding:16px; overflow:auto; }
|
#ext-body { padding:16px; overflow:auto; }
|
||||||
#ext-list { display:grid; gap:8px; margin-bottom:16px; }
|
#ext-toolbar { display:flex; align-items:flex-start; gap:12px; margin-bottom:12px; }
|
||||||
.ext-card { border:1px solid var(--border); border-radius:8px; padding:12px; background:var(--panel); }
|
#ext-provider { flex:1; min-width:0; }
|
||||||
.ext-card-head { display:flex; align-items:center; gap:8px; }
|
#ext-list { display:grid; gap:8px; }
|
||||||
|
.ext-card { border:1px solid var(--border); border-radius:8px; padding:12px; background:var(--panel); cursor:pointer; transition:border-color .15s, box-shadow .15s; }
|
||||||
|
.ext-card:hover { border-color:var(--accent); box-shadow:0 2px 10px rgba(0,0,0,.06); outline:none; }
|
||||||
|
.ext-card-head { display:flex; align-items:center; flex-wrap:wrap; gap:8px; }
|
||||||
.ext-card-name { font-weight:600; }
|
.ext-card-name { font-weight:600; }
|
||||||
|
.ext-connector { display:inline-flex; align-items:center; gap:4px; padding:2px 6px; border-radius:999px; font-size:11px; font-weight:600; line-height:1.2; }
|
||||||
|
.ext-connector svg { width:13px; height:13px; fill:none; stroke:currentColor; stroke-width:1.4; stroke-linecap:round; stroke-linejoin:round; }
|
||||||
|
.ext-connector-openapi { color:#1769aa; background:#e8f3fc; }
|
||||||
|
.ext-connector-mcp { color:#7048a8; background:#f1eafb; }
|
||||||
.ext-card .meta { margin-top:5px; color:var(--muted); font-size:12px; }
|
.ext-card .meta { margin-top:5px; color:var(--muted); font-size:12px; }
|
||||||
.ext-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
|
.ext-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; }
|
||||||
#ext-form { border-top:1px solid var(--border); padding-top:14px; }
|
#ext-form { min-height:0; overflow:auto; }
|
||||||
#ext-form-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
|
#ext-form-grid { display:grid; grid-template-columns:1fr 1fr; gap:10px; padding:16px; }
|
||||||
#ext-form-grid label { display:grid; gap:4px; color:var(--muted); font-size:12px; }
|
#ext-form-grid label { display:grid; gap:4px; color:var(--muted); font-size:12px; }
|
||||||
#ext-form-grid .wide { grid-column:1 / -1; }
|
#ext-form-grid .wide { grid-column:1 / -1; }
|
||||||
#ext-form-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; margin-top:12px; }
|
#ext-err { margin:0 16px 10px; }
|
||||||
@media (max-width: 600px) { #ext-form-grid { grid-template-columns:1fr; } #ext-form-grid .wide { grid-column:auto; } }
|
#ext-form-actions { display:flex; align-items:center; justify-content:flex-end; gap:8px; padding:12px 16px; border-top:1px solid var(--border); }
|
||||||
|
@media (max-width:600px) { #ext-toolbar { align-items:stretch; flex-direction:column; } #ext-add { align-self:flex-end; } #ext-form-grid { grid-template-columns:1fr; } #ext-form-grid .wide { grid-column:auto; } }
|
||||||
.new-chat-start {
|
.new-chat-start {
|
||||||
width: min(560px, calc(100% - 32px));
|
width: min(560px, calc(100% - 32px));
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
@ -1704,33 +1714,45 @@
|
||||||
|
|
||||||
<!-- ───── 外部系统连接 modal ───── -->
|
<!-- ───── 外部系统连接 modal ───── -->
|
||||||
<div id="external-modal" class="modal">
|
<div id="external-modal" class="modal">
|
||||||
<div class="card">
|
<div class="card" role="dialog" aria-modal="true" aria-labelledby="ext-dialog-title">
|
||||||
<h3>
|
<h3>
|
||||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1"></path><path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1"></path></svg>
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 13a5 5 0 0 0 7.1.1l2-2a5 5 0 0 0-7.1-7.1l-1.1 1.1"></path><path d="M14 11a5 5 0 0 0-7.1-.1l-2 2A5 5 0 0 0 12 20l1.1-1.1"></path></svg>
|
||||||
<span>外部系统</span>
|
<span id="ext-dialog-title">外部系统</span>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button id="ext-close" class="sk-x" title="关闭">✕</button>
|
<button id="ext-close" class="sk-x" type="button" title="关闭">✕</button>
|
||||||
</h3>
|
</h3>
|
||||||
<div id="ext-body">
|
<div id="ext-body">
|
||||||
<div class="muted" style="font-size:12px;margin-bottom:12px;">连接使用你在外部系统中的原有权限。密码加密保存且不会回显、写入对话或交给模型。</div>
|
<div class="muted" style="font-size:12px;margin-bottom:12px;">连接使用你在外部系统中的原有权限。密码加密保存且不会回显、写入对话或交给模型。</div>
|
||||||
<div id="ext-provider" class="muted" style="font-size:12px;margin-bottom:10px;">加载中…</div>
|
<div id="ext-toolbar">
|
||||||
|
<div id="ext-provider" class="muted" style="font-size:12px;">加载中…</div>
|
||||||
|
<button id="ext-add" class="primary" type="button">新建连接</button>
|
||||||
|
</div>
|
||||||
<div id="ext-list"><div class="muted">加载中…</div></div>
|
<div id="ext-list"><div class="muted">加载中…</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="external-connection-modal" class="modal">
|
||||||
|
<div class="card" role="dialog" aria-modal="true" aria-labelledby="ext-form-title">
|
||||||
|
<h3>
|
||||||
|
<span id="ext-form-title">新建外部系统连接</span>
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button id="ext-form-close" class="sk-x" type="button" title="关闭">✕</button>
|
||||||
|
</h3>
|
||||||
<form id="ext-form">
|
<form id="ext-form">
|
||||||
<div style="font-weight:600;margin-bottom:10px;" id="ext-form-title">连接外部系统</div>
|
|
||||||
<div id="ext-form-grid">
|
<div id="ext-form-grid">
|
||||||
<label class="wide">外部系统<select id="ext-definition"></select></label>
|
<label class="wide">外部系统<select id="ext-definition"></select></label>
|
||||||
<label class="wide">连接名称<input id="ext-name" maxlength="80" autocomplete="off"></label>
|
<label class="wide">连接名称<input id="ext-name" maxlength="80" autocomplete="off"></label>
|
||||||
<div id="ext-credential-fields" class="wide" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;"></div>
|
<div id="ext-credential-fields" class="wide" style="display:grid;grid-template-columns:1fr 1fr;gap:8px;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="ext-err" class="err" style="margin-top:8px;"></div>
|
<div id="ext-err" class="err"></div>
|
||||||
<div id="ext-form-actions">
|
<div id="ext-form-actions">
|
||||||
<button id="ext-form-cancel" type="button" hidden>取消更新</button>
|
<button id="ext-form-cancel" type="button">取消</button>
|
||||||
<button id="ext-save" class="primary" type="submit">连接并验证</button>
|
<button id="ext-save" class="primary" type="submit">连接并验证</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ───── 技能查看 modal ───── -->
|
<!-- ───── 技能查看 modal ───── -->
|
||||||
<div id="skills-modal" class="modal">
|
<div id="skills-modal" class="modal">
|
||||||
|
|
|
||||||
|
|
@ -22,50 +22,76 @@ function renderCredentialFields(fields = []) {
|
||||||
`).join("");
|
`).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectionForDefinition(definitionId) {
|
function renderDefinitionOptions(items) {
|
||||||
return Array.from(systemsById.values())
|
$("ext-definition").innerHTML = items.map(item =>
|
||||||
.find(item => item.definition_id === definitionId);
|
`<option value="${escapeHtml(item.definition_id)}">${escapeHtml(item.name)}${item.host ? " · " + escapeHtml(item.host) : ""}</option>`
|
||||||
|
).join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitButtonLabel() {
|
function unconnectedDefinitions() {
|
||||||
if (!editingId) return "连接并验证";
|
const connectedIds = new Set(Array.from(systemsById.values()).map(item => item.definition_id));
|
||||||
return $("ext-definition").disabled ? "验证并更新" : "重新连接并验证";
|
return definitions.filter(item => !connectedIds.has(item.definition_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetForm() {
|
function resetForm() {
|
||||||
editingId = "";
|
editingId = "";
|
||||||
$("ext-form-title").textContent = "连接外部系统";
|
$("ext-form-title").textContent = "新建外部系统连接";
|
||||||
$("ext-name").disabled = false;
|
$("ext-name").disabled = false;
|
||||||
$("ext-definition").disabled = false;
|
$("ext-definition").disabled = false;
|
||||||
$("ext-err").textContent = "";
|
$("ext-err").textContent = "";
|
||||||
$("ext-save").textContent = "连接并验证";
|
$("ext-save").textContent = "连接并验证";
|
||||||
$("ext-form-cancel").hidden = true;
|
const available = unconnectedDefinitions();
|
||||||
const selected = selectedDefinition() || definitions[0];
|
renderDefinitionOptions(available);
|
||||||
const connection = selected && connectionForDefinition(selected.definition_id);
|
const selected = available[0];
|
||||||
if (connection) {
|
|
||||||
editingId = connection.external_system_id;
|
|
||||||
$("ext-form-title").textContent = `重新连接${connection.system_name || "外部系统"}`;
|
|
||||||
$("ext-name").value = connection.name;
|
|
||||||
$("ext-name").disabled = true;
|
|
||||||
$("ext-save").textContent = "重新连接并验证";
|
|
||||||
renderCredentialFields(connection.credential_fields || []);
|
|
||||||
} else {
|
|
||||||
$("ext-name").value = selected ? selected.name : "";
|
$("ext-name").value = selected ? selected.name : "";
|
||||||
renderCredentialFields((selected || {}).credential_fields || []);
|
renderCredentialFields((selected || {}).credential_fields || []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeConnectionModal() {
|
||||||
|
$("external-connection-modal").classList.remove("show");
|
||||||
|
editingId = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function openConnectionModal(system = null) {
|
||||||
|
if (!providerReady) return;
|
||||||
|
resetForm();
|
||||||
|
if (system) {
|
||||||
|
editingId = system.external_system_id;
|
||||||
|
$("ext-form-title").textContent = `更新凭据:${system.system_name || system.name}`;
|
||||||
|
$("ext-name").value = system.name;
|
||||||
|
$("ext-name").disabled = true;
|
||||||
|
renderDefinitionOptions(definitions.filter(item => item.definition_id === system.definition_id));
|
||||||
|
$("ext-definition").disabled = true;
|
||||||
|
$("ext-save").textContent = "验证并更新";
|
||||||
|
renderCredentialFields(system.credential_fields || []);
|
||||||
|
}
|
||||||
|
$("external-connection-modal").classList.add("show");
|
||||||
|
$("ext-credential-fields").querySelector("input")?.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function closeExternalSystemsModal() {
|
export function closeExternalSystemsModal() {
|
||||||
|
closeConnectionModal();
|
||||||
$("external-modal").classList.remove("show");
|
$("external-modal").classList.remove("show");
|
||||||
resetForm();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openExternalSystemsModal() {
|
async function openExternalSystemsModal() {
|
||||||
$("external-modal").classList.add("show");
|
$("external-modal").classList.add("show");
|
||||||
resetForm();
|
|
||||||
await loadExternalSystems();
|
await loadExternalSystems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connectorBadgeHtml(connector) {
|
||||||
|
if (connector === "mcp") {
|
||||||
|
return `<span class="ext-connector ext-connector-mcp" title="MCP 连接">
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5 2v3M11 2v3M3.5 5h9v2.5A4.5 4.5 0 0 1 8 12v2M6 14h4"/></svg>
|
||||||
|
MCP
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
return `<span class="ext-connector ext-connector-openapi" title="OpenAPI 连接">
|
||||||
|
<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="3" cy="8" r="1.5"/><circle cx="13" cy="4" r="1.5"/><circle cx="13" cy="12" r="1.5"/><path d="M4.5 7.4 11.5 4.6M4.5 8.6l7 2.8"/></svg>
|
||||||
|
OpenAPI
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
function cardHtml(item) {
|
function cardHtml(item) {
|
||||||
const good = item.status === "active";
|
const good = item.status === "active";
|
||||||
const badge = good
|
const badge = good
|
||||||
|
|
@ -75,7 +101,7 @@ function cardHtml(item) {
|
||||||
: (item.status === "needs_reverify" ? "需重新验证" : "需更新凭据"));
|
: (item.status === "needs_reverify" ? "需重新验证" : "需更新凭据"));
|
||||||
const checked = item.last_verified_at ? fmtTime(item.last_verified_at) : "尚未验证";
|
const checked = item.last_verified_at ? fmtTime(item.last_verified_at) : "尚未验证";
|
||||||
return `<div class="ext-card" data-id="${escapeHtml(item.external_system_id)}">
|
return `<div class="ext-card" data-id="${escapeHtml(item.external_system_id)}">
|
||||||
<div class="ext-card-head"><span class="ext-card-name">${escapeHtml(item.name)}</span><span class="sk-badge">${badge}</span></div>
|
<div class="ext-card-head"><span class="ext-card-name">${escapeHtml(item.name)}</span>${connectorBadgeHtml(item.connector)}<span class="sk-badge">${badge}</span></div>
|
||||||
<div class="meta">${escapeHtml(item.system_name || "外部系统")} · ${escapeHtml(item.username_masked || "***")} · 最近验证 ${escapeHtml(checked)}</div>
|
<div class="meta">${escapeHtml(item.system_name || "外部系统")} · ${escapeHtml(item.username_masked || "***")} · 最近验证 ${escapeHtml(checked)}</div>
|
||||||
<div class="ext-actions">
|
<div class="ext-actions">
|
||||||
<button type="button" class="small" data-ext-test>测试连接</button>
|
<button type="button" class="small" data-ext-test>测试连接</button>
|
||||||
|
|
@ -98,36 +124,43 @@ async function loadExternalSystems() {
|
||||||
const providers = catalog.providers || [];
|
const providers = catalog.providers || [];
|
||||||
definitions = providers.flatMap(item => item.definitions || []);
|
definitions = providers.flatMap(item => item.definitions || []);
|
||||||
providerReady = definitions.length > 0 && providers.some(item => item.configured);
|
providerReady = definitions.length > 0 && providers.some(item => item.configured);
|
||||||
$("ext-definition").innerHTML = definitions.map(item =>
|
|
||||||
`<option value="${escapeHtml(item.definition_id)}">${escapeHtml(item.name)}${item.host ? " · " + escapeHtml(item.host) : ""}</option>`
|
|
||||||
).join("");
|
|
||||||
const rows = response.results || [];
|
const rows = response.results || [];
|
||||||
systemsById = new Map(rows.map(item => [item.external_system_id, item]));
|
systemsById = new Map(rows.map(item => [item.external_system_id, item]));
|
||||||
provider.textContent = providerReady
|
provider.textContent = providerReady
|
||||||
? `管理员已配置 ${definitions.length} 个外部系统,请选择后填写自己的访问凭据`
|
? `管理员已配置 ${definitions.length} 个外部系统,请选择后填写自己的访问凭据`
|
||||||
: `暂没有可连接的外部系统${providers.find(item => item.reason)?.reason ? ":" + providers.find(item => item.reason).reason : ",请联系管理员配置"}`;
|
: `暂没有可连接的外部系统${providers.find(item => item.reason)?.reason ? ":" + providers.find(item => item.reason).reason : ",请联系管理员配置"}`;
|
||||||
$("ext-save").disabled = !providerReady;
|
const hasAvailableDefinition = unconnectedDefinitions().length > 0;
|
||||||
|
$("ext-add").disabled = !providerReady || !hasAvailableDefinition;
|
||||||
|
$("ext-add").title = hasAvailableDefinition ? "" : "所有可用外部系统均已连接";
|
||||||
list.innerHTML = rows.length
|
list.innerHTML = rows.length
|
||||||
? rows.map(cardHtml).join("")
|
? rows.map(cardHtml).join("")
|
||||||
: '<div class="sk-empty">还没有外部系统连接。填写下方凭据后,助手即可按你的权限查询。</div>';
|
: '<div class="sk-empty">还没有外部系统连接。点击“新建连接”后,助手即可按你的权限查询。</div>';
|
||||||
resetForm();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
providerReady = false;
|
providerReady = false;
|
||||||
provider.textContent = "加载失败";
|
provider.textContent = "加载失败";
|
||||||
list.innerHTML = `<div class="err">${escapeHtml(error.message)}</div>`;
|
list.innerHTML = `<div class="err">${escapeHtml(error.message)}</div>`;
|
||||||
$("ext-save").disabled = true;
|
$("ext-add").disabled = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("hd-external").onclick = openExternalSystemsModal;
|
$("hd-external").onclick = openExternalSystemsModal;
|
||||||
$("ext-close").onclick = closeExternalSystemsModal;
|
$("ext-close").onclick = closeExternalSystemsModal;
|
||||||
|
$("ext-add").onclick = () => openConnectionModal();
|
||||||
|
$("ext-form-close").onclick = closeConnectionModal;
|
||||||
|
$("ext-form-cancel").onclick = closeConnectionModal;
|
||||||
$("external-modal").addEventListener("click", event => {
|
$("external-modal").addEventListener("click", event => {
|
||||||
if (event.target.id === "external-modal") closeExternalSystemsModal();
|
if (event.target.id === "external-modal") closeExternalSystemsModal();
|
||||||
});
|
});
|
||||||
$("ext-form-cancel").onclick = resetForm;
|
$("external-connection-modal").addEventListener("click", event => {
|
||||||
|
if (event.target.id === "external-connection-modal") closeConnectionModal();
|
||||||
|
});
|
||||||
|
$("external-connection-modal").addEventListener("keydown", event => {
|
||||||
|
if (event.key === "Escape") closeConnectionModal();
|
||||||
|
});
|
||||||
$("ext-definition").onchange = () => {
|
$("ext-definition").onchange = () => {
|
||||||
if ($("ext-definition").disabled) return;
|
const selected = selectedDefinition();
|
||||||
resetForm();
|
if (selected) $("ext-name").value = selected.name;
|
||||||
|
renderCredentialFields((selected || {}).credential_fields || []);
|
||||||
};
|
};
|
||||||
|
|
||||||
$("ext-form").addEventListener("submit", async event => {
|
$("ext-form").addEventListener("submit", async event => {
|
||||||
|
|
@ -144,24 +177,25 @@ $("ext-form").addEventListener("submit", async event => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const button = $("ext-save");
|
const button = $("ext-save");
|
||||||
|
const wasEditing = Boolean(editingId);
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.textContent = "正在验证…";
|
button.textContent = "正在验证…";
|
||||||
$("ext-err").textContent = "";
|
$("ext-err").textContent = "";
|
||||||
try {
|
try {
|
||||||
if (editingId) {
|
if (wasEditing) {
|
||||||
await api("PUT", `/v1/external-systems/${editingId}/credentials`, { credentials });
|
await api("PUT", `/v1/external-systems/${editingId}/credentials`, { credentials });
|
||||||
message("外部系统已重新连接,凭据已更新", "success");
|
message("外部系统已重新连接,凭据已更新", "success");
|
||||||
} else {
|
} else {
|
||||||
await api("POST", "/v1/external-systems", { definition_id: definitionId, name, credentials });
|
await api("POST", "/v1/external-systems", { definition_id: definitionId, name, credentials });
|
||||||
message("外部系统已连接;下一轮对话即可使用", "success");
|
message("外部系统已连接;下一轮对话即可使用", "success");
|
||||||
}
|
}
|
||||||
resetForm();
|
closeConnectionModal();
|
||||||
await loadExternalSystems();
|
await loadExternalSystems();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
$("ext-err").textContent = error.message;
|
$("ext-err").textContent = error.message;
|
||||||
} finally {
|
} finally {
|
||||||
button.disabled = !providerReady;
|
button.disabled = false;
|
||||||
button.textContent = submitButtonLabel();
|
button.textContent = wasEditing ? "验证并更新" : "连接并验证";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -169,20 +203,7 @@ $("ext-list").addEventListener("click", async event => {
|
||||||
const card = event.target.closest(".ext-card");
|
const card = event.target.closest(".ext-card");
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const id = card.dataset.id;
|
const id = card.dataset.id;
|
||||||
if (event.target.closest("[data-ext-edit]")) {
|
|
||||||
const system = systemsById.get(id);
|
|
||||||
editingId = id;
|
|
||||||
$("ext-form-title").textContent = "更新外部系统凭据";
|
|
||||||
$("ext-name").value = system.name;
|
|
||||||
$("ext-name").disabled = true;
|
|
||||||
$("ext-definition").value = system.definition_id;
|
|
||||||
$("ext-definition").disabled = true;
|
|
||||||
renderCredentialFields(system.credential_fields || []);
|
|
||||||
$("ext-save").textContent = "验证并更新";
|
|
||||||
$("ext-form-cancel").hidden = false;
|
|
||||||
$("ext-credential-fields").querySelector("input")?.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (event.target.closest("[data-ext-test]")) {
|
if (event.target.closest("[data-ext-test]")) {
|
||||||
const button = event.target.closest("button");
|
const button = event.target.closest("button");
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
@ -200,8 +221,9 @@ $("ext-list").addEventListener("click", async event => {
|
||||||
try {
|
try {
|
||||||
await api("DELETE", `/v1/external-systems/${id}`);
|
await api("DELETE", `/v1/external-systems/${id}`);
|
||||||
message("外部系统已断开", "success");
|
message("外部系统已断开", "success");
|
||||||
resetForm();
|
|
||||||
await loadExternalSystems();
|
await loadExternalSystems();
|
||||||
} catch (error) { message("断开失败:" + error.message, "error"); }
|
} catch (error) { message("断开失败:" + error.message, "error"); }
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
openConnectionModal(systemsById.get(id));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue