zcbot/web/static/js/external_systems.js

230 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 外部系统连接管理:凭据字段由系统定义声明,只提交、不回显。
import { $ } from "./dom.js";
import { api } from "./api.js";
import { escapeHtml, fmtTime } from "./format.js";
import { dialogConfirm, message } from "./dialog.js";
let editingId = "";
let providerReady = false;
let definitions = [];
let systemsById = new Map();
function selectedDefinition() {
return definitions.find(x => x.definition_id === $("ext-definition").value);
}
function renderCredentialFields(fields = []) {
$("ext-credential-fields").innerHTML = fields.map(field => `
<label>${escapeHtml(field.label)}<input
data-credential-name="${escapeHtml(field.name)}"
type="${field.secret ? "password" : "text"}"
autocomplete="${escapeHtml(field.autocomplete || "off")}" required></label>
`).join("");
}
function renderDefinitionOptions(items) {
$("ext-definition").innerHTML = items.map(item =>
`<option value="${escapeHtml(item.definition_id)}">${escapeHtml(item.name)}${item.host ? " · " + escapeHtml(item.host) : ""}</option>`
).join("");
}
function unconnectedDefinitions() {
const connectedIds = new Set(Array.from(systemsById.values()).map(item => item.definition_id));
return definitions.filter(item => !connectedIds.has(item.definition_id));
}
function resetForm() {
editingId = "";
$("ext-form-title").textContent = "新建外部系统连接";
$("ext-name").disabled = false;
$("ext-definition").disabled = false;
$("ext-err").textContent = "";
$("ext-save").textContent = "连接并验证";
const available = unconnectedDefinitions();
renderDefinitionOptions(available);
const selected = available[0];
$("ext-name").value = selected ? selected.name : "";
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() {
closeConnectionModal();
$("external-modal").classList.remove("show");
}
async function openExternalSystemsModal() {
$("external-modal").classList.add("show");
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) {
const good = item.status === "active";
const badge = good
? "已连接"
: (item.status === "disabled"
? "系统已停用"
: (item.status === "needs_reverify" ? "需重新验证" : "需更新凭据"));
const checked = item.last_verified_at ? fmtTime(item.last_verified_at) : "尚未验证";
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>${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="ext-actions">
<button type="button" class="small" data-ext-test>测试连接</button>
<button type="button" class="small" data-ext-edit>更新凭据</button>
<button type="button" class="small danger" data-ext-delete>断开</button>
</div>
</div>`;
}
async function loadExternalSystems() {
const provider = $("ext-provider");
const list = $("ext-list");
provider.textContent = "加载中…";
list.innerHTML = '<div class="muted">加载中…</div>';
try {
const [catalog, response] = await Promise.all([
api("GET", "/v1/external-system-providers"),
api("GET", "/v1/external-systems"),
]);
const providers = catalog.providers || [];
definitions = providers.flatMap(item => item.definitions || []);
providerReady = definitions.length > 0 && providers.some(item => item.configured);
const rows = response.results || [];
systemsById = new Map(rows.map(item => [item.external_system_id, item]));
provider.textContent = providerReady
? `管理员已配置 ${definitions.length} 个外部系统,请选择后填写自己的访问凭据`
: `暂没有可连接的外部系统${providers.find(item => item.reason)?.reason ? "" + providers.find(item => item.reason).reason : ",请联系管理员配置"}`;
const hasAvailableDefinition = unconnectedDefinitions().length > 0;
$("ext-add").disabled = !providerReady || !hasAvailableDefinition;
$("ext-add").title = hasAvailableDefinition ? "" : "所有可用外部系统均已连接";
list.innerHTML = rows.length
? rows.map(cardHtml).join("")
: '<div class="sk-empty">还没有外部系统连接。点击“新建连接”后,助手即可按你的权限查询。</div>';
} catch (error) {
providerReady = false;
provider.textContent = "加载失败";
list.innerHTML = `<div class="err">${escapeHtml(error.message)}</div>`;
$("ext-add").disabled = true;
}
}
$("hd-external").onclick = openExternalSystemsModal;
$("ext-close").onclick = closeExternalSystemsModal;
$("ext-add").onclick = () => openConnectionModal();
$("ext-form-close").onclick = closeConnectionModal;
$("ext-form-cancel").onclick = closeConnectionModal;
$("external-modal").addEventListener("click", event => {
if (event.target.id === "external-modal") closeExternalSystemsModal();
});
$("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 = () => {
const selected = selectedDefinition();
if (selected) $("ext-name").value = selected.name;
renderCredentialFields((selected || {}).credential_fields || []);
};
$("ext-form").addEventListener("submit", async event => {
event.preventDefault();
if (!providerReady) return;
const name = $("ext-name").value.trim();
const definitionId = $("ext-definition").value;
const credentials = Object.fromEntries(
Array.from($("ext-credential-fields").querySelectorAll("[data-credential-name]"))
.map(input => [input.dataset.credentialName, input.value]),
);
if (Object.values(credentials).some(value => !value.trim()) || (!editingId && (!name || !definitionId))) {
$("ext-err").textContent = "请填写连接名称和全部访问凭据";
return;
}
const button = $("ext-save");
const wasEditing = Boolean(editingId);
button.disabled = true;
button.textContent = "正在验证…";
$("ext-err").textContent = "";
try {
if (wasEditing) {
await api("PUT", `/v1/external-systems/${editingId}/credentials`, { credentials });
message("外部系统已重新连接,凭据已更新", "success");
} else {
await api("POST", "/v1/external-systems", { definition_id: definitionId, name, credentials });
message("外部系统已连接;下一轮对话即可使用", "success");
}
closeConnectionModal();
await loadExternalSystems();
} catch (error) {
$("ext-err").textContent = error.message;
} finally {
button.disabled = false;
button.textContent = wasEditing ? "验证并更新" : "连接并验证";
}
});
$("ext-list").addEventListener("click", async event => {
const card = event.target.closest(".ext-card");
if (!card) return;
const id = card.dataset.id;
if (event.target.closest("[data-ext-test]")) {
const button = event.target.closest("button");
button.disabled = true;
try {
const result = await api("POST", `/v1/external-systems/${id}/test`);
message(result.ok ? `连接正常,可用接口 ${result.operation_count || 0}` : `连接失败:${result.error}`, result.ok ? "success" : "error");
await loadExternalSystems();
} catch (error) { message("测试失败:" + error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (event.target.closest("[data-ext-delete]")) {
const name = card.querySelector(".ext-card-name").textContent;
if (!await dialogConfirm({ title:"断开外部系统", message:`断开「${name}」?保存的密文凭据和连接配置将被清除。`, okText:"断开", danger:true })) return;
try {
await api("DELETE", `/v1/external-systems/${id}`);
message("外部系统已断开", "success");
await loadExternalSystems();
} catch (error) { message("断开失败:" + error.message, "error"); }
return;
}
openConnectionModal(systemsById.get(id));
});