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.
@@ -5,7 +5,7 @@ const node_fs_1 = require("node:fs");
5
5
  const node_path_1 = require("node:path");
6
6
  const MANIFEST = {
7
7
  id: "abweb",
8
- version: "1.0.1",
8
+ version: "1.0.3",
9
9
  displayName: "Agent Bridge Web",
10
10
  entry: "dist/index.js",
11
11
  apiVersion: "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 picker = node("#plugin-picker");
14
- const aidList = node("#aids");
14
+ const channelTabs = node("#channel-tabs");
15
+ const channelPanel = node("#channel-panel");
15
16
  const versionsNode = node("#versions");
16
- tokenNode.value = sessionStorage.getItem("mab-token") ?? "";
17
- let aids = [];
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 pluginConfigs = {};
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
- packageNode.replaceChildren(...[["Agent", "agent"], ["Channel", "channel"], ["管理", "management"]].flatMap(([label, type]) => {
26
- const group = document.createElement("optgroup");
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
- ...(tokenNode.value ? { authorization: `Bearer ${tokenNode.value}` } : {}),
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, identities, messages, versions] = await Promise.all([
57
- request("/api/health"), request("/api/plugins"), request("/api/aids"), request("/api/messages"), request("/api/versions"),
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
- const groups = identities.groups ?? { evolcore: aids, muAgentBridge: [] };
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.filter((item) => item.id === picker.value)) {
124
- const item = document.createElement("article");
125
- item.className = "plugin-row";
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 meta = document.createElement("span");
131
- meta.className = "muted";
132
- meta.textContent = `${plugin.id} · 当前版本 v${plugin.version}`;
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
- main.append(name, meta, status);
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
- main.append(detail);
216
+ state.append(detail);
142
217
  }
143
- const controls = document.createElement("div");
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
- const select = document.createElement("select");
148
- select.setAttribute("aria-label", `${plugin.id} 的 AID`);
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(main, controls);
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
- pluginsNode.textContent = "暂无插件";
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
- for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
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
- picker.onchange = () => renderPlugins(plugins);
200
- node("#save-token").onclick = () => { sessionStorage.setItem("mab-token", tokenNode.value); void load(); };
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
- tokenNode.value = result.token;
207
- sessionStorage.setItem("mab-token", result.token);
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
- void loadCatalog().catch((cause) => error(String(cause)));
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<HTMLDivElement>("#plugins");
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 picker = node<HTMLSelectElement>("#plugin-picker");
15
- const aidList = node<HTMLDivElement>("#aids");
15
+ const channelTabs = node<HTMLElement>("#channel-tabs");
16
+ const channelPanel = node<HTMLElement>("#channel-panel");
16
17
  const versionsNode = node<HTMLDivElement>("#versions");
17
- tokenNode.value = sessionStorage.getItem("mab-token") ?? "";
18
- let aids: string[] = [];
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 pluginConfigs: Record<string, { aid?: string }> = {};
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
- packageNode.replaceChildren(...[["Agent", "agent"], ["Channel", "channel"], ["管理", "management"]].flatMap(([label, type]) => {
28
- const group = document.createElement("optgroup"); group.label = label;
29
- group.append(...items.filter((item) => item.type === type).map((item) => new Option(`${item.name} · v${item.version}`, item.name)));
30
- return group.children.length ? [group] : [];
31
- }));
32
- catalogNode.replaceChildren(...items.map((plugin) => { const item = document.createElement("div"); item.className = "catalog-row"; item.textContent = `${plugin.type} · ${plugin.name} · ${plugin.description || "无描述"}`; return item; }));
33
- if (!(result.plugins ?? []).length) catalogNode.textContent = "未找到已声明类型的 npm 插件";
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
- ...(tokenNode.value ? { authorization: `Bearer ${tokenNode.value}` } : {}),
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, identities, messages, versions] = await Promise.all([
52
- request("/api/health"), request("/api/plugins"), request("/api/aids"), request("/api/messages"), request("/api/versions"),
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
- const groups = identities.groups ?? { evolcore: aids, muAgentBridge: [] };
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.filter((item) => item.id === picker.value)) {
103
- const item = document.createElement("article"); item.className = "plugin-row";
104
- const main = document.createElement("div"); main.className = "plugin-main";
105
- const name = document.createElement("strong"); name.textContent = plugin.displayName || plugin.id;
106
- const meta = document.createElement("span"); meta.className = "muted"; meta.textContent = `${plugin.id} · 当前版本 v${plugin.version}`;
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
- main.append(name, meta, status);
109
- if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text"; detail.textContent = plugin.error; main.append(detail); }
110
- const controls = document.createElement("div"); controls.className = "controls";
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(main, controls); pluginsNode.append(item);
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) pluginsNode.textContent = "暂无插件";
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
- for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
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
- picker.onchange = () => renderPlugins(plugins);
153
- node<HTMLButtonElement>("#save-token").onclick = () => { sessionStorage.setItem("mab-token", tokenNode.value); void load(); };
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
- tokenNode.value = result.token; sessionStorage.setItem("mab-token", result.token); pairCodeNode.value = ""; return load();
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
- node<HTMLFormElement>("#create-aid-form").onsubmit = (event) => {
179
- event.preventDefault();
180
- const aid = node<HTMLInputElement>("#new-aid").value.trim();
181
- const ownerAid = node<HTMLInputElement>("#owner-aid").value.trim();
182
- const result = node<HTMLElement>("#aid-result");
183
- result.textContent = "正在创建 AID…";
184
- void request("/api/aids", "POST", { aid, ownerAid }).then(() => {
185
- result.textContent = `${aid} 已创建;连接到 AUN 后会出现在上方列表。`;
186
- return load();
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><label for="token">访问令牌</label><input id="token" type="password" autocomplete="off" placeholder="已有令牌时可直接填写"><button id="save-token" 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></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><div class="section-heading"><h2>插件</h2><select id="plugin-picker" aria-label="选择插件"></select></div><div id="plugins" class="plugins"></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 插件"><select id="package-name" aria-label="选择 npm 插件"></select><button id="install-package" type="button">安装选中插件</button><form id="install-name-form"><input id="manual-package-name" required pattern="agent-bridge-[a-z0-9-]+" placeholder="输入 npm 包名,例如 agent-bridge-workbuddy" aria-label="直接输入 npm 包名"><button type="submit">按包名安装</button></form><div id="catalog" class="catalog"></div></div><form id="install-local-form"><input id="plugin-path" required placeholder="Windows 上的插件目录路径" aria-label="本地插件目录"><button type="submit">安装本地插件</button></form></section>
17
- <section><div class="section-heading"><h2>已连接 AID</h2><span id="aid-count" class="muted"></span></div><div id="aids" class="aid-list"></div><form id="create-aid-form"><input id="new-aid" required placeholder="新 AID,例如 new.agentid.cn" aria-label="新 AID"><input id="owner-aid" required placeholder="owner AID" aria-label="owner AID"><button type="submit">创建 AID</button></form><p id="aid-result" class="muted" role="status"></p><p class="muted">在插件列表中选择插件,再用“关联 AID”下拉框保存对应身份。</p></section>
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, .plugin-row, .plugin-main, .controls { display: flex; align-items: center; gap: 12px; }
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
- .aid-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
23
- .aid-list span { padding: 5px 8px; border: 1px solid #d9dfdc; border-radius: 4px; background: #fff; font-size: 13px; }
24
- .aid-list strong { flex-basis: 100%; font-size: 13px; margin-top: 8px; }
25
- .aid-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; flex-basis: 100%; }
26
- .aid-row span { min-width: 210px; }
27
- .plugins, .messages { margin-top: 13px; }
28
- .plugin-row { justify-content: space-between; border-bottom: 1px solid #e0e5e2; padding: 12px 0; flex-wrap: wrap; }
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-row { padding: 7px 0; color: #566761; font-size: 13px; border-bottom: 1px solid #edf1ef; }
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%; } .controls select { max-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.1",
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" },
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "abweb",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "type": "management",
5
5
  "displayName": "Agent Bridge Web",
6
6
  "entry": "dist/src-ts/plugins/abweb/index.js",