230 lines
9.6 KiB
JavaScript
230 lines
9.6 KiB
JavaScript
// 外部系统连接管理:凭据字段由系统定义声明,只提交、不回显。
|
||
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));
|
||
});
|