agent-bridge-abweb 0.0.0-stage → 1.0.0

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 CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # abweb 管理插件
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 供 `mu-agent-bridge` 使用的网页管理插件。核心包已内置此插件;默认页面为 `http://127.0.0.1:26107/abweb/`。
4
+
5
+ 独立安装:`npm install -g agent-bridge-abweb`。
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "additionalProperties": false,
5
+ "properties": {
6
+ "publicPath": {
7
+ "type": "string",
8
+ "description": "URL prefix used to serve the management UI",
9
+ "default": "/abweb",
10
+ "pattern": "^/[A-Za-z0-9._/-]*$"
11
+ },
12
+ "controlBaseUrl": {
13
+ "type": "string",
14
+ "description": "Local daemon control API base URL",
15
+ "default": "http://127.0.0.1:26107"
16
+ },
17
+ "token": {
18
+ "type": "string",
19
+ "description": "Bearer token for the daemon control API",
20
+ "writeOnly": true
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentError = void 0;
4
+ class AgentError extends Error {
5
+ constructor(message, options) {
6
+ super(message, options);
7
+ this.name = "AgentError";
8
+ }
9
+ }
10
+ exports.AgentError = AgentError;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ControlApiClient = exports.ControlApiError = void 0;
4
+ class ControlApiError extends Error {
5
+ status;
6
+ payload;
7
+ constructor(message, status, payload) {
8
+ super(message);
9
+ this.name = "ControlApiError";
10
+ this.status = status;
11
+ this.payload = payload;
12
+ }
13
+ }
14
+ exports.ControlApiError = ControlApiError;
15
+ /** Small fetch-based client used by abweb's server and browser-facing handlers. */
16
+ class ControlApiClient {
17
+ baseUrl;
18
+ token;
19
+ fetchImpl;
20
+ constructor(baseUrl, token, fetchImpl = fetch) {
21
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
22
+ this.token = token;
23
+ this.fetchImpl = fetchImpl;
24
+ }
25
+ listPlugins() {
26
+ return this.request("/api/plugins");
27
+ }
28
+ getHealth() {
29
+ return this.request("/api/health");
30
+ }
31
+ startPlugin(id) {
32
+ return this.mutate(`/api/plugins/${encodeURIComponent(id)}/start`);
33
+ }
34
+ stopPlugin(id) {
35
+ return this.mutate(`/api/plugins/${encodeURIComponent(id)}/stop`);
36
+ }
37
+ reloadPlugin(id) {
38
+ return this.mutate(`/api/plugins/${encodeURIComponent(id)}/reload`);
39
+ }
40
+ uninstallPlugin(id) {
41
+ return this.request(`/api/plugins/${encodeURIComponent(id)}`, { method: "DELETE" });
42
+ }
43
+ installPlugin(source) {
44
+ return this.mutate("/api/plugins/install", { source });
45
+ }
46
+ getConfig(id) {
47
+ return this.request(`/api/plugins/${encodeURIComponent(id)}/config`);
48
+ }
49
+ updateConfig(id, config) {
50
+ return this.request(`/api/plugins/${encodeURIComponent(id)}/config`, {
51
+ method: "PATCH",
52
+ body: JSON.stringify(config),
53
+ });
54
+ }
55
+ getLogs(id, limit = 100) {
56
+ return this.request(`/api/plugins/${encodeURIComponent(id)}/logs?limit=${Math.max(1, Math.min(500, limit))}`);
57
+ }
58
+ mutate(path, payload) {
59
+ return this.request(path, {
60
+ method: "POST",
61
+ body: payload === undefined ? undefined : JSON.stringify(payload),
62
+ });
63
+ }
64
+ async request(path, options = {}) {
65
+ const headers = new Headers(options.headers);
66
+ headers.set("accept", "application/json");
67
+ if (options.body !== undefined)
68
+ headers.set("content-type", "application/json");
69
+ if (this.token)
70
+ headers.set("authorization", `Bearer ${this.token}`);
71
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, { ...options, headers });
72
+ const text = await response.text();
73
+ let payload = undefined;
74
+ if (text) {
75
+ try {
76
+ payload = JSON.parse(text);
77
+ }
78
+ catch {
79
+ payload = text;
80
+ }
81
+ }
82
+ if (!response.ok) {
83
+ const message = typeof payload === "object" && payload !== null && "error" in payload
84
+ ? String(payload.error)
85
+ : `control API request failed (${response.status})`;
86
+ throw new ControlApiError(message, response.status, payload);
87
+ }
88
+ return payload;
89
+ }
90
+ }
91
+ exports.ControlApiClient = ControlApiClient;
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AbwebPlugin = void 0;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const MANIFEST = {
7
+ id: "abweb",
8
+ version: "1.0.0",
9
+ displayName: "Agent Bridge Web",
10
+ entry: "dist/index.js",
11
+ apiVersion: "1",
12
+ process: "in-process",
13
+ capabilities: ["web", "management", "health"],
14
+ configSchema: "config.schema.json",
15
+ };
16
+ const DEFAULT_PUBLIC_PATH = "/abweb";
17
+ const ASSET_NAMES = {
18
+ "": { file: "index.html", contentType: "text/html; charset=utf-8" },
19
+ "/": { file: "index.html", contentType: "text/html; charset=utf-8" },
20
+ "/index.html": { file: "index.html", contentType: "text/html; charset=utf-8" },
21
+ "/app.js": { file: "app.js", contentType: "text/javascript; charset=utf-8" },
22
+ "/app.ts": { file: "app.ts", contentType: "text/javascript; charset=utf-8" },
23
+ "/style.css": { file: "style.css", contentType: "text/css; charset=utf-8" },
24
+ };
25
+ class AbwebPlugin {
26
+ manifest = MANIFEST;
27
+ context;
28
+ publicPath = DEFAULT_PUBLIC_PATH;
29
+ unregister = [];
30
+ status = "stopped";
31
+ async activate(context) {
32
+ if (this.status === "ready")
33
+ return;
34
+ this.status = "starting";
35
+ this.context = context;
36
+ this.publicPath = normalizePublicPath(context.config.publicPath);
37
+ try {
38
+ const register = context.http.registerRoute ?? context.http.register;
39
+ if (!register)
40
+ throw new Error("abweb requires a plugin HTTP route registry");
41
+ for (const suffix of Object.keys(ASSET_NAMES)) {
42
+ this.addRoute(register, `${this.publicPath}${suffix}`);
43
+ }
44
+ this.status = "ready";
45
+ context.logger?.info?.("abweb management UI activated", { publicPath: this.publicPath });
46
+ }
47
+ catch (error) {
48
+ this.unregisterAll();
49
+ this.context = undefined;
50
+ this.status = "failed";
51
+ throw error;
52
+ }
53
+ }
54
+ async deactivate(reason) {
55
+ this.unregisterAll();
56
+ this.context?.logger?.info?.("abweb management UI deactivated", { reason });
57
+ this.context = undefined;
58
+ this.status = "stopped";
59
+ }
60
+ async health() {
61
+ return {
62
+ status: this.status,
63
+ message: this.status === "ready" ? undefined : "abweb management UI is not active",
64
+ details: { routeCount: this.unregister.length, publicPath: this.publicPath },
65
+ };
66
+ }
67
+ /** Abweb is a management plugin and intentionally does not handle model requests. */
68
+ async handle() {
69
+ throw new Error("abweb does not handle agent requests");
70
+ }
71
+ addRoute(register, path) {
72
+ const cleanup = register.call(this.context?.http, path, (request) => this.serve(request));
73
+ if (typeof cleanup === "function")
74
+ this.unregister.push(cleanup);
75
+ else
76
+ this.unregister.push(() => undefined);
77
+ }
78
+ unregisterAll() {
79
+ for (const unregister of this.unregister.splice(0)) {
80
+ try {
81
+ unregister();
82
+ }
83
+ catch { /* cleanup is best effort */ }
84
+ }
85
+ }
86
+ serve(request) {
87
+ if ((request.method ?? "GET").toUpperCase() !== "GET")
88
+ return response(405, "method not allowed", "text/plain; charset=utf-8");
89
+ const rawPath = request.path ?? this.publicPath;
90
+ const pathWithoutQuery = rawPath.split("?", 1)[0];
91
+ const suffix = pathWithoutQuery === this.publicPath ? "" : pathWithoutQuery.startsWith(`${this.publicPath}/`)
92
+ ? pathWithoutQuery.slice(this.publicPath.length) : "__unknown__";
93
+ const asset = ASSET_NAMES[suffix];
94
+ if (!asset)
95
+ return response(404, "not found", "text/plain; charset=utf-8");
96
+ try {
97
+ const body = readAsset(asset.file);
98
+ return response(200, body, asset.contentType);
99
+ }
100
+ catch (error) {
101
+ this.context?.logger?.error?.("abweb static asset unavailable", { asset: asset.file, error: String(error) });
102
+ return response(503, "asset unavailable", "text/plain; charset=utf-8");
103
+ }
104
+ }
105
+ }
106
+ exports.AbwebPlugin = AbwebPlugin;
107
+ function normalizePublicPath(value) {
108
+ if (typeof value !== "string" || !value.trim())
109
+ return DEFAULT_PUBLIC_PATH;
110
+ const normalized = value.trim().replace(/\\/g, "/").replace(/\/+$/, "");
111
+ if (!normalized.startsWith("/") || normalized.includes("..") || /[^a-zA-Z0-9._/-]/.test(normalized))
112
+ return DEFAULT_PUBLIC_PATH;
113
+ return normalized || DEFAULT_PUBLIC_PATH;
114
+ }
115
+ function readAsset(file) {
116
+ const candidates = [
117
+ (0, node_path_1.join)(__dirname, "public", file),
118
+ (0, node_path_1.join)(process.cwd(), "src-ts", "plugins", "abweb", "public", file),
119
+ (0, node_path_1.join)(process.cwd(), "plugins", "abweb", "public", file),
120
+ ];
121
+ for (const candidate of candidates) {
122
+ try {
123
+ return (0, node_fs_1.readFileSync)(candidate, "utf8");
124
+ }
125
+ catch { /* try next location */ }
126
+ }
127
+ throw new Error(`asset not found: ${file}`);
128
+ }
129
+ function response(status, body, contentType) {
130
+ return { status, body, headers: { "content-type": contentType, "cache-control": "no-store" } };
131
+ }
132
+ exports.default = AbwebPlugin;
@@ -0,0 +1,213 @@
1
+ (() => {
2
+ const node = (selector) => document.querySelector(selector);
3
+ const pluginsNode = node("#plugins");
4
+ const messagesNode = node("#messages");
5
+ const messagesEmpty = node("#messages-empty");
6
+ const errorNode = node("#error");
7
+ const tokenNode = node("#token");
8
+ const pairCodeNode = node("#pair-code");
9
+ const packageNode = node("#package-name");
10
+ const pluginPathNode = node("#plugin-path");
11
+ const picker = node("#plugin-picker");
12
+ const aidList = node("#aids");
13
+ const versionsNode = node("#versions");
14
+ tokenNode.value = sessionStorage.getItem("mab-token") ?? "";
15
+ let aids = [];
16
+ let plugins = [];
17
+ let pluginConfigs = {};
18
+ let pluginVersions = {};
19
+ function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
20
+ async function request(path, method = "GET", body) {
21
+ const response = await fetch(path, { method, headers: {
22
+ accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
23
+ ...(tokenNode.value ? { authorization: `Bearer ${tokenNode.value}` } : {}),
24
+ }, ...(body ? { body: JSON.stringify(body) } : {}) });
25
+ const data = await response.json();
26
+ if (!response.ok)
27
+ throw new Error(data.error ?? `请求失败:${response.status}`);
28
+ return data;
29
+ }
30
+ function button(label, title, run) {
31
+ const item = document.createElement("button");
32
+ item.type = "button";
33
+ item.textContent = label;
34
+ item.title = title;
35
+ item.onclick = () => { void run().catch((cause) => error(String(cause))); };
36
+ return item;
37
+ }
38
+ async function load() {
39
+ try {
40
+ 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"),
43
+ ]);
44
+ renderVersions(versions);
45
+ pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin) => [plugin.id, plugin.versionStatus]));
46
+ node("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
47
+ node("#access-panel").hidden = true;
48
+ aids = identities.aids ?? [];
49
+ 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
+ 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} 个`;
78
+ renderMessages(messages.messages ?? []);
79
+ }
80
+ catch (cause) {
81
+ node("#access-panel").hidden = false;
82
+ error(cause instanceof Error ? cause.message : String(cause));
83
+ }
84
+ }
85
+ function renderVersions(data) {
86
+ const entries = [data.core, ...(data.plugins ?? []).map((plugin) => plugin.versionStatus)].filter(Boolean);
87
+ versionsNode.replaceChildren(...entries.map((entry) => {
88
+ const row = document.createElement("div");
89
+ row.className = "version-row";
90
+ row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${entry.state}${entry.error ? ` · ${entry.error}` : ""}`;
91
+ return row;
92
+ }));
93
+ if (!entries.length)
94
+ versionsNode.textContent = "尚未检查版本";
95
+ }
96
+ function renderPlugins(items) {
97
+ 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
+ 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");
108
+ name.textContent = plugin.displayName || plugin.id;
109
+ const meta = document.createElement("span");
110
+ meta.className = "muted";
111
+ meta.textContent = `${plugin.id} · 当前版本 v${plugin.version}`;
112
+ const status = document.createElement("span");
113
+ status.className = `status status-${plugin.status}`;
114
+ status.textContent = plugin.status;
115
+ main.append(name, meta, status);
116
+ if (plugin.error) {
117
+ const detail = document.createElement("small");
118
+ detail.className = "error-text";
119
+ detail.textContent = plugin.error;
120
+ main.append(detail);
121
+ }
122
+ const controls = document.createElement("div");
123
+ controls.className = "controls";
124
+ if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules"))
125
+ 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
+ }));
135
+ for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
136
+ controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
137
+ }
138
+ if (plugin.path.includes(".agent-bridge") && plugin.path.includes("node_modules"))
139
+ controls.append(button("卸载", `卸载 ${plugin.id}`, async () => {
140
+ if (confirm(`卸载 ${plugin.displayName}?`)) {
141
+ await request(`/api/plugins/${encodeURIComponent(plugin.id)}`, "DELETE");
142
+ await load();
143
+ }
144
+ }));
145
+ item.append(main, controls);
146
+ pluginsNode.append(item);
147
+ function controlsUpgrade(id) { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
148
+ }
149
+ if (!items.length)
150
+ pluginsNode.textContent = "暂无插件";
151
+ }
152
+ function renderMessages(messages) {
153
+ messagesNode.replaceChildren();
154
+ messagesEmpty.hidden = messages.length > 0;
155
+ for (const message of messages) {
156
+ const row = document.createElement("tr");
157
+ if (message.direction === "error")
158
+ row.className = "message-error";
159
+ for (const value of [new Date(message.time).toLocaleString(), message.pluginId, message.agentAid || "未提供", message.peer || "未提供", { request: "请求", reply: "回复", error: "错误" }[message.direction], message.direction === "error" ? "—" : message.text, message.direction === "error" ? message.text : "—"]) {
160
+ const cell = document.createElement("td");
161
+ cell.textContent = value;
162
+ row.append(cell);
163
+ }
164
+ messagesNode.append(row);
165
+ }
166
+ }
167
+ for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
168
+ node(`#${tabId}`).onclick = () => {
169
+ for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
170
+ const selected = otherTab === tabId;
171
+ node(`#${otherTab}`).setAttribute("aria-selected", String(selected));
172
+ node(`#${otherPanel}`).hidden = !selected;
173
+ }
174
+ };
175
+ }
176
+ 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(); };
180
+ node("#pair-button").onclick = () => {
181
+ const code = pairCodeNode.value.trim();
182
+ if (!/^\d{6}$/.test(code))
183
+ return error("请输入 6 位配置码");
184
+ void request("/api/pair", "POST", { code }).then((result) => {
185
+ tokenNode.value = result.token;
186
+ sessionStorage.setItem("mab-token", result.token);
187
+ pairCodeNode.value = "";
188
+ return load();
189
+ }).catch((cause) => error(String(cause)));
190
+ };
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
+ };
196
+ node("#install-local-form").onsubmit = (event) => {
197
+ event.preventDefault();
198
+ void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
199
+ .then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
200
+ };
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
+ void load();
213
+ })();
@@ -0,0 +1,165 @@
1
+ (() => {
2
+ type Plugin = { id: string; displayName: string; version: string; status: string; error?: string; path: string };
3
+ const node = <T extends HTMLElement>(selector: string): T => document.querySelector<T>(selector)!;
4
+ const pluginsNode = node<HTMLDivElement>("#plugins");
5
+ const messagesNode = node<HTMLTableSectionElement>("#messages");
6
+ const messagesEmpty = node<HTMLParagraphElement>("#messages-empty");
7
+ const errorNode = node<HTMLParagraphElement>("#error");
8
+ const tokenNode = node<HTMLInputElement>("#token");
9
+ const pairCodeNode = node<HTMLInputElement>("#pair-code");
10
+ const packageNode = node<HTMLSelectElement>("#package-name");
11
+ const pluginPathNode = node<HTMLInputElement>("#plugin-path");
12
+ const picker = node<HTMLSelectElement>("#plugin-picker");
13
+ const aidList = node<HTMLDivElement>("#aids");
14
+ const versionsNode = node<HTMLDivElement>("#versions");
15
+ tokenNode.value = sessionStorage.getItem("mab-token") ?? "";
16
+ let aids: string[] = [];
17
+ let plugins: Plugin[] = [];
18
+ let pluginConfigs: Record<string, { aid?: string }> = {};
19
+ let pluginVersions: Record<string, any> = {};
20
+
21
+ function error(message: string): void { errorNode.textContent = message; errorNode.hidden = !message; }
22
+ async function request(path: string, method = "GET", body?: unknown): Promise<any> {
23
+ const response = await fetch(path, { method, headers: {
24
+ accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
25
+ ...(tokenNode.value ? { authorization: `Bearer ${tokenNode.value}` } : {}),
26
+ }, ...(body ? { body: JSON.stringify(body) } : {}) });
27
+ const data = await response.json();
28
+ if (!response.ok) throw new Error(data.error ?? `请求失败:${response.status}`);
29
+ return data;
30
+ }
31
+ function button(label: string, title: string, run: () => Promise<void>): HTMLButtonElement {
32
+ const item = document.createElement("button"); item.type = "button"; item.textContent = label; item.title = title;
33
+ item.onclick = () => { void run().catch((cause) => error(String(cause))); }; return item;
34
+ }
35
+ async function load(): Promise<void> {
36
+ try {
37
+ 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"),
40
+ ]);
41
+ renderVersions(versions);
42
+ pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin: any) => [plugin.id, plugin.versionStatus]));
43
+ node<HTMLElement>("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
44
+ node<HTMLElement>("#access-panel").hidden = true;
45
+ aids = identities.aids ?? [];
46
+ 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
+ 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} 个`;
66
+ renderMessages(messages.messages ?? []);
67
+ } catch (cause) { node<HTMLElement>("#access-panel").hidden = false; error(cause instanceof Error ? cause.message : String(cause)); }
68
+ }
69
+ function renderVersions(data: any): void {
70
+ const entries = [data.core, ...(data.plugins ?? []).map((plugin: any) => plugin.versionStatus)].filter(Boolean);
71
+ versionsNode.replaceChildren(...entries.map((entry: any) => {
72
+ const row = document.createElement("div"); row.className = "version-row";
73
+ row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${entry.state}${entry.error ? ` · ${entry.error}` : ""}`;
74
+ return row;
75
+ }));
76
+ if (!entries.length) versionsNode.textContent = "尚未检查版本";
77
+ }
78
+ function renderPlugins(items: Plugin[]): void {
79
+ 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
+ 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}`;
89
+ 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
+ }));
100
+ for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
101
+ controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
102
+ }
103
+ if (plugin.path.includes(".agent-bridge") && plugin.path.includes("node_modules")) controls.append(button("卸载", `卸载 ${plugin.id}`, async () => {
104
+ if (confirm(`卸载 ${plugin.displayName}?`)) { await request(`/api/plugins/${encodeURIComponent(plugin.id)}`, "DELETE"); await load(); }
105
+ }));
106
+ item.append(main, controls); pluginsNode.append(item);
107
+ function controlsUpgrade(id: string): void { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
108
+ }
109
+ if (!items.length) pluginsNode.textContent = "暂无插件";
110
+ }
111
+ function renderMessages(messages: Array<{ time: string; pluginId: string; direction: "request" | "reply" | "error"; text: string; peer?: string; agentAid?: string }>): void {
112
+ messagesNode.replaceChildren();
113
+ messagesEmpty.hidden = messages.length > 0;
114
+ for (const message of messages) {
115
+ const row = document.createElement("tr");
116
+ if (message.direction === "error") row.className = "message-error";
117
+ for (const value of [new Date(message.time).toLocaleString(), message.pluginId, message.agentAid || "未提供", message.peer || "未提供", { request: "请求", reply: "回复", error: "错误" }[message.direction], message.direction === "error" ? "—" : message.text, message.direction === "error" ? message.text : "—"]) {
118
+ const cell = document.createElement("td"); cell.textContent = value; row.append(cell);
119
+ }
120
+ messagesNode.append(row);
121
+ }
122
+ }
123
+ for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
124
+ node<HTMLButtonElement>(`#${tabId}`).onclick = () => {
125
+ for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
126
+ const selected = otherTab === tabId;
127
+ node<HTMLButtonElement>(`#${otherTab}`).setAttribute("aria-selected", String(selected));
128
+ node<HTMLDivElement>(`#${otherPanel}`).hidden = !selected;
129
+ }
130
+ };
131
+ }
132
+ 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(); };
136
+ node<HTMLButtonElement>("#pair-button").onclick = () => {
137
+ const code = pairCodeNode.value.trim();
138
+ if (!/^\d{6}$/.test(code)) return error("请输入 6 位配置码");
139
+ void request("/api/pair", "POST", { code }).then((result) => {
140
+ tokenNode.value = result.token; sessionStorage.setItem("mab-token", result.token); pairCodeNode.value = ""; return load();
141
+ }).catch((cause) => error(String(cause)));
142
+ };
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
+ };
148
+ node<HTMLFormElement>("#install-local-form").onsubmit = (event) => {
149
+ event.preventDefault();
150
+ void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
151
+ .then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
152
+ };
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
+ void load();
165
+ })();
@@ -0,0 +1,24 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>mab 管理</title>
7
+ <link rel="stylesheet" href="/abweb/style.css">
8
+ </head>
9
+ <body>
10
+ <main class="shell">
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>
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>
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>
20
+ <p id="error" class="error" role="alert" hidden></p>
21
+ </main>
22
+ <script src="/abweb/app.js"></script>
23
+ </body>
24
+ </html>
@@ -0,0 +1,43 @@
1
+ :root { font-family: "Segoe UI", "Microsoft YaHei", sans-serif; color: #182027; background: #f4f6f5; }
2
+ * { box-sizing: border-box; }
3
+ body { margin: 0; }
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; }
6
+ header, .section-heading { justify-content: space-between; }
7
+ .tabs { display: flex; gap: 20px; border-bottom: 1px solid #cbd5d0; margin-top: 22px; }
8
+ .tabs button { border: 0; border-bottom: 2px solid transparent; border-radius: 0; background: transparent; padding: 8px 2px; }
9
+ .tabs button[aria-selected="true"] { color: #136a44; border-bottom-color: #136a44; font-weight: 600; }
10
+ [hidden] { display: none !important; }
11
+ h1 { font-size: 25px; margin: 0; }
12
+ h2 { font-size: 17px; margin: 0; }
13
+ p { margin: 5px 0; }
14
+ section { border-top: 1px solid #d9dfdc; padding: 18px 0; }
15
+ .access { flex-wrap: wrap; }
16
+ .access input { min-width: 220px; }
17
+ input, select, button { min-height: 34px; padding: 6px 9px; border: 1px solid #bfc9c5; border-radius: 5px; background: white; color: inherit; font: inherit; }
18
+ button { cursor: pointer; white-space: nowrap; }
19
+ button:hover { background: #e5f2ea; }
20
+ form { display: flex; gap: 8px; }
21
+ 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; }
32
+ .muted { color: #65736e; font-size: 13px; }
33
+ .status { font-size: 12px; padding: 3px 7px; background: #e7ebea; border-radius: 4px; }
34
+ .status-ready { color: #136a44; background: #def3e5; }
35
+ .status-failed { color: #a83427; background: #fce9e5; }
36
+ .error, .error-text { color: #a83427; }
37
+ .table-scroll { overflow-x: auto; margin-top: 13px; }
38
+ table { width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px; font-size: 13px; }
39
+ th, td { padding: 10px 9px; text-align: left; vertical-align: top; border-bottom: 1px solid #dce3df; overflow-wrap: anywhere; white-space: pre-wrap; }
40
+ th { color: #566761; font-weight: 600; background: #edf1ef; }
41
+ 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
+ .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%; } }
package/package.json CHANGED
@@ -1,6 +1,23 @@
1
1
  {
2
2
  "name": "agent-bridge-abweb",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "agent-bridge 的网页管理插件。",
5
+ "repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/abweb" },
6
+ "main": "dist/src-ts/plugins/abweb/index.js",
7
+ "files": [
8
+ "dist/",
9
+ "plugin.json",
10
+ "config.schema.json",
11
+ "README.md"
12
+ ],
13
+ "agentBridge": {
14
+ "plugin": "./plugin.json"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public",
18
+ "registry": "https://registry.npmjs.org"
19
+ },
20
+ "engines": {
21
+ "node": ">=22"
22
+ }
23
+ }
package/plugin.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "abweb",
3
+ "version": "1.0.0",
4
+ "displayName": "Agent Bridge Web",
5
+ "entry": "dist/src-ts/plugins/abweb/index.js",
6
+ "apiVersion": "1",
7
+ "process": "in-process",
8
+ "capabilities": ["web", "management", "health"],
9
+ "configSchema": "config.schema.json"
10
+ }