zcbot/web/static/js/external_systems.js

208 lines
8.4 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 connectionForDefinition(definitionId) {
return Array.from(systemsById.values())
.find(item => item.definition_id === definitionId);
}
function submitButtonLabel() {
if (!editingId) return "连接并验证";
return $("ext-definition").disabled ? "验证并更新" : "重新连接并验证";
}
function resetForm() {
editingId = "";
$("ext-form-title").textContent = "连接外部系统";
$("ext-name").disabled = false;
$("ext-definition").disabled = false;
$("ext-err").textContent = "";
$("ext-save").textContent = "连接并验证";
$("ext-form-cancel").hidden = true;
const selected = selectedDefinition() || definitions[0];
const connection = selected && connectionForDefinition(selected.definition_id);
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 : "";
renderCredentialFields((selected || {}).credential_fields || []);
}
}
export function closeExternalSystemsModal() {
$("external-modal").classList.remove("show");
resetForm();
}
async function openExternalSystemsModal() {
$("external-modal").classList.add("show");
resetForm();
await loadExternalSystems();
}
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><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);
$("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 || [];
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 : ",请联系管理员配置"}`;
$("ext-save").disabled = !providerReady;
list.innerHTML = rows.length
? rows.map(cardHtml).join("")
: '<div class="sk-empty">还没有外部系统连接。填写下方凭据后,助手即可按你的权限查询。</div>';
resetForm();
} catch (error) {
providerReady = false;
provider.textContent = "加载失败";
list.innerHTML = `<div class="err">${escapeHtml(error.message)}</div>`;
$("ext-save").disabled = true;
}
}
$("hd-external").onclick = openExternalSystemsModal;
$("ext-close").onclick = closeExternalSystemsModal;
$("external-modal").addEventListener("click", event => {
if (event.target.id === "external-modal") closeExternalSystemsModal();
});
$("ext-form-cancel").onclick = resetForm;
$("ext-definition").onchange = () => {
if ($("ext-definition").disabled) return;
resetForm();
};
$("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");
button.disabled = true;
button.textContent = "正在验证…";
$("ext-err").textContent = "";
try {
if (editingId) {
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");
}
resetForm();
await loadExternalSystems();
} catch (error) {
$("ext-err").textContent = error.message;
} finally {
button.disabled = !providerReady;
button.textContent = submitButtonLabel();
}
});
$("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-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]")) {
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");
resetForm();
await loadExternalSystems();
} catch (error) { message("断开失败:" + error.message, "error"); }
}
});