mu-agent-bridge 1.0.5 → 1.0.13

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.
Files changed (42) hide show
  1. package/README.md +16 -11
  2. package/dist/src-ts/channel-sessions.js +68 -0
  3. package/dist/src-ts/cli.js +60 -25
  4. package/dist/src-ts/control-server.js +75 -94
  5. package/dist/src-ts/local-aids.js +26 -0
  6. package/dist/src-ts/log-store.js +64 -0
  7. package/dist/src-ts/plugin-manager.js +240 -39
  8. package/dist/src-ts/plugin-manifest.js +5 -1
  9. package/dist/src-ts/plugins/abweb/index.js +1 -1
  10. package/dist/src-ts/plugins/abweb/public/app.js +164 -100
  11. package/dist/src-ts/plugins/abweb/public/index.html +7 -6
  12. package/dist/src-ts/plugins/abweb/public/style.css +12 -13
  13. package/dist/src-ts/plugins/aun/evolcore.js +54 -0
  14. package/dist/src-ts/plugins/aun/index.js +253 -51
  15. package/dist/src-ts/plugins/aun/public/app.js +66 -0
  16. package/dist/src-ts/plugins/aun/public/index.html +14 -0
  17. package/dist/src-ts/plugins/aun/public/style.css +21 -0
  18. package/dist/src-ts/plugins/doubao/cdp-client.js +5 -4
  19. package/dist/src-ts/plugins/doubao/index.js +3 -2
  20. package/dist/src-ts/plugins/feishu/client.js +133 -0
  21. package/dist/src-ts/plugins/feishu/index.js +257 -0
  22. package/dist/src-ts/plugins/feishu/public/app.js +147 -0
  23. package/dist/src-ts/plugins/feishu/public/index.html +17 -0
  24. package/dist/src-ts/plugins/feishu/public/style.css +30 -0
  25. package/dist/src-ts/plugins/workbuddy/cdp-client.js +118 -0
  26. package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
  27. package/dist/src-ts/plugins/workbuddy/index.js +139 -0
  28. package/dist/src-ts/version-manager.js +9 -1
  29. package/dist/tests-ts/abweb-plugin.test.js +13 -2
  30. package/dist/tests-ts/aun-plugin.test.js +108 -4
  31. package/dist/tests-ts/control-server.test.js +88 -6
  32. package/dist/tests-ts/doubao-plugin.test.js +18 -0
  33. package/dist/tests-ts/feishu-client.test.js +72 -0
  34. package/dist/tests-ts/feishu-page.test.js +53 -0
  35. package/dist/tests-ts/feishu-plugin.test.js +148 -0
  36. package/dist/tests-ts/log-store.test.js +27 -0
  37. package/dist/tests-ts/platform-integration.test.js +40 -1
  38. package/dist/tests-ts/plugin-catalog.test.js +8 -0
  39. package/dist/tests-ts/plugin-manager.test.js +219 -13
  40. package/dist/tests-ts/workbuddy-plugin.test.js +85 -0
  41. package/package.json +30 -6
  42. package/plugins/abweb/plugin.json +1 -1
@@ -3,20 +3,31 @@
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
11
  const packageSearchNode = node("#package-search");
10
12
  const catalogNode = node("#catalog");
11
13
  const pluginPathNode = node("#plugin-path");
12
- const picker = node("#plugin-picker");
13
- const aidList = node("#aids");
14
+ const channelTabs = node("#channel-tabs");
15
+ const channelPanel = node("#channel-panel");
14
16
  const versionsNode = node("#versions");
15
- tokenNode.value = sessionStorage.getItem("mab-token") ?? "";
16
- 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") ?? "";
17
25
  let plugins = [];
18
- let pluginConfigs = {};
26
+ let selectedChannelId = "";
19
27
  let pluginVersions = {};
28
+ let logPage = 1;
29
+ let logHasNext = false;
30
+ let loading = false;
20
31
  const installingPackages = new Set();
21
32
  function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
22
33
  async function loadCatalog() {
@@ -78,9 +89,16 @@
78
89
  async function request(path, method = "GET", body) {
79
90
  const response = await fetch(path, { method, headers: {
80
91
  accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
81
- ...(tokenNode.value ? { authorization: `Bearer ${tokenNode.value}` } : {}),
92
+ ...(authToken ? { authorization: `Bearer ${authToken}` } : {}),
82
93
  }, ...(body ? { body: JSON.stringify(body) } : {}) });
83
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
+ }
84
102
  if (!response.ok)
85
103
  throw new Error(data.error ?? `请求失败:${response.status}`);
86
104
  return data;
@@ -93,62 +111,83 @@
93
111
  item.onclick = () => { void run().catch((cause) => error(String(cause))); };
94
112
  return item;
95
113
  }
96
- async function load() {
114
+ async function load(refreshCatalog = true) {
115
+ if (loading)
116
+ return;
117
+ loading = true;
97
118
  try {
98
119
  error("");
99
- const [health, plugins, identities, messages, versions] = await Promise.all([
100
- 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"),
101
122
  ]);
123
+ policyNode.value = policy.mode;
124
+ policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
102
125
  renderVersions(versions);
103
126
  pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin) => [plugin.id, plugin.versionStatus]));
104
127
  node("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
105
128
  node("#access-panel").hidden = true;
106
- aids = identities.aids ?? [];
107
129
  const installed = plugins.plugins ?? [];
108
- pluginConfigs = Object.fromEntries(await Promise.all(installed.map(async (plugin) => [plugin.id, await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`)])));
109
130
  renderPlugins(installed);
110
- const groups = identities.groups ?? { evolcore: aids, muAgentBridge: [] };
111
- aidList.replaceChildren(...[["EvolCore", groups.evolcore], ["mu-agent-bridge", groups.muAgentBridge]].flatMap(([label, values]) => {
112
- const items = values.map((aid) => {
113
- const item = document.createElement("tr");
114
- item.className = "aid-row";
115
- const name = document.createElement("td");
116
- name.textContent = aid;
117
- const source = document.createElement("td");
118
- source.textContent = String(label);
119
- const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
120
- const current = document.createElement("td");
121
- current.className = "muted";
122
- current.textContent = linked.length ? linked.join("、") : "未关联插件";
123
- const action = document.createElement("td");
124
- const select = document.createElement("select");
125
- select.setAttribute("aria-label", `${aid} 关联插件`);
126
- select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
127
- select.onchange = () => { if (select.value)
128
- void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
129
- action.append(select);
130
- item.append(name, source, current, action);
131
- return item;
132
- });
133
- return items;
134
- }));
135
- if (!aids.length) {
131
+ renderChannels(installed);
132
+ renderMessages(messages.messages ?? []);
133
+ const sessionTable = node("#sessions");
134
+ sessionTable.replaceChildren();
135
+ for (const session of sessions.sessions ?? []) {
136
136
  const row = document.createElement("tr");
137
- const cell = document.createElement("td");
138
- cell.colSpan = 4;
139
- cell.className = "muted";
140
- cell.textContent = "当前没有已连接的 AID";
141
- row.append(cell);
142
- aidList.append(row);
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
143
  }
144
- node("#aid-count").textContent = `${aids.length} 个`;
145
- renderMessages(messages.messages ?? []);
146
- void loadCatalog().catch((cause) => error(String(cause)));
144
+ if (refreshCatalog)
145
+ void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
147
146
  }
148
147
  catch (cause) {
149
148
  node("#access-panel").hidden = false;
150
149
  error(cause instanceof Error ? cause.message : String(cause));
151
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);
152
191
  }
153
192
  function renderVersions(data) {
154
193
  const entries = [data.core, ...(data.plugins ?? []).map((plugin) => plugin.versionStatus)].filter(Boolean);
@@ -163,49 +202,49 @@
163
202
  }
164
203
  function renderPlugins(items) {
165
204
  plugins = items;
166
- const selected = picker.value || items[0]?.id;
167
- 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")))]];
168
- picker.replaceChildren(...groups.flatMap(([label, group]) => {
169
- const options = document.createElement("optgroup");
170
- options.label = label;
171
- options.append(...group.map((plugin) => new Option(`${plugin.displayName || plugin.id} · v${plugin.version} · ${plugin.status}`, plugin.id)));
172
- return group.length ? [options] : [];
173
- }));
174
- picker.value = items.some((plugin) => plugin.id === selected) ? selected : items[0]?.id ?? "";
175
205
  pluginsNode.replaceChildren();
176
- for (const plugin of items.filter((item) => item.id === picker.value)) {
177
- const item = document.createElement("article");
178
- item.className = "plugin-row";
179
- const main = document.createElement("div");
180
- main.className = "plugin-main";
181
- const name = document.createElement("strong");
206
+ for (const plugin of items) {
207
+ const item = document.createElement("tr");
208
+ const name = document.createElement("td");
182
209
  name.textContent = plugin.displayName || plugin.id;
183
- const meta = document.createElement("span");
184
- meta.className = "muted";
185
- 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");
186
219
  const status = document.createElement("span");
187
220
  status.className = `status status-${plugin.status}`;
188
221
  status.textContent = plugin.status;
189
- 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);
190
230
  if (plugin.error) {
191
231
  const detail = document.createElement("small");
192
- detail.className = "error-text";
232
+ detail.className = "error-text plugin-id";
193
233
  detail.textContent = plugin.error;
194
- main.append(detail);
234
+ state.append(detail);
195
235
  }
196
- const controls = document.createElement("div");
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);
241
+ }
242
+ const controls = document.createElement("td");
197
243
  controls.className = "controls";
198
- 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")) {
199
245
  controlsUpgrade(plugin.id);
200
- const select = document.createElement("select");
201
- select.setAttribute("aria-label", `${plugin.id} 的 AID`);
202
- select.append(new Option("不绑定 AID", ""), ...aids.map((aid) => new Option(aid, aid)));
203
- select.value = pluginConfigs[plugin.id]?.aid ?? "";
204
- if (plugin.id !== "abweb")
205
- controls.append(select, button("关联 AID", "保存此插件使用的 AID", async () => {
206
- await request(`/api/plugins/${encodeURIComponent(plugin.id)}/aid`, "PATCH", { aid: select.value });
207
- await load();
208
- }));
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
+ }
209
248
  for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
210
249
  controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
211
250
  }
@@ -216,12 +255,18 @@
216
255
  await load();
217
256
  }
218
257
  }));
219
- item.append(main, controls);
258
+ item.append(name, type, version, state, controls);
220
259
  pluginsNode.append(item);
221
260
  function controlsUpgrade(id) { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
222
261
  }
223
- if (!items.length)
224
- 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
+ }
225
270
  }
226
271
  function renderMessages(messages) {
227
272
  messagesNode.replaceChildren();
@@ -238,26 +283,52 @@
238
283
  messagesNode.append(row);
239
284
  }
240
285
  }
241
- 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"]]) {
242
304
  node(`#${tabId}`).onclick = () => {
243
- 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"]]) {
244
306
  const selected = otherTab === tabId;
245
307
  node(`#${otherTab}`).setAttribute("aria-selected", String(selected));
246
308
  node(`#${otherPanel}`).hidden = !selected;
247
309
  }
310
+ if (tabId === "logs-tab")
311
+ void loadLogs().catch((cause) => error(String(cause)));
248
312
  };
249
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))); };
250
319
  node("#refresh").onclick = () => void load();
251
- node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(load).catch((cause) => error(String(cause)));
252
- picker.onchange = () => renderPlugins(plugins);
253
- 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))); };
254
324
  node("#pair-button").onclick = () => {
255
325
  const code = pairCodeNode.value.trim();
256
326
  if (!/^\d{6}$/.test(code))
257
327
  return error("请输入 6 位配置码");
258
328
  void request("/api/pair", "POST", { code }).then((result) => {
259
- tokenNode.value = result.token;
260
- 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));
261
332
  pairCodeNode.value = "";
262
333
  return load();
263
334
  }).catch((cause) => error(String(cause)));
@@ -270,18 +341,11 @@
270
341
  void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
271
342
  .then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
272
343
  };
273
- node("#create-aid-form").onsubmit = (event) => {
274
- event.preventDefault();
275
- const aid = node("#new-aid").value.trim();
276
- const ownerAid = node("#owner-aid").value.trim();
277
- const result = node("#aid-result");
278
- result.textContent = "正在创建 AID…";
279
- void request("/api/aids", "POST", { aid, ownerAid }).then(() => {
280
- result.textContent = `${aid} 已创建;连接到 AUN 后会出现在上方列表。`;
281
- return load();
282
- }).catch((cause) => { result.textContent = ""; error(String(cause)); });
283
- };
284
344
  void load();
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);
285
349
  function newerVersion(remote, current) {
286
350
  const parse = (value) => { const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value); return match ? match.slice(1).map(Number) : undefined; };
287
351
  const a = parse(remote), b = parse(current);
@@ -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 插件"><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></section>
17
- <section><div class="section-heading"><h2>已连接 AID</h2><span id="aid-count" class="muted"></span></div><div class="aid-list"><table class="aid-table"><thead><tr><th scope="col">AID</th><th scope="col">身份来源</th><th scope="col">已关联插件</th><th scope="col">操作</th></tr></thead><tbody id="aids" aria-live="polite"></tbody></table></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></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,17 +22,13 @@ 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 { overflow-x: auto; margin-top: 12px; }
23
- .aid-table { width: 100%; min-width: 720px; border-collapse: collapse; table-layout: fixed; font-size: 13px; }
24
- .aid-table th, .aid-table td { padding: 9px 8px; text-align: left; vertical-align: middle; border-bottom: 1px solid #dce3df; overflow-wrap: anywhere; }
25
- .aid-table th { color: #566761; font-weight: 600; background: #edf1ef; }
26
- .aid-table th:nth-child(1) { width: 230px; } .aid-table th:nth-child(2) { width: 145px; } .aid-table th:nth-child(4) { width: 180px; }
27
- .aid-row select { max-width: 165px; }
28
- .plugins, .messages { margin-top: 13px; }
29
- .plugin-row { justify-content: space-between; border-bottom: 1px solid #e0e5e2; padding: 12px 0; flex-wrap: wrap; }
30
- .plugin-main { flex-wrap: wrap; }
31
- .controls { flex-wrap: wrap; }
32
- .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; }
33
32
  .plugin-catalog { border-top: 1px solid #e0e5e2; margin-top: 18px; padding-top: 14px; }
34
33
  .plugin-catalog h3 { font-size: 14px; margin: 0; }
35
34
  .plugin-catalog input { width: min(100%, 420px); margin: 10px 0; }
@@ -53,4 +52,4 @@ th, td { padding: 10px 9px; text-align: left; vertical-align: top; border-bottom
53
52
  th { color: #566761; font-weight: 600; background: #edf1ef; }
54
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; }
55
54
  .message-error td:last-child { color: #a83427; }
56
- @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%; } }
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runEc = runEc;
4
+ exports.configureAidForPlugin = configureAidForPlugin;
5
+ exports.listConnectedAids = listConnectedAids;
6
+ const node_child_process_1 = require("node:child_process");
7
+ const node_util_1 = require("node:util");
8
+ const local_aids_1 = require("../../local-aids");
9
+ const execFileAsync = (0, node_util_1.promisify)(node_child_process_1.execFile);
10
+ async function runEc(args) {
11
+ try {
12
+ const { stdout } = await execFileAsync("ec", args, { shell: process.platform === "win32", timeout: 15_000, maxBuffer: 1024 * 1024 });
13
+ return JSON.parse(stdout);
14
+ }
15
+ catch (error) {
16
+ const stdout = error.stdout;
17
+ if (stdout) {
18
+ try {
19
+ return JSON.parse(stdout);
20
+ }
21
+ catch { /* 保留原始命令错误 */ }
22
+ }
23
+ throw error;
24
+ }
25
+ }
26
+ async function configureAidForPlugin(pluginId, aid, port, run = runEc) {
27
+ if (!aid)
28
+ return;
29
+ if (!/^[a-z0-9_-]+$/.test(pluginId))
30
+ throw new Error("插件 ID 格式无效");
31
+ const path = pluginId === "dsh" ? "/dsh/v1" : pluginId === "qwenwork" ? "/v1" : `/api/agents/${pluginId}/v1`;
32
+ for (const [key, value] of [
33
+ ["baseagents.ecagent.baseUrl", `http://127.0.0.1:${port}${path}`],
34
+ ["baseagents.ecagent.apiKey", "mab-local"],
35
+ ["baseagents.ecagent.model", pluginId],
36
+ ["active_baseagent", "ecagent"],
37
+ ]) {
38
+ const result = await run(["agent", "set", aid, key, value, "--format", "json"]);
39
+ if (!result?.ok)
40
+ throw new Error(`配置 AID ${aid} 失败:${key};${result?.code ?? ""} ${result?.error ?? "未知错误"}`);
41
+ }
42
+ const result = await run(["agent", "reload", aid, "--format", "json"]);
43
+ if (!result?.ok)
44
+ throw new Error(`重载 AID ${aid} 失败:${result?.code ?? ""} ${result?.error ?? "未知错误"}`);
45
+ }
46
+ async function listConnectedAids(run = runEc) {
47
+ const result = await run(["agent", "list", "--format", "json"]);
48
+ if (!result.ok || !Array.isArray(result.agents))
49
+ throw new Error("读取 EvolCore Agent 列表失败");
50
+ const candidates = result.agents.filter((agent) => agent.status === "running" && typeof agent.aid === "string" && /^[a-zA-Z0-9.-]+$/.test(agent.aid) && agent.channels?.some((channel) => channel.startsWith("aun#")));
51
+ const details = await Promise.all(candidates.map((agent) => run(["agent", "show", agent.aid, "--format", "json"]).catch(() => null)));
52
+ const local = new Set(await (0, local_aids_1.listExistingAids)());
53
+ return details.filter((detail) => detail?.ok && detail.status === "running" && detail.connection?.status === "connected" && local.has(detail.aid)).map((detail) => detail.aid).sort();
54
+ }