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.
- package/README.md +16 -11
- package/dist/src-ts/channel-sessions.js +68 -0
- package/dist/src-ts/cli.js +60 -25
- package/dist/src-ts/control-server.js +75 -94
- package/dist/src-ts/local-aids.js +26 -0
- package/dist/src-ts/log-store.js +64 -0
- package/dist/src-ts/plugin-manager.js +240 -39
- package/dist/src-ts/plugin-manifest.js +5 -1
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +164 -100
- package/dist/src-ts/plugins/abweb/public/index.html +7 -6
- package/dist/src-ts/plugins/abweb/public/style.css +12 -13
- package/dist/src-ts/plugins/aun/evolcore.js +54 -0
- package/dist/src-ts/plugins/aun/index.js +253 -51
- package/dist/src-ts/plugins/aun/public/app.js +66 -0
- package/dist/src-ts/plugins/aun/public/index.html +14 -0
- package/dist/src-ts/plugins/aun/public/style.css +21 -0
- package/dist/src-ts/plugins/doubao/cdp-client.js +5 -4
- package/dist/src-ts/plugins/doubao/index.js +3 -2
- package/dist/src-ts/plugins/feishu/client.js +133 -0
- package/dist/src-ts/plugins/feishu/index.js +257 -0
- package/dist/src-ts/plugins/feishu/public/app.js +147 -0
- package/dist/src-ts/plugins/feishu/public/index.html +17 -0
- package/dist/src-ts/plugins/feishu/public/style.css +30 -0
- package/dist/src-ts/plugins/workbuddy/cdp-client.js +118 -0
- package/dist/src-ts/plugins/workbuddy/cli-client.js +39 -0
- package/dist/src-ts/plugins/workbuddy/index.js +139 -0
- package/dist/src-ts/version-manager.js +9 -1
- package/dist/tests-ts/abweb-plugin.test.js +13 -2
- package/dist/tests-ts/aun-plugin.test.js +108 -4
- package/dist/tests-ts/control-server.test.js +88 -6
- package/dist/tests-ts/doubao-plugin.test.js +18 -0
- package/dist/tests-ts/feishu-client.test.js +72 -0
- package/dist/tests-ts/feishu-page.test.js +53 -0
- package/dist/tests-ts/feishu-plugin.test.js +148 -0
- package/dist/tests-ts/log-store.test.js +27 -0
- package/dist/tests-ts/platform-integration.test.js +40 -1
- package/dist/tests-ts/plugin-catalog.test.js +8 -0
- package/dist/tests-ts/plugin-manager.test.js +219 -13
- package/dist/tests-ts/workbuddy-plugin.test.js +85 -0
- package/package.json +30 -6
- 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
|
|
13
|
-
const
|
|
14
|
+
const channelTabs = node("#channel-tabs");
|
|
15
|
+
const channelPanel = node("#channel-panel");
|
|
14
16
|
const versionsNode = node("#versions");
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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
|
-
...(
|
|
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,
|
|
100
|
-
request("/api/health"), request("/api/plugins"), request("/api/
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
145
|
-
|
|
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
|
|
177
|
-
const item = document.createElement("
|
|
178
|
-
|
|
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
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
234
|
+
state.append(detail);
|
|
195
235
|
}
|
|
196
|
-
|
|
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
|
-
|
|
201
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
node("#
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|
13
|
-
<nav class="tabs" role="tablist" aria-label="管理视图"><button id="settings-tab" type="button" role="tab" aria-selected="true" aria-controls="settings-panel">插件设置</button><button id="messages-tab" type="button" role="tab" aria-selected="false" aria-controls="messages-panel">通信记录</button></nav>
|
|
12
|
+
<section id="access-panel" class="access" hidden><label for="pair-code">配置码</label><input id="pair-code" inputmode="numeric" maxlength="6" pattern="[0-9]{6}" autocomplete="one-time-code" placeholder="运行 mab pair 获取"><button id="pair-button" type="button">配对</button></section>
|
|
13
|
+
<nav class="tabs" role="tablist" aria-label="管理视图"><button id="settings-tab" type="button" role="tab" aria-selected="true" aria-controls="settings-panel">插件设置</button><button id="messages-tab" type="button" role="tab" aria-selected="false" aria-controls="messages-panel">通信记录</button><button id="logs-tab" type="button" role="tab" aria-selected="false" aria-controls="logs-panel">运行日志</button></nav>
|
|
14
14
|
<div id="settings-panel" role="tabpanel" aria-labelledby="settings-tab">
|
|
15
|
-
<section><div class="section-heading"><h2>版本管理</h2><button id="check-versions" type="button">检查更新</button></div><div id="versions" class="muted"></div></section>
|
|
16
|
-
<section><
|
|
17
|
-
<section><div class="section-heading"><h2
|
|
15
|
+
<section><div class="section-heading"><h2>版本管理</h2><div><button id="check-versions" type="button">检查更新</button><button id="update-core" type="button">升级后台</button></div></div><label for="update-policy">插件更新策略</label><select id="update-policy"><option value="auto-install">自动下载并安装</option><option value="download-only">自动下载,手动安装</option><option value="manual">手动下载和安装</option></select><button id="save-update-policy" type="button">保存策略</button><span id="update-policy-status" class="muted"></span><div id="versions" class="muted"></div></section>
|
|
16
|
+
<section><details id="installed-plugins" open><summary><h2>已安装插件</h2></summary><div class="table-scroll"><table class="plugin-table"><thead><tr><th scope="col">名称</th><th scope="col">类型</th><th scope="col">版本</th><th scope="col">状态</th><th scope="col">操作</th></tr></thead><tbody id="plugins"></tbody></table></div><div class="plugin-catalog"><div class="section-heading"><h3>npm 插件目录</h3><button id="search-packages" type="button">刷新目录</button></div><input id="package-search" placeholder="搜索插件名称或功能" aria-label="搜索 npm 插件"><div id="catalog" class="catalog" aria-live="polite"></div></div><form id="install-local-form"><input id="plugin-path" required placeholder="Windows 上的插件目录路径" aria-label="本地插件目录"><button type="submit">安装本地插件</button></form></details></section>
|
|
17
|
+
<section><div class="section-heading"><h2>Channel 设置</h2></div><nav id="channel-tabs" class="tabs channel-tabs" role="tablist" aria-label="Channel 设置"></nav><div id="channel-panel" role="tabpanel" aria-live="polite"></div></section>
|
|
18
18
|
</div>
|
|
19
|
-
<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
|
|
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
|
-
.
|
|
23
|
-
.
|
|
24
|
-
.
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
.
|
|
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%; }
|
|
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
|
+
}
|