agent-bridge-abweb 1.0.1 → 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 +200 -112
- package/dist/src-ts/plugins/abweb/public/app.ts +115 -92
- package/dist/src-ts/plugins/abweb/public/index.html +6 -5
- package/dist/src-ts/plugins/abweb/public/style.css +20 -14
- package/package.json +1 -1
- package/plugin.json +1 -1
|
@@ -3,41 +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 packageNode = node("#package-name");
|
|
10
11
|
const packageSearchNode = node("#package-search");
|
|
11
12
|
const catalogNode = node("#catalog");
|
|
12
13
|
const pluginPathNode = node("#plugin-path");
|
|
13
|
-
const
|
|
14
|
-
const
|
|
14
|
+
const channelTabs = node("#channel-tabs");
|
|
15
|
+
const channelPanel = node("#channel-panel");
|
|
15
16
|
const versionsNode = node("#versions");
|
|
16
|
-
|
|
17
|
-
|
|
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") ?? "";
|
|
18
25
|
let plugins = [];
|
|
19
|
-
let
|
|
26
|
+
let selectedChannelId = "";
|
|
20
27
|
let pluginVersions = {};
|
|
28
|
+
let logPage = 1;
|
|
29
|
+
let logHasNext = false;
|
|
30
|
+
let loading = false;
|
|
31
|
+
const installingPackages = new Set();
|
|
21
32
|
function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
22
33
|
async function loadCatalog() {
|
|
23
34
|
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
24
35
|
const items = result.plugins ?? [];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
group.label = label;
|
|
28
|
-
group.append(...items.filter((item) => item.type === type).map((item) => new Option(`${item.name} · v${item.version}`, item.name)));
|
|
29
|
-
return group.children.length ? [group] : [];
|
|
30
|
-
}));
|
|
31
|
-
catalogNode.replaceChildren(...items.map((plugin) => { const item = document.createElement("div"); item.className = "catalog-row"; item.textContent = `${plugin.type} · ${plugin.name} · ${plugin.description || "无描述"}`; return item; }));
|
|
32
|
-
if (!(result.plugins ?? []).length)
|
|
36
|
+
catalogNode.replaceChildren();
|
|
37
|
+
if (!items.length) {
|
|
33
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);
|
|
34
88
|
}
|
|
35
89
|
async function request(path, method = "GET", body) {
|
|
36
90
|
const response = await fetch(path, { method, headers: {
|
|
37
91
|
accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
|
|
38
|
-
...(
|
|
92
|
+
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
|
|
39
93
|
}, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
40
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
|
+
}
|
|
41
102
|
if (!response.ok)
|
|
42
103
|
throw new Error(data.error ?? `请求失败:${response.status}`);
|
|
43
104
|
return data;
|
|
@@ -50,52 +111,72 @@
|
|
|
50
111
|
item.onclick = () => { void run().catch((cause) => error(String(cause))); };
|
|
51
112
|
return item;
|
|
52
113
|
}
|
|
53
|
-
async function load() {
|
|
114
|
+
async function load(refreshCatalog = true) {
|
|
115
|
+
if (loading)
|
|
116
|
+
return;
|
|
117
|
+
loading = true;
|
|
54
118
|
try {
|
|
55
119
|
error("");
|
|
56
|
-
const [health, plugins,
|
|
57
|
-
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"),
|
|
58
122
|
]);
|
|
123
|
+
policyNode.value = policy.mode;
|
|
124
|
+
policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
59
125
|
renderVersions(versions);
|
|
60
126
|
pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin) => [plugin.id, plugin.versionStatus]));
|
|
61
127
|
node("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
62
128
|
node("#access-panel").hidden = true;
|
|
63
|
-
aids = identities.aids ?? [];
|
|
64
129
|
const installed = plugins.plugins ?? [];
|
|
65
|
-
pluginConfigs = Object.fromEntries(await Promise.all(installed.map(async (plugin) => [plugin.id, await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`)])));
|
|
66
130
|
renderPlugins(installed);
|
|
67
|
-
|
|
68
|
-
aidList.replaceChildren(...[["EvolCore", groups.evolcore], ["mu-agent-bridge", groups.muAgentBridge]].flatMap(([label, values]) => {
|
|
69
|
-
const title = document.createElement("strong");
|
|
70
|
-
title.textContent = String(label);
|
|
71
|
-
const items = values.map((aid) => {
|
|
72
|
-
const item = document.createElement("div");
|
|
73
|
-
item.className = "aid-row";
|
|
74
|
-
const name = document.createElement("span");
|
|
75
|
-
name.textContent = aid;
|
|
76
|
-
const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
|
|
77
|
-
const current = document.createElement("small");
|
|
78
|
-
current.className = "muted";
|
|
79
|
-
current.textContent = linked.length ? `已关联:${linked.join("、")}` : "未关联插件";
|
|
80
|
-
const select = document.createElement("select");
|
|
81
|
-
select.setAttribute("aria-label", `${aid} 关联插件`);
|
|
82
|
-
select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
|
|
83
|
-
select.onchange = () => { if (select.value)
|
|
84
|
-
void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
|
|
85
|
-
item.append(name, current, select);
|
|
86
|
-
return item;
|
|
87
|
-
});
|
|
88
|
-
return [title, ...items];
|
|
89
|
-
}));
|
|
90
|
-
if (!aids.length)
|
|
91
|
-
aidList.textContent = "当前没有已连接的 AID";
|
|
92
|
-
node("#aid-count").textContent = `${aids.length} 个`;
|
|
131
|
+
renderChannels(installed);
|
|
93
132
|
renderMessages(messages.messages ?? []);
|
|
133
|
+
if (refreshCatalog)
|
|
134
|
+
void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
|
|
94
135
|
}
|
|
95
136
|
catch (cause) {
|
|
96
137
|
node("#access-panel").hidden = false;
|
|
97
138
|
error(cause instanceof Error ? cause.message : String(cause));
|
|
98
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);
|
|
99
180
|
}
|
|
100
181
|
function renderVersions(data) {
|
|
101
182
|
const entries = [data.core, ...(data.plugins ?? []).map((plugin) => plugin.versionStatus)].filter(Boolean);
|
|
@@ -110,49 +191,36 @@
|
|
|
110
191
|
}
|
|
111
192
|
function renderPlugins(items) {
|
|
112
193
|
plugins = items;
|
|
113
|
-
const selected = picker.value || items[0]?.id;
|
|
114
|
-
const groups = [["Agent", items.filter((plugin) => plugin.type === "agent" || (!plugin.type && plugin.capabilities?.includes("chat")))], ["Channel", items.filter((plugin) => plugin.type === "channel" || plugin.capabilities?.includes("channel"))], ["管理", items.filter((plugin) => plugin.type === "management" || (!plugin.type && plugin.capabilities?.includes("management")))]];
|
|
115
|
-
picker.replaceChildren(...groups.flatMap(([label, group]) => {
|
|
116
|
-
const options = document.createElement("optgroup");
|
|
117
|
-
options.label = label;
|
|
118
|
-
options.append(...group.map((plugin) => new Option(`${plugin.displayName || plugin.id} · v${plugin.version} · ${plugin.status}`, plugin.id)));
|
|
119
|
-
return group.length ? [options] : [];
|
|
120
|
-
}));
|
|
121
|
-
picker.value = items.some((plugin) => plugin.id === selected) ? selected : items[0]?.id ?? "";
|
|
122
194
|
pluginsNode.replaceChildren();
|
|
123
|
-
for (const plugin of items
|
|
124
|
-
const item = document.createElement("
|
|
125
|
-
|
|
126
|
-
const main = document.createElement("div");
|
|
127
|
-
main.className = "plugin-main";
|
|
128
|
-
const name = document.createElement("strong");
|
|
195
|
+
for (const plugin of items) {
|
|
196
|
+
const item = document.createElement("tr");
|
|
197
|
+
const name = document.createElement("td");
|
|
129
198
|
name.textContent = plugin.displayName || plugin.id;
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
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");
|
|
133
208
|
const status = document.createElement("span");
|
|
134
209
|
status.className = `status status-${plugin.status}`;
|
|
135
210
|
status.textContent = plugin.status;
|
|
136
|
-
|
|
211
|
+
state.append(status);
|
|
137
212
|
if (plugin.error) {
|
|
138
213
|
const detail = document.createElement("small");
|
|
139
|
-
detail.className = "error-text";
|
|
214
|
+
detail.className = "error-text plugin-id";
|
|
140
215
|
detail.textContent = plugin.error;
|
|
141
|
-
|
|
216
|
+
state.append(detail);
|
|
142
217
|
}
|
|
143
|
-
const controls = document.createElement("
|
|
218
|
+
const controls = document.createElement("td");
|
|
144
219
|
controls.className = "controls";
|
|
145
|
-
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")) {
|
|
146
221
|
controlsUpgrade(plugin.id);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
select.append(new Option("不绑定 AID", ""), ...aids.map((aid) => new Option(aid, aid)));
|
|
150
|
-
select.value = pluginConfigs[plugin.id]?.aid ?? "";
|
|
151
|
-
if (plugin.id !== "abweb")
|
|
152
|
-
controls.append(select, button("关联 AID", "保存此插件使用的 AID", async () => {
|
|
153
|
-
await request(`/api/plugins/${encodeURIComponent(plugin.id)}/aid`, "PATCH", { aid: select.value });
|
|
154
|
-
await load();
|
|
155
|
-
}));
|
|
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
|
+
}
|
|
156
224
|
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
157
225
|
controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
|
|
158
226
|
}
|
|
@@ -163,12 +231,18 @@
|
|
|
163
231
|
await load();
|
|
164
232
|
}
|
|
165
233
|
}));
|
|
166
|
-
item.append(
|
|
234
|
+
item.append(name, type, version, state, controls);
|
|
167
235
|
pluginsNode.append(item);
|
|
168
236
|
function controlsUpgrade(id) { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
|
|
169
237
|
}
|
|
170
|
-
if (!items.length)
|
|
171
|
-
|
|
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
|
+
}
|
|
172
246
|
}
|
|
173
247
|
function renderMessages(messages) {
|
|
174
248
|
messagesNode.replaceChildren();
|
|
@@ -185,41 +259,56 @@
|
|
|
185
259
|
messagesNode.append(row);
|
|
186
260
|
}
|
|
187
261
|
}
|
|
188
|
-
|
|
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"]]) {
|
|
189
280
|
node(`#${tabId}`).onclick = () => {
|
|
190
|
-
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"]]) {
|
|
191
282
|
const selected = otherTab === tabId;
|
|
192
283
|
node(`#${otherTab}`).setAttribute("aria-selected", String(selected));
|
|
193
284
|
node(`#${otherPanel}`).hidden = !selected;
|
|
194
285
|
}
|
|
286
|
+
if (tabId === "logs-tab")
|
|
287
|
+
void loadLogs().catch((cause) => error(String(cause)));
|
|
195
288
|
};
|
|
196
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))); };
|
|
197
295
|
node("#refresh").onclick = () => void load();
|
|
198
|
-
node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(load).catch((cause) => error(String(cause)));
|
|
199
|
-
|
|
200
|
-
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))); };
|
|
201
300
|
node("#pair-button").onclick = () => {
|
|
202
301
|
const code = pairCodeNode.value.trim();
|
|
203
302
|
if (!/^\d{6}$/.test(code))
|
|
204
303
|
return error("请输入 6 位配置码");
|
|
205
304
|
void request("/api/pair", "POST", { code }).then((result) => {
|
|
206
|
-
|
|
207
|
-
|
|
305
|
+
authToken = result.token;
|
|
306
|
+
localStorage.setItem("mab-login-token", result.token);
|
|
307
|
+
localStorage.setItem("mab-login-expires-at", String(result.expiresAt));
|
|
208
308
|
pairCodeNode.value = "";
|
|
209
309
|
return load();
|
|
210
310
|
}).catch((cause) => error(String(cause)));
|
|
211
311
|
};
|
|
212
|
-
node("#install-package").onclick = () => {
|
|
213
|
-
if (!packageNode.value)
|
|
214
|
-
return error("请先选择插件");
|
|
215
|
-
void request("/api/plugins/install", "POST", { packageName: packageNode.value })
|
|
216
|
-
.then(() => load()).catch((cause) => error(String(cause)));
|
|
217
|
-
};
|
|
218
|
-
node("#install-name-form").onsubmit = (event) => {
|
|
219
|
-
event.preventDefault();
|
|
220
|
-
const packageName = node("#manual-package-name").value.trim();
|
|
221
|
-
void request("/api/plugins/install", "POST", { packageName }).then(() => load()).catch((cause) => error(String(cause)));
|
|
222
|
-
};
|
|
223
312
|
node("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
|
|
224
313
|
packageSearchNode.onkeydown = (event) => { if (event.key === "Enter")
|
|
225
314
|
void loadCatalog().catch((cause) => error(String(cause))); };
|
|
@@ -228,17 +317,16 @@
|
|
|
228
317
|
void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
|
|
229
318
|
.then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
|
|
230
319
|
};
|
|
231
|
-
node("#create-aid-form").onsubmit = (event) => {
|
|
232
|
-
event.preventDefault();
|
|
233
|
-
const aid = node("#new-aid").value.trim();
|
|
234
|
-
const ownerAid = node("#owner-aid").value.trim();
|
|
235
|
-
const result = node("#aid-result");
|
|
236
|
-
result.textContent = "正在创建 AID…";
|
|
237
|
-
void request("/api/aids", "POST", { aid, ownerAid }).then(() => {
|
|
238
|
-
result.textContent = `${aid} 已创建;连接到 AUN 后会出现在上方列表。`;
|
|
239
|
-
return load();
|
|
240
|
-
}).catch((cause) => { result.textContent = ""; error(String(cause)); });
|
|
241
|
-
};
|
|
242
320
|
void load();
|
|
243
|
-
|
|
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
|
+
}
|
|
244
332
|
})();
|
|
@@ -1,43 +1,75 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
type Plugin = { id: string; displayName: string; version: string; status: string; type?: string; capabilities?: 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 packageNode = node<HTMLSelectElement>("#package-name");
|
|
11
12
|
const packageSearchNode = node<HTMLInputElement>("#package-search");
|
|
12
13
|
const catalogNode = node<HTMLDivElement>("#catalog");
|
|
13
14
|
const pluginPathNode = node<HTMLInputElement>("#plugin-path");
|
|
14
|
-
const
|
|
15
|
-
const
|
|
15
|
+
const channelTabs = node<HTMLElement>("#channel-tabs");
|
|
16
|
+
const channelPanel = node<HTMLElement>("#channel-panel");
|
|
16
17
|
const versionsNode = node<HTMLDivElement>("#versions");
|
|
17
|
-
|
|
18
|
-
|
|
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") ?? "";
|
|
19
23
|
let plugins: Plugin[] = [];
|
|
20
|
-
let
|
|
24
|
+
let selectedChannelId = "";
|
|
21
25
|
let pluginVersions: Record<string, any> = {};
|
|
26
|
+
let logPage = 1;
|
|
27
|
+
let logHasNext = false;
|
|
28
|
+
let loading = false;
|
|
29
|
+
const installingPackages = new Set<string>();
|
|
22
30
|
|
|
23
31
|
function error(message: string): void { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
24
32
|
async function loadCatalog(): Promise<void> {
|
|
25
33
|
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
26
34
|
const items: Array<{ name: string; version: string; description: string; type: string }> = result.plugins ?? [];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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);
|
|
34
60
|
}
|
|
35
61
|
async function request(path: string, method = "GET", body?: unknown): Promise<any> {
|
|
36
62
|
const response = await fetch(path, { method, headers: {
|
|
37
63
|
accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
|
|
38
|
-
...(
|
|
64
|
+
...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
|
|
39
65
|
}, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
40
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
|
+
}
|
|
41
73
|
if (!response.ok) throw new Error(data.error ?? `请求失败:${response.status}`);
|
|
42
74
|
return data;
|
|
43
75
|
}
|
|
@@ -45,39 +77,46 @@
|
|
|
45
77
|
const item = document.createElement("button"); item.type = "button"; item.textContent = label; item.title = title;
|
|
46
78
|
item.onclick = () => { void run().catch((cause) => error(String(cause))); }; return item;
|
|
47
79
|
}
|
|
48
|
-
async function load(): Promise<void> {
|
|
80
|
+
async function load(refreshCatalog = true): Promise<void> {
|
|
81
|
+
if (loading) return;
|
|
82
|
+
loading = true;
|
|
49
83
|
try {
|
|
50
84
|
error("");
|
|
51
|
-
const [health, plugins,
|
|
52
|
-
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"),
|
|
53
87
|
]);
|
|
88
|
+
policyNode.value = policy.mode; policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
54
89
|
renderVersions(versions);
|
|
55
90
|
pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin: any) => [plugin.id, plugin.versionStatus]));
|
|
56
91
|
node<HTMLElement>("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
57
92
|
node<HTMLElement>("#access-panel").hidden = true;
|
|
58
|
-
aids = identities.aids ?? [];
|
|
59
93
|
const installed: Plugin[] = plugins.plugins ?? [];
|
|
60
|
-
pluginConfigs = Object.fromEntries(await Promise.all(installed.map(async (plugin) => [plugin.id, await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`)] as const)));
|
|
61
94
|
renderPlugins(installed);
|
|
62
|
-
|
|
63
|
-
aidList.replaceChildren(...[ ["EvolCore", groups.evolcore], ["mu-agent-bridge", groups.muAgentBridge] ].flatMap(([label, values]) => {
|
|
64
|
-
const title = document.createElement("strong"); title.textContent = String(label);
|
|
65
|
-
const items = (values as string[]).map((aid) => {
|
|
66
|
-
const item = document.createElement("div"); item.className = "aid-row";
|
|
67
|
-
const name = document.createElement("span"); name.textContent = aid;
|
|
68
|
-
const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
|
|
69
|
-
const current = document.createElement("small"); current.className = "muted"; current.textContent = linked.length ? `已关联:${linked.join("、")}` : "未关联插件";
|
|
70
|
-
const select = document.createElement("select"); select.setAttribute("aria-label", `${aid} 关联插件`);
|
|
71
|
-
select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
|
|
72
|
-
select.onchange = () => { if (select.value) void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
|
|
73
|
-
item.append(name, current, select); return item;
|
|
74
|
-
});
|
|
75
|
-
return [title, ...items];
|
|
76
|
-
}));
|
|
77
|
-
if (!aids.length) aidList.textContent = "当前没有已连接的 AID";
|
|
78
|
-
node<HTMLElement>("#aid-count").textContent = `${aids.length} 个`;
|
|
95
|
+
renderChannels(installed);
|
|
79
96
|
renderMessages(messages.messages ?? []);
|
|
97
|
+
if (refreshCatalog) void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
|
|
80
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);
|
|
81
120
|
}
|
|
82
121
|
function renderVersions(data: any): void {
|
|
83
122
|
const entries = [data.core, ...(data.plugins ?? []).map((plugin: any) => plugin.versionStatus)].filter(Boolean);
|
|
@@ -90,41 +129,29 @@
|
|
|
90
129
|
}
|
|
91
130
|
function renderPlugins(items: Plugin[]): void {
|
|
92
131
|
plugins = items;
|
|
93
|
-
const selected = picker.value || items[0]?.id;
|
|
94
|
-
const groups = [["Agent", items.filter((plugin) => plugin.type === "agent" || (!plugin.type && plugin.capabilities?.includes("chat")))], ["Channel", items.filter((plugin) => plugin.type === "channel" || plugin.capabilities?.includes("channel"))], ["管理", items.filter((plugin) => plugin.type === "management" || (!plugin.type && plugin.capabilities?.includes("management")))]] as const;
|
|
95
|
-
picker.replaceChildren(...groups.flatMap(([label, group]) => {
|
|
96
|
-
const options = document.createElement("optgroup"); options.label = label;
|
|
97
|
-
options.append(...group.map((plugin) => new Option(`${plugin.displayName || plugin.id} · v${plugin.version} · ${plugin.status}`, plugin.id)));
|
|
98
|
-
return group.length ? [options] : [];
|
|
99
|
-
}));
|
|
100
|
-
picker.value = items.some((plugin) => plugin.id === selected) ? selected : items[0]?.id ?? "";
|
|
101
132
|
pluginsNode.replaceChildren();
|
|
102
|
-
for (const plugin of items
|
|
103
|
-
const item = document.createElement("
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
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");
|
|
107
140
|
const status = document.createElement("span"); status.className = `status status-${plugin.status}`; status.textContent = plugin.status;
|
|
108
|
-
|
|
109
|
-
if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text"; detail.textContent = plugin.error;
|
|
110
|
-
const controls = document.createElement("
|
|
111
|
-
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) controlsUpgrade(plugin.id);
|
|
112
|
-
const select = document.createElement("select"); select.setAttribute("aria-label", `${plugin.id} 的 AID`);
|
|
113
|
-
select.append(new Option("不绑定 AID", ""), ...aids.map((aid) => new Option(aid, aid)));
|
|
114
|
-
select.value = pluginConfigs[plugin.id]?.aid ?? "";
|
|
115
|
-
if (plugin.id !== "abweb") controls.append(select, button("关联 AID", "保存此插件使用的 AID", async () => {
|
|
116
|
-
await request(`/api/plugins/${encodeURIComponent(plugin.id)}/aid`, "PATCH", { aid: select.value }); await load();
|
|
117
|
-
}));
|
|
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(); })); }
|
|
118
145
|
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
119
146
|
controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
|
|
120
147
|
}
|
|
121
148
|
if (plugin.path.includes(".agent-bridge") && plugin.path.includes("node_modules")) controls.append(button("卸载", `卸载 ${plugin.id}`, async () => {
|
|
122
149
|
if (confirm(`卸载 ${plugin.displayName}?`)) { await request(`/api/plugins/${encodeURIComponent(plugin.id)}`, "DELETE"); await load(); }
|
|
123
150
|
}));
|
|
124
|
-
item.append(
|
|
151
|
+
item.append(name, type, version, state, controls); pluginsNode.append(item);
|
|
125
152
|
function controlsUpgrade(id: string): void { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
|
|
126
153
|
}
|
|
127
|
-
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); }
|
|
128
155
|
}
|
|
129
156
|
function renderMessages(messages: Array<{ time: string; pluginId: string; direction: "request" | "reply" | "error"; text: string; peer?: string; agentAid?: string }>): void {
|
|
130
157
|
messagesNode.replaceChildren();
|
|
@@ -138,36 +165,35 @@
|
|
|
138
165
|
messagesNode.append(row);
|
|
139
166
|
}
|
|
140
167
|
}
|
|
141
|
-
|
|
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"]]) {
|
|
142
174
|
node<HTMLButtonElement>(`#${tabId}`).onclick = () => {
|
|
143
|
-
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"]]) {
|
|
144
176
|
const selected = otherTab === tabId;
|
|
145
177
|
node<HTMLButtonElement>(`#${otherTab}`).setAttribute("aria-selected", String(selected));
|
|
146
178
|
node<HTMLDivElement>(`#${otherPanel}`).hidden = !selected;
|
|
147
179
|
}
|
|
180
|
+
if (tabId === "logs-tab") void loadLogs().catch((cause) => error(String(cause)));
|
|
148
181
|
};
|
|
149
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))); };
|
|
150
186
|
node<HTMLButtonElement>("#refresh").onclick = () => void load();
|
|
151
|
-
node<HTMLButtonElement>("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(load).catch((cause) => error(String(cause)));
|
|
152
|
-
|
|
153
|
-
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))); };
|
|
154
190
|
node<HTMLButtonElement>("#pair-button").onclick = () => {
|
|
155
191
|
const code = pairCodeNode.value.trim();
|
|
156
192
|
if (!/^\d{6}$/.test(code)) return error("请输入 6 位配置码");
|
|
157
193
|
void request("/api/pair", "POST", { code }).then((result) => {
|
|
158
|
-
|
|
194
|
+
authToken = result.token; localStorage.setItem("mab-login-token", result.token); localStorage.setItem("mab-login-expires-at", String(result.expiresAt)); pairCodeNode.value = ""; return load();
|
|
159
195
|
}).catch((cause) => error(String(cause)));
|
|
160
196
|
};
|
|
161
|
-
node<HTMLButtonElement>("#install-package").onclick = () => {
|
|
162
|
-
if (!packageNode.value) return error("请先选择插件");
|
|
163
|
-
void request("/api/plugins/install", "POST", { packageName: packageNode.value })
|
|
164
|
-
.then(() => load()).catch((cause) => error(String(cause)));
|
|
165
|
-
};
|
|
166
|
-
node<HTMLFormElement>("#install-name-form").onsubmit = (event) => {
|
|
167
|
-
event.preventDefault();
|
|
168
|
-
const packageName = node<HTMLInputElement>("#manual-package-name").value.trim();
|
|
169
|
-
void request("/api/plugins/install", "POST", { packageName }).then(() => load()).catch((cause) => error(String(cause)));
|
|
170
|
-
};
|
|
171
197
|
node<HTMLButtonElement>("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
|
|
172
198
|
packageSearchNode.onkeydown = (event) => { if (event.key === "Enter") void loadCatalog().catch((cause) => error(String(cause))); };
|
|
173
199
|
node<HTMLFormElement>("#install-local-form").onsubmit = (event) => {
|
|
@@ -175,16 +201,13 @@
|
|
|
175
201
|
void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
|
|
176
202
|
.then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
|
|
177
203
|
};
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}).catch((cause) => { result.textContent = ""; error(String(cause)); });
|
|
188
|
-
};
|
|
189
|
-
void load(); void loadCatalog().catch((cause) => error(String(cause)));
|
|
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
|
+
}
|
|
190
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,21 +22,24 @@ 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-main { flex-wrap: wrap; }
|
|
30
|
-
.controls { flex-wrap: wrap; }
|
|
31
|
-
.controls select { max-width: 210px; }
|
|
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
32
|
.plugin-catalog { border-top: 1px solid #e0e5e2; margin-top: 18px; padding-top: 14px; }
|
|
33
33
|
.plugin-catalog h3 { font-size: 14px; margin: 0; }
|
|
34
34
|
.plugin-catalog input { width: min(100%, 420px); margin: 10px 0; }
|
|
35
|
-
.catalog { margin-top: 10px; }
|
|
36
|
-
.catalog-
|
|
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; }
|
|
37
43
|
.plugin-catalog form { margin-top: 8px; }
|
|
38
44
|
.muted { color: #65736e; font-size: 13px; }
|
|
39
45
|
.status { font-size: 12px; padding: 3px 7px; background: #e7ebea; border-radius: 4px; }
|
|
@@ -46,4 +52,4 @@ th, td { padding: 10px 9px; text-align: left; vertical-align: top; border-bottom
|
|
|
46
52
|
th { color: #566761; font-weight: 600; background: #edf1ef; }
|
|
47
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; }
|
|
48
54
|
.message-error td:last-child { color: #a83427; }
|
|
49
|
-
@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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bridge-abweb",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "agent-bridge 的网页管理插件。",
|
|
5
5
|
"keywords": ["mu-agent-bridge-plugin", "mu-agent-bridge-management"],
|
|
6
6
|
"repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/abweb" },
|