import { state as appState } from "/assets/js/state.js";
import { tr } from "/assets/js/i18n.js";
import { $, esc } from "/assets/js/shared.js";
import { ownerName, rgbName } from "./schema.js";
import { get } from "/assets/js/navigation.js";
import { tokenHeaders } from "./services.js";
import { ensureHosts, loadConfig } from "/assets/js/settings.js";
export function pluginText(zh, en) {
return appState.LANG === "zh" ? zh : en;
}
export function renderPlugins() {
return `
${pluginText("仅发现插件不会接管插件的设备能力。接入后发送监控状态;暂停或断开联动不会停止插件的本地功能。", "Discovery does not take control of plugin device capabilities. Connecting sends monitoring state; pausing or disconnecting the link does not stop the plugin’s local functions.")}
${tr("刷新列表")}
`;
}
export function fanMetric(plugins) {
const fans = Object.entries(plugins)
.filter(
([, p]) =>
p.online && p.capabilities?.fan && p.status?.available !== false,
)
.map(([id, p]) => {
const st = p.status;
let value = "";
if (
p.capabilities.fan.rpm &&
typeof st.fan_rpm === "number" &&
Number.isFinite(st.fan_rpm) &&
st.fan_rpm >= 0
)
value = st.fan_rpm + " RPM";
else if (st.fan_name && st.fan_name !== "UNAVAILABLE")
value =
{
OFF: pluginText("关闭", "Off"),
L1: pluginText("低速", "Low"),
L2: pluginText("中低速", "Medium-low"),
L3: pluginText("中速", "Medium"),
L4: pluginText("高速", "High"),
MAX: pluginText("全速", "Full speed"),
}[st.fan_name] || st.fan_name;
return value ? { name: p.name || id, value } : null;
})
.filter(Boolean);
return fans.length
? ` · ${fans.map((f) => esc(pluginText("风扇", "Fan") + (fans.length > 1 ? " " + f.name : "") + " " + f.value)).join(" · ")} `
: "";
}
export function pluginSummary(plugins, host) {
if (!Object.keys(plugins).length)
return `${pluginText("风扇与硬件插件", "Fan and hardware plugins")} ${pluginText("尚未接入硬件插件,风扇状态不可用。独立安装并启动插件后,可在插件管理中接入。", "No hardware plugin connected; fan status is unavailable. Install and start a plugin independently, then connect it in plugin management.")}
${pluginText("管理插件", "Manage plugin")} `;
return Object.entries(plugins)
.map(([id, p]) => {
const st = p.status || {},
cap = p.capabilities || {},
details = [];
if (p.online) {
if (cap.display)
details.push("OLED " + esc(ownerName(st.oled_page || "")));
if (cap.rgb) details.push("RGB " + esc(rgbName(st.rgb_mode || "")));
}
return `${esc(p.name || id)} · ${p.online ? tr("在线") : tr("离线")}${details.length ? " · " + details.join(" · ") : ""}${pluginText("管理插件", "Manage plugin")}
`;
})
.join("");
}
export async function loadPlugins() {
const list = $("pluginList");
if (!list) return;
const button = $("refreshPlugins"),
feedback = $("pluginRefreshState");
if (button?.disabled) return;
if (button) button.disabled = true;
if (feedback) feedback.textContent = pluginText("正在刷新…", "Refreshing…");
list.textContent = pluginText(
"正在发现本机插件…",
"Discovering local plugins…",
);
try {
const data = await get(
"/api/v1/plugins?host=" + encodeURIComponent(appState.currentConfigHost),
);
appState.pluginRows = data.plugins;
if (feedback)
feedback.textContent =
pluginText(
"刷新完成,发现 " + appState.pluginRows.length + " 个插件 · ",
"Refreshed, " + appState.pluginRows.length + " plugins found · ",
) + new Date().toLocaleTimeString();
list.innerHTML =
appState.pluginRows
.map(
(p) =>
`${esc(p.descriptor?.name || p.id)} ${esc(p.socket)} · ${p.online ? tr("在线") : tr("离线")} · ${p.linked ? (p.paused ? pluginText("联动已暂停", "Link paused") : pluginText("已接入", "Connected")) : pluginText("尚未接入", "Not connected")}
${p.error ? `
${esc(p.error)}
` : ""}${p.status?.errors && Object.keys(p.status.errors).length ? `
${esc(JSON.stringify(p.status.errors))}
` : ""}
${Object.keys(
p.descriptor?.capabilities || {},
)
.map((k) =>
esc(
{
fan: pluginText("风扇", "Fan"),
display: pluginText("屏幕", "Display"),
rgb: pluginText("灯光", "Lighting"),
}[k] || k,
),
)
.join(
" · ",
)}
${p.linked ? `${p.paused ? pluginText("恢复告警联动", "Resume alert link") : pluginText("暂停告警联动", "Pause alert link")} ${pluginText("断开接入", "Disconnect")} ` : `${pluginText("接入插件", "Connect plugin")} `}${pluginText("插件配置", "Plugin settings")}
`,
)
.join("") ||
`${pluginText("未发现正在运行的硬件插件。刷新只查询插件,不会安装或启动服务。若已安装,请先在目标机器启动插件,再刷新并接入;未接入不影响监控。", "No running hardware plugins found. Refresh only queries plugins; it does not install or start services. Start the installed plugin on the target host, then refresh and connect. Monitoring works independently.")}
`;
list
.querySelectorAll("[data-plugin-op]")
.forEach(
(b) =>
(b.onclick = () =>
pluginAction(b.dataset.pluginId, b.dataset.pluginOp)),
);
list
.querySelectorAll("[data-plugin-edit]")
.forEach((b) => (b.onclick = () => editPlugin(b.dataset.pluginEdit)));
} catch (e) {
list.textContent = e.message;
if (feedback)
feedback.textContent = pluginText("刷新失败", "Refresh failed");
} finally {
if (button) button.disabled = false;
}
}
export async function pluginPut(path, payload) {
return get(path, {
method: "PUT",
headers: tokenHeaders(),
body: JSON.stringify({ ...payload, host: appState.currentConfigHost }),
});
}
export async function pluginAction(id, operation) {
try {
await pluginPut("/api/v1/plugins", { id, operation });
await loadPlugins();
} catch (e) {
alert(e.message);
}
}
export function capabilityField(section, key, value, spec) {
const unit =
appState.LANG === "zh"
? { s: "秒", ms: "毫秒" }[spec.unit] || spec.unit
: spec.unit,
label = spec.label?.[appState.LANG] || spec.label?.en || key,
id = `${section}.${key}`,
vis = spec.visible
? ` data-cap-visible="${esc(JSON.stringify(spec.visible))}"`
: "";
return `${esc(label)}${spec.type === "enum" ? `${spec.choices.map((x) => `${esc(pluginChoice(x))} `).join("")} ` : `${esc(unit)}
${pluginText("范围", "Range")} ${spec.min}–${spec.max} ${esc(unit)} `} `;
}
export function pluginChoice(x) {
const zh = {
off: "全灭",
solid: "自定义常亮",
flash: "自定义闪烁(实验性)",
breathe: "内置呼吸",
flow: "流水灯",
marquee: "跑马灯",
rainbow: "彩虹灯",
colorful: "炫彩灯",
red: "红色",
green: "绿色",
blue: "蓝色",
yellow: "黄色",
purple: "紫色",
cyan: "青色",
white: "白色",
};
return appState.LANG === "zh" ? zh[x] || x : x;
}
export function capabilityFields(section, fields, values) {
const seen = new Set();
return Object.entries(fields)
.map(([key, spec]) => {
if (!spec.color_group)
return capabilityField(section, key, values[key], spec);
if (seen.has(spec.color_group)) return "";
seen.add(spec.color_group);
const members = Object.entries(fields).filter(
([, v]) => v.color_group === spec.color_group,
),
hex =
"#" +
["r", "g", "b"]
.map((c) =>
Number(values[members.find(([, v]) => v.channel === c)[0]])
.toString(16)
.padStart(2, "0"),
)
.join("");
return ``;
})
.join("");
}
export async function editPlugin(id) {
try {
appState.pluginSelected = id;
appState.pluginDraft = await get(
"/api/v1/plugin/config?host=" +
encodeURIComponent(appState.currentConfigHost) +
"&id=" +
encodeURIComponent(id),
);
const data = appState.pluginDraft,
editor = $("pluginEditor");
editor.innerHTML = `${esc(id)} ${pluginText("配置由插件保存并立即应用,不重启监控。", "Settings are saved and applied by the plugin without restarting monitoring.")}
${(data.notes || []).map((n) => `
${esc(n[appState.LANG] || n.en || "")}
`).join("")}${Object.entries(
data.schema,
)
.map(
([section, fields]) =>
`
${{ fan: pluginText("风扇", "Fan"), oled: pluginText("屏幕", "Display"), rgb: pluginText("灯光", "Lighting") }[section] || esc(section)} ${capabilityFields(section, fields, data.config[section])}
`,
)
.join(
"",
)}
${pluginText("保存插件配置", "Save plugin settings")}
`;
editor.oninput = (e) => {
if (e.target.dataset.capColor) {
const group = e.target.dataset.capColor,
hex = e.target.value;
["r", "g", "b"].forEach((c, i) => {
editor.querySelector(`[data-cap-key="${group}_${c}"]`).value =
parseInt(hex.slice(1 + 2 * i, 3 + 2 * i), 16);
});
}
for (const picker of editor.querySelectorAll("[data-cap-color]")) {
picker.value =
"#" +
["r", "g", "b"]
.map((c) =>
Math.max(
0,
Math.min(
255,
Number(
editor.querySelector(
`[data-cap-key="${picker.dataset.capColor}_${c}"]`,
).value,
),
),
)
.toString(16)
.padStart(2, "0"),
)
.join("");
}
updatePluginVisibility();
};
updatePluginVisibility();
$("savePlugin").onclick = savePluginConfig;
} catch (e) {
$("pluginEditor").textContent = e.message;
}
}
export function updatePluginVisibility() {
document.querySelectorAll("[data-cap-visible]").forEach((el) => {
const rules = JSON.parse(el.dataset.capVisible);
el.classList.toggle(
"hidden",
!Object.entries(rules).every(([key, values]) =>
values.includes(
document.querySelector(`[data-cap-key="${key}"]`)?.value,
),
),
);
});
}
export async function savePluginConfig() {
const updates = {};
for (const el of document.querySelectorAll("[data-cap-key]")) {
if (!el.reportValidity()) return;
const s = el.dataset.capSection,
k = el.dataset.capKey;
(updates[s] ??= {})[k] = el.value;
}
try {
await pluginPut("/api/v1/plugin/config", {
id: appState.pluginSelected,
updates,
});
$("pluginSaveState").textContent = pluginText("保存成功", "Saved");
} catch (e) {
$("pluginSaveState").textContent = e.message;
}
}
document.addEventListener("click", async (e) => {
const b = e.target.closest("[data-manage-plugin-host]");
if (!b) return;
appState.currentConfigHost = b.dataset.managePluginHost;
await ensureHosts();
$("configHost").value = appState.currentConfigHost;
document.querySelector('[data-tab="config"]').click();
await loadConfig();
document.querySelector('[data-config-section="plugins"]')?.click();
});