agent-bridge-abweb 1.0.1 → 1.0.4

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.4",
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,83 @@
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, sessions] = await Promise.all([
121
+ request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"),
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
+ const sessionTable = node("#sessions");
134
+ sessionTable.replaceChildren();
135
+ for (const session of sessions.sessions ?? []) {
136
+ const row = document.createElement("tr");
137
+ for (const value of [session.source, session.agentPlugin, session.sessionId, new Date(session.updatedAt).toLocaleString()]) {
138
+ const cell = document.createElement("td");
139
+ cell.textContent = value;
140
+ row.append(cell);
141
+ }
142
+ sessionTable.append(row);
143
+ }
144
+ if (refreshCatalog)
145
+ void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
94
146
  }
95
147
  catch (cause) {
96
148
  node("#access-panel").hidden = false;
97
149
  error(cause instanceof Error ? cause.message : String(cause));
98
150
  }
151
+ finally {
152
+ loading = false;
153
+ }
154
+ }
155
+ function renderChannels(items) {
156
+ const channels = items.filter((item) => item.type === "channel");
157
+ if (!channels.some((item) => item.id === selectedChannelId))
158
+ selectedChannelId = channels[0]?.id ?? "";
159
+ channelTabs.replaceChildren(...channels.map((plugin) => {
160
+ const tab = document.createElement("button");
161
+ tab.type = "button";
162
+ tab.role = "tab";
163
+ tab.textContent = plugin.displayName || plugin.id;
164
+ tab.setAttribute("aria-selected", String(plugin.id === selectedChannelId));
165
+ tab.onclick = () => { selectedChannelId = plugin.id; renderChannels(items); };
166
+ return tab;
167
+ }));
168
+ const selected = channels.find((item) => item.id === selectedChannelId);
169
+ if (!selected) {
170
+ channelPanel.textContent = "暂无已安装的 Channel 插件";
171
+ return;
172
+ }
173
+ if (selected.status !== "ready") {
174
+ channelPanel.textContent = selected.error || `插件状态:${selected.status}。请先启动插件。`;
175
+ return;
176
+ }
177
+ const page = selected.manifest?.managementPage;
178
+ if (!page) {
179
+ channelPanel.textContent = "插件未提供设置页面";
180
+ return;
181
+ }
182
+ const current = channelPanel.querySelector("iframe.channel-frame");
183
+ if (current?.getAttribute("src") === page)
184
+ return;
185
+ const frame = document.createElement("iframe");
186
+ frame.className = "channel-frame";
187
+ frame.title = `${selected.displayName || selected.id} 设置`;
188
+ frame.src = page;
189
+ frame.onerror = () => { channelPanel.textContent = "设置页面加载失败,请刷新重试"; };
190
+ channelPanel.replaceChildren(frame);
99
191
  }
100
192
  function renderVersions(data) {
101
193
  const entries = [data.core, ...(data.plugins ?? []).map((plugin) => plugin.versionStatus)].filter(Boolean);
@@ -110,49 +202,49 @@
110
202
  }
111
203
  function renderPlugins(items) {
112
204
  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
205
  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");
206
+ for (const plugin of items) {
207
+ const item = document.createElement("tr");
208
+ const name = document.createElement("td");
129
209
  name.textContent = plugin.displayName || plugin.id;
130
- const meta = document.createElement("span");
131
- meta.className = "muted";
132
- meta.textContent = `${plugin.id} · 当前版本 v${plugin.version}`;
210
+ const id = document.createElement("small");
211
+ id.className = "muted plugin-id";
212
+ id.textContent = plugin.id;
213
+ name.append(id);
214
+ const type = document.createElement("td");
215
+ type.textContent = { agent: "Agent", channel: "Channel", management: "管理" }[plugin.type ?? ""] ?? plugin.type ?? "其他";
216
+ const version = document.createElement("td");
217
+ version.textContent = `v${plugin.version}`;
218
+ const state = document.createElement("td");
133
219
  const status = document.createElement("span");
134
220
  status.className = `status status-${plugin.status}`;
135
221
  status.textContent = plugin.status;
136
- main.append(name, meta, status);
222
+ const dependencies = Object.entries(plugin.manifest?.dependencies ?? {});
223
+ if (dependencies.length) {
224
+ const detail = document.createElement("small");
225
+ detail.className = "muted plugin-id";
226
+ detail.textContent = `依赖:${dependencies.map(([id, range]) => `${id} ${range}`).join(";")}`;
227
+ name.append(detail);
228
+ }
229
+ state.append(status);
137
230
  if (plugin.error) {
138
231
  const detail = document.createElement("small");
139
- detail.className = "error-text";
232
+ detail.className = "error-text plugin-id";
140
233
  detail.textContent = plugin.error;
141
- main.append(detail);
234
+ state.append(detail);
235
+ }
236
+ if (plugin.dependencyStatus?.length) {
237
+ const detail = document.createElement("small");
238
+ detail.className = "error-text plugin-id";
239
+ detail.textContent = `依赖:${plugin.dependencyStatus.join(";")}`;
240
+ state.append(detail);
142
241
  }
143
- const controls = document.createElement("div");
242
+ const controls = document.createElement("td");
144
243
  controls.className = "controls";
145
- if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules"))
244
+ if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) {
146
245
  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
- }));
246
+ 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(); }));
247
+ }
156
248
  for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
157
249
  controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
158
250
  }
@@ -163,12 +255,18 @@
163
255
  await load();
164
256
  }
165
257
  }));
166
- item.append(main, controls);
258
+ item.append(name, type, version, state, controls);
167
259
  pluginsNode.append(item);
168
260
  function controlsUpgrade(id) { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
169
261
  }
170
- if (!items.length)
171
- pluginsNode.textContent = "暂无插件";
262
+ if (!items.length) {
263
+ const row = document.createElement("tr");
264
+ const cell = document.createElement("td");
265
+ cell.colSpan = 5;
266
+ cell.textContent = "暂无插件";
267
+ row.append(cell);
268
+ pluginsNode.append(row);
269
+ }
172
270
  }
173
271
  function renderMessages(messages) {
174
272
  messagesNode.replaceChildren();
@@ -185,41 +283,56 @@
185
283
  messagesNode.append(row);
186
284
  }
187
285
  }
188
- for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
286
+ function renderLogs(data) {
287
+ logsNode.replaceChildren();
288
+ logsEmpty.hidden = data.logs.length > 0;
289
+ logPage = data.page;
290
+ logHasNext = data.hasNext;
291
+ logsPageNode.textContent = `第 ${data.page} 页`;
292
+ for (const log of data.logs) {
293
+ const row = document.createElement("tr");
294
+ for (const value of [new Date(log.time).toLocaleString(), log.level, log.source, log.message, log.fields ? JSON.stringify(log.fields) : ""]) {
295
+ const cell = document.createElement("td");
296
+ cell.textContent = value;
297
+ row.append(cell);
298
+ }
299
+ logsNode.append(row);
300
+ }
301
+ }
302
+ async function loadLogs(page = logPage) { renderLogs(await request(`/api/logs?page=${page}&pageSize=100`)); }
303
+ for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
189
304
  node(`#${tabId}`).onclick = () => {
190
- for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
305
+ for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
191
306
  const selected = otherTab === tabId;
192
307
  node(`#${otherTab}`).setAttribute("aria-selected", String(selected));
193
308
  node(`#${otherPanel}`).hidden = !selected;
194
309
  }
310
+ if (tabId === "logs-tab")
311
+ void loadLogs().catch((cause) => error(String(cause)));
195
312
  };
196
313
  }
314
+ node("#logs-refresh").onclick = () => void loadLogs().catch((cause) => error(String(cause)));
315
+ node("#logs-prev").onclick = () => { if (logPage > 1)
316
+ void loadLogs(logPage - 1).catch((cause) => error(String(cause))); };
317
+ node("#logs-next").onclick = () => { if (logHasNext)
318
+ void loadLogs(logPage + 1).catch((cause) => error(String(cause))); };
197
319
  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(); };
320
+ node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
321
+ node("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
322
+ node("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?"))
323
+ void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
201
324
  node("#pair-button").onclick = () => {
202
325
  const code = pairCodeNode.value.trim();
203
326
  if (!/^\d{6}$/.test(code))
204
327
  return error("请输入 6 位配置码");
205
328
  void request("/api/pair", "POST", { code }).then((result) => {
206
- tokenNode.value = result.token;
207
- sessionStorage.setItem("mab-token", result.token);
329
+ authToken = result.token;
330
+ localStorage.setItem("mab-login-token", result.token);
331
+ localStorage.setItem("mab-login-expires-at", String(result.expiresAt));
208
332
  pairCodeNode.value = "";
209
333
  return load();
210
334
  }).catch((cause) => error(String(cause)));
211
335
  };
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
336
  node("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
224
337
  packageSearchNode.onkeydown = (event) => { if (event.key === "Enter")
225
338
  void loadCatalog().catch((cause) => error(String(cause))); };
@@ -228,17 +341,16 @@
228
341
  void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
229
342
  .then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
230
343
  };
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
344
  void load();
243
- void loadCatalog().catch((cause) => error(String(cause)));
345
+ setInterval(() => { if (!document.hidden)
346
+ void load(false); }, 10_000);
347
+ setInterval(() => { if (!document.hidden)
348
+ void loadCatalog().catch((cause) => error(String(cause))); }, 300000);
349
+ function newerVersion(remote, current) {
350
+ const parse = (value) => { const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value); return match ? match.slice(1).map(Number) : undefined; };
351
+ const a = parse(remote), b = parse(current);
352
+ if (!a || !b)
353
+ return false;
354
+ return a.some((part, index) => part !== b[index] && part > b[index] && a.slice(0, index).every((item, i) => item === b[i]));
355
+ }
244
356
  })();
@@ -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; dependencyStatus?: string[]; manifest?: { dependencies?: Record<string, string>; 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,52 @@
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, sessions] = await Promise.all([
86
+ request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"),
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
+ const sessionTable = node<HTMLTableSectionElement>("#sessions"); sessionTable.replaceChildren();
98
+ for (const session of sessions.sessions ?? []) {
99
+ const row = document.createElement("tr");
100
+ for (const value of [session.source, session.agentPlugin, session.sessionId, new Date(session.updatedAt).toLocaleString()]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); }
101
+ sessionTable.append(row);
102
+ }
103
+ if (refreshCatalog) void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
80
104
  } catch (cause) { node<HTMLElement>("#access-panel").hidden = false; error(cause instanceof Error ? cause.message : String(cause)); }
105
+ finally { loading = false; }
106
+ }
107
+ function renderChannels(items: Plugin[]): void {
108
+ const channels = items.filter((item) => item.type === "channel");
109
+ if (!channels.some((item) => item.id === selectedChannelId)) selectedChannelId = channels[0]?.id ?? "";
110
+ channelTabs.replaceChildren(...channels.map((plugin) => {
111
+ const tab = document.createElement("button"); tab.type = "button"; tab.role = "tab"; tab.textContent = plugin.displayName || plugin.id;
112
+ tab.setAttribute("aria-selected", String(plugin.id === selectedChannelId));
113
+ tab.onclick = () => { selectedChannelId = plugin.id; renderChannels(items); };
114
+ return tab;
115
+ }));
116
+ const selected = channels.find((item) => item.id === selectedChannelId);
117
+ if (!selected) { channelPanel.textContent = "暂无已安装的 Channel 插件"; return; }
118
+ if (selected.status !== "ready") { channelPanel.textContent = selected.error || `插件状态:${selected.status}。请先启动插件。`; return; }
119
+ const page = selected.manifest?.managementPage;
120
+ if (!page) { channelPanel.textContent = "插件未提供设置页面"; return; }
121
+ const current = channelPanel.querySelector<HTMLIFrameElement>("iframe.channel-frame");
122
+ if (current?.getAttribute("src") === page) return;
123
+ const frame = document.createElement("iframe"); frame.className = "channel-frame"; frame.title = `${selected.displayName || selected.id} 设置`; frame.src = page;
124
+ frame.onerror = () => { channelPanel.textContent = "设置页面加载失败,请刷新重试"; };
125
+ channelPanel.replaceChildren(frame);
81
126
  }
82
127
  function renderVersions(data: any): void {
83
128
  const entries = [data.core, ...(data.plugins ?? []).map((plugin: any) => plugin.versionStatus)].filter(Boolean);
@@ -90,41 +135,32 @@
90
135
  }
91
136
  function renderPlugins(items: Plugin[]): void {
92
137
  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
138
  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}`;
139
+ for (const plugin of items) {
140
+ const item = document.createElement("tr");
141
+ const name = document.createElement("td"); name.textContent = plugin.displayName || plugin.id;
142
+ const id = document.createElement("small"); id.className = "muted plugin-id"; id.textContent = plugin.id; name.append(id);
143
+ const type = document.createElement("td"); type.textContent = ({ agent: "Agent", channel: "Channel", management: "管理" } as Record<string, string>)[plugin.type ?? ""] ?? plugin.type ?? "其他";
144
+ const version = document.createElement("td"); version.textContent = `v${plugin.version}`;
145
+ const state = document.createElement("td");
107
146
  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
- }));
147
+ const dependencies = Object.entries(plugin.manifest?.dependencies ?? {});
148
+ if (dependencies.length) { const detail = document.createElement("small"); detail.className = "muted plugin-id"; detail.textContent = `依赖:${dependencies.map(([id, range]) => `${id} ${range}`).join(";")}`; name.append(detail); }
149
+ state.append(status);
150
+ if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text plugin-id"; detail.textContent = plugin.error; state.append(detail); }
151
+ if (plugin.dependencyStatus?.length) { const detail = document.createElement("small"); detail.className = "error-text plugin-id"; detail.textContent = `依赖:${plugin.dependencyStatus.join(";")}`; state.append(detail); }
152
+ const controls = document.createElement("td"); controls.className = "controls";
153
+ 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
154
  for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
119
155
  controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
120
156
  }
121
157
  if (plugin.path.includes(".agent-bridge") && plugin.path.includes("node_modules")) controls.append(button("卸载", `卸载 ${plugin.id}`, async () => {
122
158
  if (confirm(`卸载 ${plugin.displayName}?`)) { await request(`/api/plugins/${encodeURIComponent(plugin.id)}`, "DELETE"); await load(); }
123
159
  }));
124
- item.append(main, controls); pluginsNode.append(item);
160
+ item.append(name, type, version, state, controls); pluginsNode.append(item);
125
161
  function controlsUpgrade(id: string): void { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
126
162
  }
127
- if (!items.length) pluginsNode.textContent = "暂无插件";
163
+ 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
164
  }
129
165
  function renderMessages(messages: Array<{ time: string; pluginId: string; direction: "request" | "reply" | "error"; text: string; peer?: string; agentAid?: string }>): void {
130
166
  messagesNode.replaceChildren();
@@ -138,36 +174,35 @@
138
174
  messagesNode.append(row);
139
175
  }
140
176
  }
141
- for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
177
+ function renderLogs(data: { logs: Array<{ time: string; level: string; source: string; message: string; fields?: Record<string, unknown> }>; page: number; hasNext: boolean }): void {
178
+ logsNode.replaceChildren(); logsEmpty.hidden = data.logs.length > 0; logPage = data.page; logHasNext = data.hasNext; logsPageNode.textContent = `第 ${data.page} 页`;
179
+ 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); }
180
+ }
181
+ async function loadLogs(page = logPage): Promise<void> { renderLogs(await request(`/api/logs?page=${page}&pageSize=100`)); }
182
+ for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
142
183
  node<HTMLButtonElement>(`#${tabId}`).onclick = () => {
143
- for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
184
+ for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"], ["logs-tab", "logs-panel"]]) {
144
185
  const selected = otherTab === tabId;
145
186
  node<HTMLButtonElement>(`#${otherTab}`).setAttribute("aria-selected", String(selected));
146
187
  node<HTMLDivElement>(`#${otherPanel}`).hidden = !selected;
147
188
  }
189
+ if (tabId === "logs-tab") void loadLogs().catch((cause) => error(String(cause)));
148
190
  };
149
191
  }
192
+ node<HTMLButtonElement>("#logs-refresh").onclick = () => void loadLogs().catch((cause) => error(String(cause)));
193
+ node<HTMLButtonElement>("#logs-prev").onclick = () => { if (logPage > 1) void loadLogs(logPage - 1).catch((cause) => error(String(cause))); };
194
+ node<HTMLButtonElement>("#logs-next").onclick = () => { if (logHasNext) void loadLogs(logPage + 1).catch((cause) => error(String(cause))); };
150
195
  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(); };
196
+ node<HTMLButtonElement>("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
197
+ node<HTMLButtonElement>("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
198
+ node<HTMLButtonElement>("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?")) void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
154
199
  node<HTMLButtonElement>("#pair-button").onclick = () => {
155
200
  const code = pairCodeNode.value.trim();
156
201
  if (!/^\d{6}$/.test(code)) return error("请输入 6 位配置码");
157
202
  void request("/api/pair", "POST", { code }).then((result) => {
158
- tokenNode.value = result.token; sessionStorage.setItem("mab-token", result.token); pairCodeNode.value = ""; return load();
203
+ authToken = result.token; localStorage.setItem("mab-login-token", result.token); localStorage.setItem("mab-login-expires-at", String(result.expiresAt)); pairCodeNode.value = ""; return load();
159
204
  }).catch((cause) => error(String(cause)));
160
205
  };
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
206
  node<HTMLButtonElement>("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
172
207
  packageSearchNode.onkeydown = (event) => { if (event.key === "Enter") void loadCatalog().catch((cause) => error(String(cause))); };
173
208
  node<HTMLFormElement>("#install-local-form").onsubmit = (event) => {
@@ -175,16 +210,13 @@
175
210
  void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
176
211
  .then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
177
212
  };
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)));
213
+ void load();
214
+ setInterval(() => { if (!document.hidden) void load(false); }, 10_000);
215
+ setInterval(() => { if (!document.hidden) void loadCatalog().catch((cause) => error(String(cause))); }, 300000);
216
+
217
+ function newerVersion(remote: string, current: string): boolean {
218
+ const parse = (value: string) => { const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value); return match ? match.slice(1).map(Number) : undefined; };
219
+ const a = parse(remote), b = parse(current); if (!a || !b) return false;
220
+ return a.some((part, index) => part !== b[index] && part > b[index] && a.slice(0, index).every((item, i) => item === b[i]));
221
+ }
190
222
  })();
@@ -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
- <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>
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><h2>通道会话</h2><div class="table-scroll"><table><thead><tr><th>来源</th><th>Agent</th><th>会话 ID</th><th>最近活跃时间</th></tr></thead><tbody id="sessions"></tbody></table></div></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.4",
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.4",
4
4
  "type": "management",
5
5
  "displayName": "Agent Bridge Web",
6
6
  "entry": "dist/src-ts/plugins/abweb/index.js",