agent-bridge-abweb 1.0.0 → 1.0.3

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