agent-bridge-abweb 1.0.0 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +210 -91
- package/dist/src-ts/plugins/abweb/public/app.ts +122 -74
- package/dist/src-ts/plugins/abweb/public/index.html +6 -5
- package/dist/src-ts/plugins/abweb/public/style.css +24 -12
- package/package.json +4 -2
- package/plugin.json +2 -1
|
@@ -3,26 +3,102 @@
|
|
|
3
3
|
const pluginsNode = node("#plugins");
|
|
4
4
|
const messagesNode = node("#messages");
|
|
5
5
|
const messagesEmpty = node("#messages-empty");
|
|
6
|
+
const logsNode = node("#logs");
|
|
7
|
+
const logsEmpty = node("#logs-empty");
|
|
8
|
+
const logsPageNode = node("#logs-page");
|
|
6
9
|
const errorNode = node("#error");
|
|
7
|
-
const tokenNode = node("#token");
|
|
8
10
|
const pairCodeNode = node("#pair-code");
|
|
9
|
-
const
|
|
11
|
+
const packageSearchNode = node("#package-search");
|
|
12
|
+
const catalogNode = node("#catalog");
|
|
10
13
|
const pluginPathNode = node("#plugin-path");
|
|
11
|
-
const
|
|
12
|
-
const
|
|
14
|
+
const channelTabs = node("#channel-tabs");
|
|
15
|
+
const channelPanel = node("#channel-panel");
|
|
13
16
|
const versionsNode = node("#versions");
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
const policyNode = node("#update-policy");
|
|
18
|
+
const policyStatusNode = node("#update-policy-status");
|
|
19
|
+
const loginExpiry = Number(localStorage.getItem("mab-login-expires-at") ?? 0);
|
|
20
|
+
if (loginExpiry <= Date.now()) {
|
|
21
|
+
localStorage.removeItem("mab-login-token");
|
|
22
|
+
localStorage.removeItem("mab-login-expires-at");
|
|
23
|
+
}
|
|
24
|
+
let authToken = localStorage.getItem("mab-login-token") ?? "";
|
|
16
25
|
let plugins = [];
|
|
17
|
-
let
|
|
26
|
+
let selectedChannelId = "";
|
|
18
27
|
let pluginVersions = {};
|
|
28
|
+
let logPage = 1;
|
|
29
|
+
let logHasNext = false;
|
|
30
|
+
let loading = false;
|
|
31
|
+
const installingPackages = new Set();
|
|
19
32
|
function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
33
|
+
async function loadCatalog() {
|
|
34
|
+
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
35
|
+
const items = result.plugins ?? [];
|
|
36
|
+
catalogNode.replaceChildren();
|
|
37
|
+
if (!items.length) {
|
|
38
|
+
catalogNode.textContent = "未找到已声明类型的 npm 插件";
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const typeNames = { agent: "Agent", channel: "通道", management: "管理" };
|
|
42
|
+
const table = document.createElement("table");
|
|
43
|
+
table.className = "catalog-table";
|
|
44
|
+
const head = document.createElement("thead");
|
|
45
|
+
const headRow = document.createElement("tr");
|
|
46
|
+
for (const label of ["名称", "版本", "类型", "描述", "操作"]) {
|
|
47
|
+
const cell = document.createElement("th");
|
|
48
|
+
cell.scope = "col";
|
|
49
|
+
cell.textContent = label;
|
|
50
|
+
headRow.append(cell);
|
|
51
|
+
}
|
|
52
|
+
head.append(headRow);
|
|
53
|
+
const body = document.createElement("tbody");
|
|
54
|
+
for (const plugin of items) {
|
|
55
|
+
const installed = plugins.find((item) => item.id === plugin.name.replace(/^agent-bridge-/, ""));
|
|
56
|
+
const versionStatus = installed ? pluginVersions[installed.id] : undefined;
|
|
57
|
+
const isUpgrade = Boolean(installed && versionStatus?.packageName === plugin.name && newerVersion(plugin.version, installed.version));
|
|
58
|
+
const row = document.createElement("tr");
|
|
59
|
+
for (const value of [plugin.name, `v${plugin.version}`, typeNames[plugin.type] ?? plugin.type, plugin.description || "无描述"]) {
|
|
60
|
+
const cell = document.createElement("td");
|
|
61
|
+
cell.textContent = value;
|
|
62
|
+
row.append(cell);
|
|
63
|
+
}
|
|
64
|
+
const actionCell = document.createElement("td");
|
|
65
|
+
const action = isUpgrade ? "升级" : installed ? "已安装" : "安装";
|
|
66
|
+
const install = document.createElement("button");
|
|
67
|
+
install.type = "button";
|
|
68
|
+
install.className = "catalog-action";
|
|
69
|
+
install.title = `${action} ${plugin.name}`;
|
|
70
|
+
install.textContent = installingPackages.has(plugin.name) ? "处理中…" : action;
|
|
71
|
+
install.disabled = Boolean(installed && !isUpgrade) || installingPackages.has(plugin.name);
|
|
72
|
+
if (!install.disabled)
|
|
73
|
+
install.onclick = () => {
|
|
74
|
+
installingPackages.add(plugin.name);
|
|
75
|
+
install.disabled = true;
|
|
76
|
+
install.textContent = isUpgrade ? "升级中…" : "安装中…";
|
|
77
|
+
void (isUpgrade ? request(`/api/plugins/${encodeURIComponent(installed.id)}/update`, "POST", { packageName: plugin.name, version: plugin.version }) : request("/api/plugins/install", "POST", { packageName: plugin.name })).then(() => load()).catch((cause) => error(String(cause))).finally(() => { installingPackages.delete(plugin.name); if (install.isConnected) {
|
|
78
|
+
install.disabled = false;
|
|
79
|
+
install.textContent = action;
|
|
80
|
+
} });
|
|
81
|
+
};
|
|
82
|
+
actionCell.append(install);
|
|
83
|
+
row.append(actionCell);
|
|
84
|
+
body.append(row);
|
|
85
|
+
}
|
|
86
|
+
table.append(head, body);
|
|
87
|
+
catalogNode.append(table);
|
|
88
|
+
}
|
|
20
89
|
async function request(path, method = "GET", body) {
|
|
21
90
|
const response = await fetch(path, { method, headers: {
|
|
22
91
|
accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
|
|
23
|
-
...(
|
|
92
|
+
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
|
|
24
93
|
}, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
25
94
|
const data = await response.json();
|
|
95
|
+
if (response.status === 401) {
|
|
96
|
+
localStorage.removeItem("mab-login-token");
|
|
97
|
+
localStorage.removeItem("mab-login-expires-at");
|
|
98
|
+
authToken = "";
|
|
99
|
+
node("#access-panel").hidden = false;
|
|
100
|
+
throw new Error("登录已过期,请重新配对");
|
|
101
|
+
}
|
|
26
102
|
if (!response.ok)
|
|
27
103
|
throw new Error(data.error ?? `请求失败:${response.status}`);
|
|
28
104
|
return data;
|
|
@@ -35,52 +111,72 @@
|
|
|
35
111
|
item.onclick = () => { void run().catch((cause) => error(String(cause))); };
|
|
36
112
|
return item;
|
|
37
113
|
}
|
|
38
|
-
async function load() {
|
|
114
|
+
async function load(refreshCatalog = true) {
|
|
115
|
+
if (loading)
|
|
116
|
+
return;
|
|
117
|
+
loading = true;
|
|
39
118
|
try {
|
|
40
119
|
error("");
|
|
41
|
-
const [health, plugins,
|
|
42
|
-
request("/api/health"), request("/api/plugins"), request("/api/
|
|
120
|
+
const [health, plugins, messages, versions, policy] = await Promise.all([
|
|
121
|
+
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"),
|
|
43
122
|
]);
|
|
123
|
+
policyNode.value = policy.mode;
|
|
124
|
+
policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
44
125
|
renderVersions(versions);
|
|
45
126
|
pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin) => [plugin.id, plugin.versionStatus]));
|
|
46
127
|
node("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
47
128
|
node("#access-panel").hidden = true;
|
|
48
|
-
aids = identities.aids ?? [];
|
|
49
129
|
const installed = plugins.plugins ?? [];
|
|
50
|
-
pluginConfigs = Object.fromEntries(await Promise.all(installed.map(async (plugin) => [plugin.id, await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`)])));
|
|
51
130
|
renderPlugins(installed);
|
|
52
|
-
|
|
53
|
-
aidList.replaceChildren(...[["EvolCore", groups.evolcore], ["mu-agent-bridge", groups.muAgentBridge]].flatMap(([label, values]) => {
|
|
54
|
-
const title = document.createElement("strong");
|
|
55
|
-
title.textContent = String(label);
|
|
56
|
-
const items = values.map((aid) => {
|
|
57
|
-
const item = document.createElement("div");
|
|
58
|
-
item.className = "aid-row";
|
|
59
|
-
const name = document.createElement("span");
|
|
60
|
-
name.textContent = aid;
|
|
61
|
-
const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
|
|
62
|
-
const current = document.createElement("small");
|
|
63
|
-
current.className = "muted";
|
|
64
|
-
current.textContent = linked.length ? `已关联:${linked.join("、")}` : "未关联插件";
|
|
65
|
-
const select = document.createElement("select");
|
|
66
|
-
select.setAttribute("aria-label", `${aid} 关联插件`);
|
|
67
|
-
select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
|
|
68
|
-
select.onchange = () => { if (select.value)
|
|
69
|
-
void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
|
|
70
|
-
item.append(name, current, select);
|
|
71
|
-
return item;
|
|
72
|
-
});
|
|
73
|
-
return [title, ...items];
|
|
74
|
-
}));
|
|
75
|
-
if (!aids.length)
|
|
76
|
-
aidList.textContent = "当前没有已连接的 AID";
|
|
77
|
-
node("#aid-count").textContent = `${aids.length} 个`;
|
|
131
|
+
renderChannels(installed);
|
|
78
132
|
renderMessages(messages.messages ?? []);
|
|
133
|
+
if (refreshCatalog)
|
|
134
|
+
void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
|
|
79
135
|
}
|
|
80
136
|
catch (cause) {
|
|
81
137
|
node("#access-panel").hidden = false;
|
|
82
138
|
error(cause instanceof Error ? cause.message : String(cause));
|
|
83
139
|
}
|
|
140
|
+
finally {
|
|
141
|
+
loading = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function renderChannels(items) {
|
|
145
|
+
const channels = items.filter((item) => item.type === "channel");
|
|
146
|
+
if (!channels.some((item) => item.id === selectedChannelId))
|
|
147
|
+
selectedChannelId = channels[0]?.id ?? "";
|
|
148
|
+
channelTabs.replaceChildren(...channels.map((plugin) => {
|
|
149
|
+
const tab = document.createElement("button");
|
|
150
|
+
tab.type = "button";
|
|
151
|
+
tab.role = "tab";
|
|
152
|
+
tab.textContent = plugin.displayName || plugin.id;
|
|
153
|
+
tab.setAttribute("aria-selected", String(plugin.id === selectedChannelId));
|
|
154
|
+
tab.onclick = () => { selectedChannelId = plugin.id; renderChannels(items); };
|
|
155
|
+
return tab;
|
|
156
|
+
}));
|
|
157
|
+
const selected = channels.find((item) => item.id === selectedChannelId);
|
|
158
|
+
if (!selected) {
|
|
159
|
+
channelPanel.textContent = "暂无已安装的 Channel 插件";
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (selected.status !== "ready") {
|
|
163
|
+
channelPanel.textContent = selected.error || `插件状态:${selected.status}。请先启动插件。`;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const page = selected.manifest?.managementPage;
|
|
167
|
+
if (!page) {
|
|
168
|
+
channelPanel.textContent = "插件未提供设置页面";
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const current = channelPanel.querySelector("iframe.channel-frame");
|
|
172
|
+
if (current?.getAttribute("src") === page)
|
|
173
|
+
return;
|
|
174
|
+
const frame = document.createElement("iframe");
|
|
175
|
+
frame.className = "channel-frame";
|
|
176
|
+
frame.title = `${selected.displayName || selected.id} 设置`;
|
|
177
|
+
frame.src = page;
|
|
178
|
+
frame.onerror = () => { channelPanel.textContent = "设置页面加载失败,请刷新重试"; };
|
|
179
|
+
channelPanel.replaceChildren(frame);
|
|
84
180
|
}
|
|
85
181
|
function renderVersions(data) {
|
|
86
182
|
const entries = [data.core, ...(data.plugins ?? []).map((plugin) => plugin.versionStatus)].filter(Boolean);
|
|
@@ -95,43 +191,36 @@
|
|
|
95
191
|
}
|
|
96
192
|
function renderPlugins(items) {
|
|
97
193
|
plugins = items;
|
|
98
|
-
const selected = picker.value || items[0]?.id;
|
|
99
|
-
picker.replaceChildren(...items.map((plugin) => new Option(`${plugin.displayName || plugin.id} · v${plugin.version} · ${plugin.status}`, plugin.id)));
|
|
100
|
-
picker.value = items.some((plugin) => plugin.id === selected) ? selected : items[0]?.id ?? "";
|
|
101
194
|
pluginsNode.replaceChildren();
|
|
102
|
-
for (const plugin of items
|
|
103
|
-
const item = document.createElement("
|
|
104
|
-
|
|
105
|
-
const main = document.createElement("div");
|
|
106
|
-
main.className = "plugin-main";
|
|
107
|
-
const name = document.createElement("strong");
|
|
195
|
+
for (const plugin of items) {
|
|
196
|
+
const item = document.createElement("tr");
|
|
197
|
+
const name = document.createElement("td");
|
|
108
198
|
name.textContent = plugin.displayName || plugin.id;
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
199
|
+
const id = document.createElement("small");
|
|
200
|
+
id.className = "muted plugin-id";
|
|
201
|
+
id.textContent = plugin.id;
|
|
202
|
+
name.append(id);
|
|
203
|
+
const type = document.createElement("td");
|
|
204
|
+
type.textContent = { agent: "Agent", channel: "Channel", management: "管理" }[plugin.type ?? ""] ?? plugin.type ?? "其他";
|
|
205
|
+
const version = document.createElement("td");
|
|
206
|
+
version.textContent = `v${plugin.version}`;
|
|
207
|
+
const state = document.createElement("td");
|
|
112
208
|
const status = document.createElement("span");
|
|
113
209
|
status.className = `status status-${plugin.status}`;
|
|
114
210
|
status.textContent = plugin.status;
|
|
115
|
-
|
|
211
|
+
state.append(status);
|
|
116
212
|
if (plugin.error) {
|
|
117
213
|
const detail = document.createElement("small");
|
|
118
|
-
detail.className = "error-text";
|
|
214
|
+
detail.className = "error-text plugin-id";
|
|
119
215
|
detail.textContent = plugin.error;
|
|
120
|
-
|
|
216
|
+
state.append(detail);
|
|
121
217
|
}
|
|
122
|
-
const controls = document.createElement("
|
|
218
|
+
const controls = document.createElement("td");
|
|
123
219
|
controls.className = "controls";
|
|
124
|
-
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules"))
|
|
220
|
+
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) {
|
|
125
221
|
controlsUpgrade(plugin.id);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
select.append(new Option("不绑定 AID", ""), ...aids.map((aid) => new Option(aid, aid)));
|
|
129
|
-
select.value = pluginConfigs[plugin.id]?.aid ?? "";
|
|
130
|
-
if (plugin.id !== "abweb")
|
|
131
|
-
controls.append(select, button("关联 AID", "保存此插件使用的 AID", async () => {
|
|
132
|
-
await request(`/api/plugins/${encodeURIComponent(plugin.id)}/aid`, "PATCH", { aid: select.value });
|
|
133
|
-
await load();
|
|
134
|
-
}));
|
|
222
|
+
controls.append(button("下载", `下载 ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/download`, "POST", { packageName: plugin.packageName, version: pluginVersions[plugin.id]?.latestVersion }); await load(); }));
|
|
223
|
+
}
|
|
135
224
|
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
136
225
|
controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
|
|
137
226
|
}
|
|
@@ -142,12 +231,18 @@
|
|
|
142
231
|
await load();
|
|
143
232
|
}
|
|
144
233
|
}));
|
|
145
|
-
item.append(
|
|
234
|
+
item.append(name, type, version, state, controls);
|
|
146
235
|
pluginsNode.append(item);
|
|
147
236
|
function controlsUpgrade(id) { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
|
|
148
237
|
}
|
|
149
|
-
if (!items.length)
|
|
150
|
-
|
|
238
|
+
if (!items.length) {
|
|
239
|
+
const row = document.createElement("tr");
|
|
240
|
+
const cell = document.createElement("td");
|
|
241
|
+
cell.colSpan = 5;
|
|
242
|
+
cell.textContent = "暂无插件";
|
|
243
|
+
row.append(cell);
|
|
244
|
+
pluginsNode.append(row);
|
|
245
|
+
}
|
|
151
246
|
}
|
|
152
247
|
function renderMessages(messages) {
|
|
153
248
|
messagesNode.replaceChildren();
|
|
@@ -164,50 +259,74 @@
|
|
|
164
259
|
messagesNode.append(row);
|
|
165
260
|
}
|
|
166
261
|
}
|
|
167
|
-
|
|
262
|
+
function renderLogs(data) {
|
|
263
|
+
logsNode.replaceChildren();
|
|
264
|
+
logsEmpty.hidden = data.logs.length > 0;
|
|
265
|
+
logPage = data.page;
|
|
266
|
+
logHasNext = data.hasNext;
|
|
267
|
+
logsPageNode.textContent = `第 ${data.page} 页`;
|
|
268
|
+
for (const log of data.logs) {
|
|
269
|
+
const row = document.createElement("tr");
|
|
270
|
+
for (const value of [new Date(log.time).toLocaleString(), log.level, log.source, log.message, log.fields ? JSON.stringify(log.fields) : ""]) {
|
|
271
|
+
const cell = document.createElement("td");
|
|
272
|
+
cell.textContent = value;
|
|
273
|
+
row.append(cell);
|
|
274
|
+
}
|
|
275
|
+
logsNode.append(row);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function loadLogs(page = logPage) { renderLogs(await request(`/api/logs?page=${page}&pageSize=100`)); }
|
|
279
|
+
for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
168
280
|
node(`#${tabId}`).onclick = () => {
|
|
169
|
-
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
|
|
281
|
+
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
170
282
|
const selected = otherTab === tabId;
|
|
171
283
|
node(`#${otherTab}`).setAttribute("aria-selected", String(selected));
|
|
172
284
|
node(`#${otherPanel}`).hidden = !selected;
|
|
173
285
|
}
|
|
286
|
+
if (tabId === "logs-tab")
|
|
287
|
+
void loadLogs().catch((cause) => error(String(cause)));
|
|
174
288
|
};
|
|
175
289
|
}
|
|
290
|
+
node("#logs-refresh").onclick = () => void loadLogs().catch((cause) => error(String(cause)));
|
|
291
|
+
node("#logs-prev").onclick = () => { if (logPage > 1)
|
|
292
|
+
void loadLogs(logPage - 1).catch((cause) => error(String(cause))); };
|
|
293
|
+
node("#logs-next").onclick = () => { if (logHasNext)
|
|
294
|
+
void loadLogs(logPage + 1).catch((cause) => error(String(cause))); };
|
|
176
295
|
node("#refresh").onclick = () => void load();
|
|
177
|
-
node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(load).catch((cause) => error(String(cause)));
|
|
178
|
-
|
|
179
|
-
node("#
|
|
296
|
+
node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
|
|
297
|
+
node("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
|
|
298
|
+
node("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?"))
|
|
299
|
+
void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
|
|
180
300
|
node("#pair-button").onclick = () => {
|
|
181
301
|
const code = pairCodeNode.value.trim();
|
|
182
302
|
if (!/^\d{6}$/.test(code))
|
|
183
303
|
return error("请输入 6 位配置码");
|
|
184
304
|
void request("/api/pair", "POST", { code }).then((result) => {
|
|
185
|
-
|
|
186
|
-
|
|
305
|
+
authToken = result.token;
|
|
306
|
+
localStorage.setItem("mab-login-token", result.token);
|
|
307
|
+
localStorage.setItem("mab-login-expires-at", String(result.expiresAt));
|
|
187
308
|
pairCodeNode.value = "";
|
|
188
309
|
return load();
|
|
189
310
|
}).catch((cause) => error(String(cause)));
|
|
190
311
|
};
|
|
191
|
-
node("#
|
|
192
|
-
|
|
193
|
-
void
|
|
194
|
-
.then(() => load()).catch((cause) => error(String(cause)));
|
|
195
|
-
};
|
|
312
|
+
node("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
|
|
313
|
+
packageSearchNode.onkeydown = (event) => { if (event.key === "Enter")
|
|
314
|
+
void loadCatalog().catch((cause) => error(String(cause))); };
|
|
196
315
|
node("#install-local-form").onsubmit = (event) => {
|
|
197
316
|
event.preventDefault();
|
|
198
317
|
void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
|
|
199
318
|
.then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
|
|
200
319
|
};
|
|
201
|
-
node("#create-aid-form").onsubmit = (event) => {
|
|
202
|
-
event.preventDefault();
|
|
203
|
-
const aid = node("#new-aid").value.trim();
|
|
204
|
-
const ownerAid = node("#owner-aid").value.trim();
|
|
205
|
-
const result = node("#aid-result");
|
|
206
|
-
result.textContent = "正在创建 AID…";
|
|
207
|
-
void request("/api/aids", "POST", { aid, ownerAid }).then(() => {
|
|
208
|
-
result.textContent = `${aid} 已创建;连接到 AUN 后会出现在上方列表。`;
|
|
209
|
-
return load();
|
|
210
|
-
}).catch((cause) => { result.textContent = ""; error(String(cause)); });
|
|
211
|
-
};
|
|
212
320
|
void load();
|
|
321
|
+
setInterval(() => { if (!document.hidden)
|
|
322
|
+
void load(false); }, 10_000);
|
|
323
|
+
setInterval(() => { if (!document.hidden)
|
|
324
|
+
void loadCatalog().catch((cause) => error(String(cause))); }, 300000);
|
|
325
|
+
function newerVersion(remote, current) {
|
|
326
|
+
const parse = (value) => { const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value); return match ? match.slice(1).map(Number) : undefined; };
|
|
327
|
+
const a = parse(remote), b = parse(current);
|
|
328
|
+
if (!a || !b)
|
|
329
|
+
return false;
|
|
330
|
+
return a.some((part, index) => part !== b[index] && part > b[index] && a.slice(0, index).every((item, i) => item === b[i]));
|
|
331
|
+
}
|
|
213
332
|
})();
|
|
@@ -1,30 +1,75 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
type Plugin = { id: string; displayName: string; version: string; status: string; error?: string; path: string };
|
|
2
|
+
type Plugin = { id: string; displayName: string; version: string; status: string; type?: string; capabilities?: string[]; error?: string; path: string; packageName?: string; manifest?: { managementPage?: string } };
|
|
3
3
|
const node = <T extends HTMLElement>(selector: string): T => document.querySelector<T>(selector)!;
|
|
4
|
-
const pluginsNode = node<
|
|
4
|
+
const pluginsNode = node<HTMLTableSectionElement>("#plugins");
|
|
5
5
|
const messagesNode = node<HTMLTableSectionElement>("#messages");
|
|
6
6
|
const messagesEmpty = node<HTMLParagraphElement>("#messages-empty");
|
|
7
|
+
const logsNode = node<HTMLTableSectionElement>("#logs");
|
|
8
|
+
const logsEmpty = node<HTMLParagraphElement>("#logs-empty");
|
|
9
|
+
const logsPageNode = node<HTMLSpanElement>("#logs-page");
|
|
7
10
|
const errorNode = node<HTMLParagraphElement>("#error");
|
|
8
|
-
const tokenNode = node<HTMLInputElement>("#token");
|
|
9
11
|
const pairCodeNode = node<HTMLInputElement>("#pair-code");
|
|
10
|
-
const
|
|
12
|
+
const packageSearchNode = node<HTMLInputElement>("#package-search");
|
|
13
|
+
const catalogNode = node<HTMLDivElement>("#catalog");
|
|
11
14
|
const pluginPathNode = node<HTMLInputElement>("#plugin-path");
|
|
12
|
-
const
|
|
13
|
-
const
|
|
15
|
+
const channelTabs = node<HTMLElement>("#channel-tabs");
|
|
16
|
+
const channelPanel = node<HTMLElement>("#channel-panel");
|
|
14
17
|
const versionsNode = node<HTMLDivElement>("#versions");
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
const policyNode = node<HTMLSelectElement>("#update-policy");
|
|
19
|
+
const policyStatusNode = node<HTMLSpanElement>("#update-policy-status");
|
|
20
|
+
const loginExpiry = Number(localStorage.getItem("mab-login-expires-at") ?? 0);
|
|
21
|
+
if (loginExpiry <= Date.now()) { localStorage.removeItem("mab-login-token"); localStorage.removeItem("mab-login-expires-at"); }
|
|
22
|
+
let authToken = localStorage.getItem("mab-login-token") ?? "";
|
|
17
23
|
let plugins: Plugin[] = [];
|
|
18
|
-
let
|
|
24
|
+
let selectedChannelId = "";
|
|
19
25
|
let pluginVersions: Record<string, any> = {};
|
|
26
|
+
let logPage = 1;
|
|
27
|
+
let logHasNext = false;
|
|
28
|
+
let loading = false;
|
|
29
|
+
const installingPackages = new Set<string>();
|
|
20
30
|
|
|
21
31
|
function error(message: string): void { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
32
|
+
async function loadCatalog(): Promise<void> {
|
|
33
|
+
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
34
|
+
const items: Array<{ name: string; version: string; description: string; type: string }> = result.plugins ?? [];
|
|
35
|
+
catalogNode.replaceChildren();
|
|
36
|
+
if (!items.length) { catalogNode.textContent = "未找到已声明类型的 npm 插件"; return; }
|
|
37
|
+
const typeNames: Record<string, string> = { agent: "Agent", channel: "通道", management: "管理" };
|
|
38
|
+
const table = document.createElement("table"); table.className = "catalog-table";
|
|
39
|
+
const head = document.createElement("thead");
|
|
40
|
+
const headRow = document.createElement("tr");
|
|
41
|
+
for (const label of ["名称", "版本", "类型", "描述", "操作"]) { const cell = document.createElement("th"); cell.scope = "col"; cell.textContent = label; headRow.append(cell); }
|
|
42
|
+
head.append(headRow);
|
|
43
|
+
const body = document.createElement("tbody");
|
|
44
|
+
for (const plugin of items) {
|
|
45
|
+
const installed = plugins.find((item) => item.id === plugin.name.replace(/^agent-bridge-/, ""));
|
|
46
|
+
const versionStatus = installed ? pluginVersions[installed.id] : undefined;
|
|
47
|
+
const isUpgrade = Boolean(installed && versionStatus?.packageName === plugin.name && newerVersion(plugin.version, installed.version));
|
|
48
|
+
const row = document.createElement("tr");
|
|
49
|
+
for (const value of [plugin.name, `v${plugin.version}`, typeNames[plugin.type] ?? plugin.type, plugin.description || "无描述"]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); }
|
|
50
|
+
const actionCell = document.createElement("td");
|
|
51
|
+
const action = isUpgrade ? "升级" : installed ? "已安装" : "安装";
|
|
52
|
+
const install = document.createElement("button"); install.type = "button"; install.className = "catalog-action"; install.title = `${action} ${plugin.name}`; install.textContent = installingPackages.has(plugin.name) ? "处理中…" : action;
|
|
53
|
+
install.disabled = Boolean(installed && !isUpgrade) || installingPackages.has(plugin.name);
|
|
54
|
+
if (!install.disabled) install.onclick = () => { installingPackages.add(plugin.name); install.disabled = true; install.textContent = isUpgrade ? "升级中…" : "安装中…";
|
|
55
|
+
void (isUpgrade ? request(`/api/plugins/${encodeURIComponent(installed!.id)}/update`, "POST", { packageName: plugin.name, version: plugin.version }) : request("/api/plugins/install", "POST", { packageName: plugin.name })).then(() => load()).catch((cause) => error(String(cause))).finally(() => { installingPackages.delete(plugin.name); if (install.isConnected) { install.disabled = false; install.textContent = action; } });
|
|
56
|
+
};
|
|
57
|
+
actionCell.append(install); row.append(actionCell); body.append(row);
|
|
58
|
+
}
|
|
59
|
+
table.append(head, body); catalogNode.append(table);
|
|
60
|
+
}
|
|
22
61
|
async function request(path: string, method = "GET", body?: unknown): Promise<any> {
|
|
23
62
|
const response = await fetch(path, { method, headers: {
|
|
24
63
|
accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
|
|
25
|
-
...(
|
|
64
|
+
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
|
|
26
65
|
}, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
27
66
|
const data = await response.json();
|
|
67
|
+
if (response.status === 401) {
|
|
68
|
+
localStorage.removeItem("mab-login-token"); localStorage.removeItem("mab-login-expires-at");
|
|
69
|
+
authToken = "";
|
|
70
|
+
node<HTMLElement>("#access-panel").hidden = false;
|
|
71
|
+
throw new Error("登录已过期,请重新配对");
|
|
72
|
+
}
|
|
28
73
|
if (!response.ok) throw new Error(data.error ?? `请求失败:${response.status}`);
|
|
29
74
|
return data;
|
|
30
75
|
}
|
|
@@ -32,39 +77,46 @@
|
|
|
32
77
|
const item = document.createElement("button"); item.type = "button"; item.textContent = label; item.title = title;
|
|
33
78
|
item.onclick = () => { void run().catch((cause) => error(String(cause))); }; return item;
|
|
34
79
|
}
|
|
35
|
-
async function load(): Promise<void> {
|
|
80
|
+
async function load(refreshCatalog = true): Promise<void> {
|
|
81
|
+
if (loading) return;
|
|
82
|
+
loading = true;
|
|
36
83
|
try {
|
|
37
84
|
error("");
|
|
38
|
-
const [health, plugins,
|
|
39
|
-
request("/api/health"), request("/api/plugins"), request("/api/
|
|
85
|
+
const [health, plugins, messages, versions, policy] = await Promise.all([
|
|
86
|
+
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"),
|
|
40
87
|
]);
|
|
88
|
+
policyNode.value = policy.mode; policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
41
89
|
renderVersions(versions);
|
|
42
90
|
pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin: any) => [plugin.id, plugin.versionStatus]));
|
|
43
91
|
node<HTMLElement>("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
44
92
|
node<HTMLElement>("#access-panel").hidden = true;
|
|
45
|
-
aids = identities.aids ?? [];
|
|
46
93
|
const installed: Plugin[] = plugins.plugins ?? [];
|
|
47
|
-
pluginConfigs = Object.fromEntries(await Promise.all(installed.map(async (plugin) => [plugin.id, await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`)] as const)));
|
|
48
94
|
renderPlugins(installed);
|
|
49
|
-
|
|
50
|
-
aidList.replaceChildren(...[ ["EvolCore", groups.evolcore], ["mu-agent-bridge", groups.muAgentBridge] ].flatMap(([label, values]) => {
|
|
51
|
-
const title = document.createElement("strong"); title.textContent = String(label);
|
|
52
|
-
const items = (values as string[]).map((aid) => {
|
|
53
|
-
const item = document.createElement("div"); item.className = "aid-row";
|
|
54
|
-
const name = document.createElement("span"); name.textContent = aid;
|
|
55
|
-
const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
|
|
56
|
-
const current = document.createElement("small"); current.className = "muted"; current.textContent = linked.length ? `已关联:${linked.join("、")}` : "未关联插件";
|
|
57
|
-
const select = document.createElement("select"); select.setAttribute("aria-label", `${aid} 关联插件`);
|
|
58
|
-
select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
|
|
59
|
-
select.onchange = () => { if (select.value) void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
|
|
60
|
-
item.append(name, current, select); return item;
|
|
61
|
-
});
|
|
62
|
-
return [title, ...items];
|
|
63
|
-
}));
|
|
64
|
-
if (!aids.length) aidList.textContent = "当前没有已连接的 AID";
|
|
65
|
-
node<HTMLElement>("#aid-count").textContent = `${aids.length} 个`;
|
|
95
|
+
renderChannels(installed);
|
|
66
96
|
renderMessages(messages.messages ?? []);
|
|
97
|
+
if (refreshCatalog) void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
|
|
67
98
|
} catch (cause) { node<HTMLElement>("#access-panel").hidden = false; error(cause instanceof Error ? cause.message : String(cause)); }
|
|
99
|
+
finally { loading = false; }
|
|
100
|
+
}
|
|
101
|
+
function renderChannels(items: Plugin[]): void {
|
|
102
|
+
const channels = items.filter((item) => item.type === "channel");
|
|
103
|
+
if (!channels.some((item) => item.id === selectedChannelId)) selectedChannelId = channels[0]?.id ?? "";
|
|
104
|
+
channelTabs.replaceChildren(...channels.map((plugin) => {
|
|
105
|
+
const tab = document.createElement("button"); tab.type = "button"; tab.role = "tab"; tab.textContent = plugin.displayName || plugin.id;
|
|
106
|
+
tab.setAttribute("aria-selected", String(plugin.id === selectedChannelId));
|
|
107
|
+
tab.onclick = () => { selectedChannelId = plugin.id; renderChannels(items); };
|
|
108
|
+
return tab;
|
|
109
|
+
}));
|
|
110
|
+
const selected = channels.find((item) => item.id === selectedChannelId);
|
|
111
|
+
if (!selected) { channelPanel.textContent = "暂无已安装的 Channel 插件"; return; }
|
|
112
|
+
if (selected.status !== "ready") { channelPanel.textContent = selected.error || `插件状态:${selected.status}。请先启动插件。`; return; }
|
|
113
|
+
const page = selected.manifest?.managementPage;
|
|
114
|
+
if (!page) { channelPanel.textContent = "插件未提供设置页面"; return; }
|
|
115
|
+
const current = channelPanel.querySelector<HTMLIFrameElement>("iframe.channel-frame");
|
|
116
|
+
if (current?.getAttribute("src") === page) return;
|
|
117
|
+
const frame = document.createElement("iframe"); frame.className = "channel-frame"; frame.title = `${selected.displayName || selected.id} 设置`; frame.src = page;
|
|
118
|
+
frame.onerror = () => { channelPanel.textContent = "设置页面加载失败,请刷新重试"; };
|
|
119
|
+
channelPanel.replaceChildren(frame);
|
|
68
120
|
}
|
|
69
121
|
function renderVersions(data: any): void {
|
|
70
122
|
const entries = [data.core, ...(data.plugins ?? []).map((plugin: any) => plugin.versionStatus)].filter(Boolean);
|
|
@@ -77,36 +129,29 @@
|
|
|
77
129
|
}
|
|
78
130
|
function renderPlugins(items: Plugin[]): void {
|
|
79
131
|
plugins = items;
|
|
80
|
-
const selected = picker.value || items[0]?.id;
|
|
81
|
-
picker.replaceChildren(...items.map((plugin) => new Option(`${plugin.displayName || plugin.id} · v${plugin.version} · ${plugin.status}`, plugin.id)));
|
|
82
|
-
picker.value = items.some((plugin) => plugin.id === selected) ? selected : items[0]?.id ?? "";
|
|
83
132
|
pluginsNode.replaceChildren();
|
|
84
|
-
for (const plugin of items
|
|
85
|
-
const item = document.createElement("
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
133
|
+
for (const plugin of items) {
|
|
134
|
+
const item = document.createElement("tr");
|
|
135
|
+
const name = document.createElement("td"); name.textContent = plugin.displayName || plugin.id;
|
|
136
|
+
const id = document.createElement("small"); id.className = "muted plugin-id"; id.textContent = plugin.id; name.append(id);
|
|
137
|
+
const type = document.createElement("td"); type.textContent = ({ agent: "Agent", channel: "Channel", management: "管理" } as Record<string, string>)[plugin.type ?? ""] ?? plugin.type ?? "其他";
|
|
138
|
+
const version = document.createElement("td"); version.textContent = `v${plugin.version}`;
|
|
139
|
+
const state = document.createElement("td");
|
|
89
140
|
const status = document.createElement("span"); status.className = `status status-${plugin.status}`; status.textContent = plugin.status;
|
|
90
|
-
|
|
91
|
-
if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text"; detail.textContent = plugin.error;
|
|
92
|
-
const controls = document.createElement("
|
|
93
|
-
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) controlsUpgrade(plugin.id);
|
|
94
|
-
const select = document.createElement("select"); select.setAttribute("aria-label", `${plugin.id} 的 AID`);
|
|
95
|
-
select.append(new Option("不绑定 AID", ""), ...aids.map((aid) => new Option(aid, aid)));
|
|
96
|
-
select.value = pluginConfigs[plugin.id]?.aid ?? "";
|
|
97
|
-
if (plugin.id !== "abweb") controls.append(select, button("关联 AID", "保存此插件使用的 AID", async () => {
|
|
98
|
-
await request(`/api/plugins/${encodeURIComponent(plugin.id)}/aid`, "PATCH", { aid: select.value }); await load();
|
|
99
|
-
}));
|
|
141
|
+
state.append(status);
|
|
142
|
+
if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text plugin-id"; detail.textContent = plugin.error; state.append(detail); }
|
|
143
|
+
const controls = document.createElement("td"); controls.className = "controls";
|
|
144
|
+
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) { controlsUpgrade(plugin.id); controls.append(button("下载", `下载 ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/download`, "POST", { packageName: plugin.packageName, version: pluginVersions[plugin.id]?.latestVersion }); await load(); })); }
|
|
100
145
|
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
101
146
|
controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
|
|
102
147
|
}
|
|
103
148
|
if (plugin.path.includes(".agent-bridge") && plugin.path.includes("node_modules")) controls.append(button("卸载", `卸载 ${plugin.id}`, async () => {
|
|
104
149
|
if (confirm(`卸载 ${plugin.displayName}?`)) { await request(`/api/plugins/${encodeURIComponent(plugin.id)}`, "DELETE"); await load(); }
|
|
105
150
|
}));
|
|
106
|
-
item.append(
|
|
151
|
+
item.append(name, type, version, state, controls); pluginsNode.append(item);
|
|
107
152
|
function controlsUpgrade(id: string): void { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
|
|
108
153
|
}
|
|
109
|
-
if (!items.length)
|
|
154
|
+
if (!items.length) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 5; cell.textContent = "暂无插件"; row.append(cell); pluginsNode.append(row); }
|
|
110
155
|
}
|
|
111
156
|
function renderMessages(messages: Array<{ time: string; pluginId: string; direction: "request" | "reply" | "error"; text: string; peer?: string; agentAid?: string }>): void {
|
|
112
157
|
messagesNode.replaceChildren();
|
|
@@ -120,46 +165,49 @@
|
|
|
120
165
|
messagesNode.append(row);
|
|
121
166
|
}
|
|
122
167
|
}
|
|
123
|
-
|
|
168
|
+
function renderLogs(data: { logs: Array<{ time: string; level: string; source: string; message: string; fields?: Record<string, unknown> }>; page: number; hasNext: boolean }): void {
|
|
169
|
+
logsNode.replaceChildren(); logsEmpty.hidden = data.logs.length > 0; logPage = data.page; logHasNext = data.hasNext; logsPageNode.textContent = `第 ${data.page} 页`;
|
|
170
|
+
for (const log of data.logs) { const row = document.createElement("tr"); for (const value of [new Date(log.time).toLocaleString(), log.level, log.source, log.message, log.fields ? JSON.stringify(log.fields) : ""]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); } logsNode.append(row); }
|
|
171
|
+
}
|
|
172
|
+
async function loadLogs(page = logPage): Promise<void> { renderLogs(await request(`/api/logs?page=${page}&pageSize=100`)); }
|
|
173
|
+
for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
124
174
|
node<HTMLButtonElement>(`#${tabId}`).onclick = () => {
|
|
125
|
-
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
|
|
175
|
+
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
|
|
126
176
|
const selected = otherTab === tabId;
|
|
127
177
|
node<HTMLButtonElement>(`#${otherTab}`).setAttribute("aria-selected", String(selected));
|
|
128
178
|
node<HTMLDivElement>(`#${otherPanel}`).hidden = !selected;
|
|
129
179
|
}
|
|
180
|
+
if (tabId === "logs-tab") void loadLogs().catch((cause) => error(String(cause)));
|
|
130
181
|
};
|
|
131
182
|
}
|
|
183
|
+
node<HTMLButtonElement>("#logs-refresh").onclick = () => void loadLogs().catch((cause) => error(String(cause)));
|
|
184
|
+
node<HTMLButtonElement>("#logs-prev").onclick = () => { if (logPage > 1) void loadLogs(logPage - 1).catch((cause) => error(String(cause))); };
|
|
185
|
+
node<HTMLButtonElement>("#logs-next").onclick = () => { if (logHasNext) void loadLogs(logPage + 1).catch((cause) => error(String(cause))); };
|
|
132
186
|
node<HTMLButtonElement>("#refresh").onclick = () => void load();
|
|
133
|
-
node<HTMLButtonElement>("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(load).catch((cause) => error(String(cause)));
|
|
134
|
-
|
|
135
|
-
node<HTMLButtonElement>("#
|
|
187
|
+
node<HTMLButtonElement>("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
|
|
188
|
+
node<HTMLButtonElement>("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
|
|
189
|
+
node<HTMLButtonElement>("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?")) void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
|
|
136
190
|
node<HTMLButtonElement>("#pair-button").onclick = () => {
|
|
137
191
|
const code = pairCodeNode.value.trim();
|
|
138
192
|
if (!/^\d{6}$/.test(code)) return error("请输入 6 位配置码");
|
|
139
193
|
void request("/api/pair", "POST", { code }).then((result) => {
|
|
140
|
-
|
|
194
|
+
authToken = result.token; localStorage.setItem("mab-login-token", result.token); localStorage.setItem("mab-login-expires-at", String(result.expiresAt)); pairCodeNode.value = ""; return load();
|
|
141
195
|
}).catch((cause) => error(String(cause)));
|
|
142
196
|
};
|
|
143
|
-
node<
|
|
144
|
-
|
|
145
|
-
void request("/api/plugins/install", "POST", { packageName: packageNode.value })
|
|
146
|
-
.then(() => load()).catch((cause) => error(String(cause)));
|
|
147
|
-
};
|
|
197
|
+
node<HTMLButtonElement>("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
|
|
198
|
+
packageSearchNode.onkeydown = (event) => { if (event.key === "Enter") void loadCatalog().catch((cause) => error(String(cause))); };
|
|
148
199
|
node<HTMLFormElement>("#install-local-form").onsubmit = (event) => {
|
|
149
200
|
event.preventDefault();
|
|
150
201
|
void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
|
|
151
202
|
.then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
|
|
152
203
|
};
|
|
153
|
-
node<HTMLFormElement>("#create-aid-form").onsubmit = (event) => {
|
|
154
|
-
event.preventDefault();
|
|
155
|
-
const aid = node<HTMLInputElement>("#new-aid").value.trim();
|
|
156
|
-
const ownerAid = node<HTMLInputElement>("#owner-aid").value.trim();
|
|
157
|
-
const result = node<HTMLElement>("#aid-result");
|
|
158
|
-
result.textContent = "正在创建 AID…";
|
|
159
|
-
void request("/api/aids", "POST", { aid, ownerAid }).then(() => {
|
|
160
|
-
result.textContent = `${aid} 已创建;连接到 AUN 后会出现在上方列表。`;
|
|
161
|
-
return load();
|
|
162
|
-
}).catch((cause) => { result.textContent = ""; error(String(cause)); });
|
|
163
|
-
};
|
|
164
204
|
void load();
|
|
205
|
+
setInterval(() => { if (!document.hidden) void load(false); }, 10_000);
|
|
206
|
+
setInterval(() => { if (!document.hidden) void loadCatalog().catch((cause) => error(String(cause))); }, 300000);
|
|
207
|
+
|
|
208
|
+
function newerVersion(remote: string, current: string): boolean {
|
|
209
|
+
const parse = (value: string) => { const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value); return match ? match.slice(1).map(Number) : undefined; };
|
|
210
|
+
const a = parse(remote), b = parse(current); if (!a || !b) return false;
|
|
211
|
+
return a.some((part, index) => part !== b[index] && part > b[index] && a.slice(0, index).every((item, i) => item === b[i]));
|
|
212
|
+
}
|
|
165
213
|
})();
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
<body>
|
|
10
10
|
<main class="shell">
|
|
11
11
|
<header><div><h1>mab 管理</h1><p id="health" class="muted">连接中</p></div><button id="refresh" type="button">刷新</button></header>
|
|
12
|
-
<section id="access-panel" class="access" hidden><label for="pair-code">配置码</label><input id="pair-code" inputmode="numeric" maxlength="6" pattern="[0-9]{6}" autocomplete="one-time-code" placeholder="运行 mab pair 获取"><button id="pair-button" type="button">配对</button
|
|
13
|
-
<nav class="tabs" role="tablist" aria-label="管理视图"><button id="settings-tab" type="button" role="tab" aria-selected="true" aria-controls="settings-panel">插件设置</button><button id="messages-tab" type="button" role="tab" aria-selected="false" aria-controls="messages-panel">通信记录</button></nav>
|
|
12
|
+
<section id="access-panel" class="access" hidden><label for="pair-code">配置码</label><input id="pair-code" inputmode="numeric" maxlength="6" pattern="[0-9]{6}" autocomplete="one-time-code" placeholder="运行 mab pair 获取"><button id="pair-button" type="button">配对</button></section>
|
|
13
|
+
<nav class="tabs" role="tablist" aria-label="管理视图"><button id="settings-tab" type="button" role="tab" aria-selected="true" aria-controls="settings-panel">插件设置</button><button id="messages-tab" type="button" role="tab" aria-selected="false" aria-controls="messages-panel">通信记录</button><button id="logs-tab" type="button" role="tab" aria-selected="false" aria-controls="logs-panel">运行日志</button></nav>
|
|
14
14
|
<div id="settings-panel" role="tabpanel" aria-labelledby="settings-tab">
|
|
15
|
-
<section><div class="section-heading"><h2>版本管理</h2><button id="check-versions" type="button">检查更新</button></div><div id="versions" class="muted"></div></section>
|
|
16
|
-
<section><
|
|
17
|
-
<section><div class="section-heading"><h2
|
|
15
|
+
<section><div class="section-heading"><h2>版本管理</h2><div><button id="check-versions" type="button">检查更新</button><button id="update-core" type="button">升级后台</button></div></div><label for="update-policy">插件更新策略</label><select id="update-policy"><option value="auto-install">自动下载并安装</option><option value="download-only">自动下载,手动安装</option><option value="manual">手动下载和安装</option></select><button id="save-update-policy" type="button">保存策略</button><span id="update-policy-status" class="muted"></span><div id="versions" class="muted"></div></section>
|
|
16
|
+
<section><details id="installed-plugins" open><summary><h2>已安装插件</h2></summary><div class="table-scroll"><table class="plugin-table"><thead><tr><th scope="col">名称</th><th scope="col">类型</th><th scope="col">版本</th><th scope="col">状态</th><th scope="col">操作</th></tr></thead><tbody id="plugins"></tbody></table></div><div class="plugin-catalog"><div class="section-heading"><h3>npm 插件目录</h3><button id="search-packages" type="button">刷新目录</button></div><input id="package-search" placeholder="搜索插件名称或功能" aria-label="搜索 npm 插件"><div id="catalog" class="catalog" aria-live="polite"></div></div><form id="install-local-form"><input id="plugin-path" required placeholder="Windows 上的插件目录路径" aria-label="本地插件目录"><button type="submit">安装本地插件</button></form></details></section>
|
|
17
|
+
<section><div class="section-heading"><h2>Channel 设置</h2></div><nav id="channel-tabs" class="tabs channel-tabs" role="tablist" aria-label="Channel 设置"></nav><div id="channel-panel" role="tabpanel" aria-live="polite"></div></section>
|
|
18
18
|
</div>
|
|
19
19
|
<div id="messages-panel" role="tabpanel" aria-labelledby="messages-tab" hidden><section><div class="section-heading"><h2>最近通信</h2></div><div class="table-scroll"><table><thead><tr><th scope="col">时间</th><th scope="col">插件</th><th scope="col">接收 AID</th><th scope="col">来源 Peer</th><th scope="col">方向</th><th scope="col">消息</th><th scope="col">错误</th></tr></thead><tbody id="messages"></tbody></table></div><p id="messages-empty" class="muted" hidden>暂无通信记录</p></section></div>
|
|
20
|
+
<div id="logs-panel" role="tabpanel" aria-labelledby="logs-tab" hidden><section><div class="section-heading"><h2>运行日志</h2><button id="logs-refresh" type="button">刷新日志</button></div><div class="table-scroll"><table><thead><tr><th>时间</th><th>级别</th><th>来源</th><th>消息</th><th>详细字段</th></tr></thead><tbody id="logs"></tbody></table></div><p id="logs-empty" class="muted" hidden>暂无运行日志</p><div class="section-heading"><button id="logs-prev" type="button">上一页</button><span id="logs-page" class="muted">第 1 页</span><button id="logs-next" type="button">下一页</button></div></section></div>
|
|
20
21
|
<p id="error" class="error" role="alert" hidden></p>
|
|
21
22
|
</main>
|
|
22
23
|
<script src="/abweb/app.js"></script>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* { box-sizing: border-box; }
|
|
3
3
|
body { margin: 0; }
|
|
4
4
|
.shell { max-width: 1100px; margin: 0 auto; padding: 24px; }
|
|
5
|
-
header, .section-heading, .access
|
|
5
|
+
header, .section-heading, .access { display: flex; align-items: center; gap: 12px; }
|
|
6
6
|
header, .section-heading { justify-content: space-between; }
|
|
7
7
|
.tabs { display: flex; gap: 20px; border-bottom: 1px solid #cbd5d0; margin-top: 22px; }
|
|
8
8
|
.tabs button { border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: transparent; padding: 8px 2px; }
|
|
@@ -10,6 +10,9 @@ header, .section-heading { justify-content: space-between; }
|
|
|
10
10
|
[hidden] { display: none !important; }
|
|
11
11
|
h1 { font-size: 25px; margin: 0; }
|
|
12
12
|
h2 { font-size: 17px; margin: 0; }
|
|
13
|
+
summary { cursor: pointer; padding: 4px 0; }
|
|
14
|
+
summary h2 { display: inline; margin-left: 6px; }
|
|
15
|
+
summary:focus-visible { outline: 2px solid #136a44; outline-offset: 4px; }
|
|
13
16
|
p { margin: 5px 0; }
|
|
14
17
|
section { border-top: 1px solid #d9dfdc; padding: 18px 0; }
|
|
15
18
|
.access { flex-wrap: wrap; }
|
|
@@ -19,16 +22,25 @@ button { cursor: pointer; white-space: nowrap; }
|
|
|
19
22
|
button:hover { background: #e5f2ea; }
|
|
20
23
|
form { display: flex; gap: 8px; }
|
|
21
24
|
form { margin-top: 14px; flex-wrap: wrap; }
|
|
22
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.plugin-
|
|
29
|
-
.plugin-
|
|
30
|
-
.
|
|
31
|
-
.
|
|
25
|
+
.channel-tabs { flex-wrap: wrap; margin: 10px 0; gap: 16px; }
|
|
26
|
+
.channel-frame { width: 100%; min-height: 430px; border: 0; background: #fff; }
|
|
27
|
+
.plugin-table { min-width: 1000px; }
|
|
28
|
+
.plugin-table th:nth-child(1) { width: 180px; } .plugin-table th:nth-child(2) { width: 90px; } .plugin-table th:nth-child(3) { width: 90px; } .plugin-table th:nth-child(4) { width: 150px; } .plugin-table th:nth-child(5) { width: 360px; }
|
|
29
|
+
.plugin-table .controls { white-space: nowrap; }
|
|
30
|
+
.plugin-table .controls button { margin: 0 5px 5px 0; }
|
|
31
|
+
.plugin-id { display: block; margin-top: 4px; }
|
|
32
|
+
.plugin-catalog { border-top: 1px solid #e0e5e2; margin-top: 18px; padding-top: 14px; }
|
|
33
|
+
.plugin-catalog h3 { font-size: 14px; margin: 0; }
|
|
34
|
+
.plugin-catalog input { width: min(100%, 420px); margin: 10px 0; }
|
|
35
|
+
.catalog { overflow-x: auto; margin-top: 10px; }
|
|
36
|
+
.catalog-table { width: 100%; min-width: 680px; border-collapse: collapse; font-size: 13px; }
|
|
37
|
+
.catalog-table th, .catalog-table td { padding: 9px 8px; text-align: left; vertical-align: top; border-bottom: 1px solid #dce3df; overflow-wrap: anywhere; }
|
|
38
|
+
.catalog-table th { color: #566761; font-weight: 600; background: #edf1ef; }
|
|
39
|
+
.catalog-table th:nth-child(1) { width: 210px; } .catalog-table th:nth-child(2) { width: 100px; } .catalog-table th:nth-child(3) { width: 90px; } .catalog-table th:nth-child(5) { width: 88px; }
|
|
40
|
+
.catalog-action { min-height: 30px; padding: 4px 10px; }
|
|
41
|
+
.catalog-action:disabled { cursor: wait; opacity: .65; }
|
|
42
|
+
.catalog:empty { color: #566761; font-size: 13px; }
|
|
43
|
+
.plugin-catalog form { margin-top: 8px; }
|
|
32
44
|
.muted { color: #65736e; font-size: 13px; }
|
|
33
45
|
.status { font-size: 12px; padding: 3px 7px; background: #e7ebea; border-radius: 4px; }
|
|
34
46
|
.status-ready { color: #136a44; background: #def3e5; }
|
|
@@ -40,4 +52,4 @@ th, td { padding: 10px 9px; text-align: left; vertical-align: top; border-bottom
|
|
|
40
52
|
th { color: #566761; font-weight: 600; background: #edf1ef; }
|
|
41
53
|
th:nth-child(1) { width: 145px; } th:nth-child(2) { width: 100px; } th:nth-child(3), th:nth-child(4) { width: 145px; } th:nth-child(5) { width: 62px; } th:nth-child(7) { width: 140px; }
|
|
42
54
|
.message-error td:last-child { color: #a83427; }
|
|
43
|
-
@media (max-width: 620px) { .shell { padding: 16px; } .section-heading { align-items: flex-start; flex-direction: column; } form, form input { width: 100%; }
|
|
55
|
+
@media (max-width: 620px) { .shell { padding: 16px; } .section-heading { align-items: flex-start; flex-direction: column; } form, form input { width: 100%; } }
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bridge-abweb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "agent-bridge 的网页管理插件。",
|
|
5
|
+
"keywords": ["mu-agent-bridge-plugin", "mu-agent-bridge-management"],
|
|
5
6
|
"repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/abweb" },
|
|
6
7
|
"main": "dist/src-ts/plugins/abweb/index.js",
|
|
7
8
|
"files": [
|
|
@@ -11,7 +12,8 @@
|
|
|
11
12
|
"README.md"
|
|
12
13
|
],
|
|
13
14
|
"agentBridge": {
|
|
14
|
-
"plugin": "./plugin.json"
|
|
15
|
+
"plugin": "./plugin.json",
|
|
16
|
+
"type": "management"
|
|
15
17
|
},
|
|
16
18
|
"publishConfig": {
|
|
17
19
|
"access": "public",
|