agent-bridge-abweb 0.0.0-stage → 1.0.1
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 +4 -2
- package/config.schema.json +23 -0
- package/dist/src-ts/agent.js +10 -0
- package/dist/src-ts/plugins/abweb/control-client.js +91 -0
- package/dist/src-ts/plugins/abweb/index.js +132 -0
- package/dist/src-ts/plugins/abweb/public/app.js +244 -0
- package/dist/src-ts/plugins/abweb/public/app.ts +190 -0
- package/dist/src-ts/plugins/abweb/public/index.html +24 -0
- package/dist/src-ts/plugins/abweb/public/style.css +49 -0
- package/package.json +23 -4
- package/plugin.json +11 -0
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# abweb 管理插件
|
|
2
2
|
|
|
3
|
-
|
|
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.1",
|
|
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,244 @@
|
|
|
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 packageSearchNode = node("#package-search");
|
|
11
|
+
const catalogNode = node("#catalog");
|
|
12
|
+
const pluginPathNode = node("#plugin-path");
|
|
13
|
+
const picker = node("#plugin-picker");
|
|
14
|
+
const aidList = node("#aids");
|
|
15
|
+
const versionsNode = node("#versions");
|
|
16
|
+
tokenNode.value = sessionStorage.getItem("mab-token") ?? "";
|
|
17
|
+
let aids = [];
|
|
18
|
+
let plugins = [];
|
|
19
|
+
let pluginConfigs = {};
|
|
20
|
+
let pluginVersions = {};
|
|
21
|
+
function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
22
|
+
async function loadCatalog() {
|
|
23
|
+
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
24
|
+
const items = result.plugins ?? [];
|
|
25
|
+
packageNode.replaceChildren(...[["Agent", "agent"], ["Channel", "channel"], ["管理", "management"]].flatMap(([label, type]) => {
|
|
26
|
+
const group = document.createElement("optgroup");
|
|
27
|
+
group.label = label;
|
|
28
|
+
group.append(...items.filter((item) => item.type === type).map((item) => new Option(`${item.name} · v${item.version}`, item.name)));
|
|
29
|
+
return group.children.length ? [group] : [];
|
|
30
|
+
}));
|
|
31
|
+
catalogNode.replaceChildren(...items.map((plugin) => { const item = document.createElement("div"); item.className = "catalog-row"; item.textContent = `${plugin.type} · ${plugin.name} · ${plugin.description || "无描述"}`; return item; }));
|
|
32
|
+
if (!(result.plugins ?? []).length)
|
|
33
|
+
catalogNode.textContent = "未找到已声明类型的 npm 插件";
|
|
34
|
+
}
|
|
35
|
+
async function request(path, method = "GET", body) {
|
|
36
|
+
const response = await fetch(path, { method, headers: {
|
|
37
|
+
accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
|
|
38
|
+
...(tokenNode.value ? { authorization: `Bearer ${tokenNode.value}` } : {}),
|
|
39
|
+
}, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
if (!response.ok)
|
|
42
|
+
throw new Error(data.error ?? `请求失败:${response.status}`);
|
|
43
|
+
return data;
|
|
44
|
+
}
|
|
45
|
+
function button(label, title, run) {
|
|
46
|
+
const item = document.createElement("button");
|
|
47
|
+
item.type = "button";
|
|
48
|
+
item.textContent = label;
|
|
49
|
+
item.title = title;
|
|
50
|
+
item.onclick = () => { void run().catch((cause) => error(String(cause))); };
|
|
51
|
+
return item;
|
|
52
|
+
}
|
|
53
|
+
async function load() {
|
|
54
|
+
try {
|
|
55
|
+
error("");
|
|
56
|
+
const [health, plugins, identities, messages, versions] = await Promise.all([
|
|
57
|
+
request("/api/health"), request("/api/plugins"), request("/api/aids"), request("/api/messages"), request("/api/versions"),
|
|
58
|
+
]);
|
|
59
|
+
renderVersions(versions);
|
|
60
|
+
pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin) => [plugin.id, plugin.versionStatus]));
|
|
61
|
+
node("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
62
|
+
node("#access-panel").hidden = true;
|
|
63
|
+
aids = identities.aids ?? [];
|
|
64
|
+
const installed = plugins.plugins ?? [];
|
|
65
|
+
pluginConfigs = Object.fromEntries(await Promise.all(installed.map(async (plugin) => [plugin.id, await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`)])));
|
|
66
|
+
renderPlugins(installed);
|
|
67
|
+
const groups = identities.groups ?? { evolcore: aids, muAgentBridge: [] };
|
|
68
|
+
aidList.replaceChildren(...[["EvolCore", groups.evolcore], ["mu-agent-bridge", groups.muAgentBridge]].flatMap(([label, values]) => {
|
|
69
|
+
const title = document.createElement("strong");
|
|
70
|
+
title.textContent = String(label);
|
|
71
|
+
const items = values.map((aid) => {
|
|
72
|
+
const item = document.createElement("div");
|
|
73
|
+
item.className = "aid-row";
|
|
74
|
+
const name = document.createElement("span");
|
|
75
|
+
name.textContent = aid;
|
|
76
|
+
const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
|
|
77
|
+
const current = document.createElement("small");
|
|
78
|
+
current.className = "muted";
|
|
79
|
+
current.textContent = linked.length ? `已关联:${linked.join("、")}` : "未关联插件";
|
|
80
|
+
const select = document.createElement("select");
|
|
81
|
+
select.setAttribute("aria-label", `${aid} 关联插件`);
|
|
82
|
+
select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
|
|
83
|
+
select.onchange = () => { if (select.value)
|
|
84
|
+
void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
|
|
85
|
+
item.append(name, current, select);
|
|
86
|
+
return item;
|
|
87
|
+
});
|
|
88
|
+
return [title, ...items];
|
|
89
|
+
}));
|
|
90
|
+
if (!aids.length)
|
|
91
|
+
aidList.textContent = "当前没有已连接的 AID";
|
|
92
|
+
node("#aid-count").textContent = `${aids.length} 个`;
|
|
93
|
+
renderMessages(messages.messages ?? []);
|
|
94
|
+
}
|
|
95
|
+
catch (cause) {
|
|
96
|
+
node("#access-panel").hidden = false;
|
|
97
|
+
error(cause instanceof Error ? cause.message : String(cause));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function renderVersions(data) {
|
|
101
|
+
const entries = [data.core, ...(data.plugins ?? []).map((plugin) => plugin.versionStatus)].filter(Boolean);
|
|
102
|
+
versionsNode.replaceChildren(...entries.map((entry) => {
|
|
103
|
+
const row = document.createElement("div");
|
|
104
|
+
row.className = "version-row";
|
|
105
|
+
row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${entry.state}${entry.error ? ` · ${entry.error}` : ""}`;
|
|
106
|
+
return row;
|
|
107
|
+
}));
|
|
108
|
+
if (!entries.length)
|
|
109
|
+
versionsNode.textContent = "尚未检查版本";
|
|
110
|
+
}
|
|
111
|
+
function renderPlugins(items) {
|
|
112
|
+
plugins = items;
|
|
113
|
+
const selected = picker.value || items[0]?.id;
|
|
114
|
+
const groups = [["Agent", items.filter((plugin) => plugin.type === "agent" || (!plugin.type && plugin.capabilities?.includes("chat")))], ["Channel", items.filter((plugin) => plugin.type === "channel" || plugin.capabilities?.includes("channel"))], ["管理", items.filter((plugin) => plugin.type === "management" || (!plugin.type && plugin.capabilities?.includes("management")))]];
|
|
115
|
+
picker.replaceChildren(...groups.flatMap(([label, group]) => {
|
|
116
|
+
const options = document.createElement("optgroup");
|
|
117
|
+
options.label = label;
|
|
118
|
+
options.append(...group.map((plugin) => new Option(`${plugin.displayName || plugin.id} · v${plugin.version} · ${plugin.status}`, plugin.id)));
|
|
119
|
+
return group.length ? [options] : [];
|
|
120
|
+
}));
|
|
121
|
+
picker.value = items.some((plugin) => plugin.id === selected) ? selected : items[0]?.id ?? "";
|
|
122
|
+
pluginsNode.replaceChildren();
|
|
123
|
+
for (const plugin of items.filter((item) => item.id === picker.value)) {
|
|
124
|
+
const item = document.createElement("article");
|
|
125
|
+
item.className = "plugin-row";
|
|
126
|
+
const main = document.createElement("div");
|
|
127
|
+
main.className = "plugin-main";
|
|
128
|
+
const name = document.createElement("strong");
|
|
129
|
+
name.textContent = plugin.displayName || plugin.id;
|
|
130
|
+
const meta = document.createElement("span");
|
|
131
|
+
meta.className = "muted";
|
|
132
|
+
meta.textContent = `${plugin.id} · 当前版本 v${plugin.version}`;
|
|
133
|
+
const status = document.createElement("span");
|
|
134
|
+
status.className = `status status-${plugin.status}`;
|
|
135
|
+
status.textContent = plugin.status;
|
|
136
|
+
main.append(name, meta, status);
|
|
137
|
+
if (plugin.error) {
|
|
138
|
+
const detail = document.createElement("small");
|
|
139
|
+
detail.className = "error-text";
|
|
140
|
+
detail.textContent = plugin.error;
|
|
141
|
+
main.append(detail);
|
|
142
|
+
}
|
|
143
|
+
const controls = document.createElement("div");
|
|
144
|
+
controls.className = "controls";
|
|
145
|
+
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules"))
|
|
146
|
+
controlsUpgrade(plugin.id);
|
|
147
|
+
const select = document.createElement("select");
|
|
148
|
+
select.setAttribute("aria-label", `${plugin.id} 的 AID`);
|
|
149
|
+
select.append(new Option("不绑定 AID", ""), ...aids.map((aid) => new Option(aid, aid)));
|
|
150
|
+
select.value = pluginConfigs[plugin.id]?.aid ?? "";
|
|
151
|
+
if (plugin.id !== "abweb")
|
|
152
|
+
controls.append(select, button("关联 AID", "保存此插件使用的 AID", async () => {
|
|
153
|
+
await request(`/api/plugins/${encodeURIComponent(plugin.id)}/aid`, "PATCH", { aid: select.value });
|
|
154
|
+
await load();
|
|
155
|
+
}));
|
|
156
|
+
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
157
|
+
controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
|
|
158
|
+
}
|
|
159
|
+
if (plugin.path.includes(".agent-bridge") && plugin.path.includes("node_modules"))
|
|
160
|
+
controls.append(button("卸载", `卸载 ${plugin.id}`, async () => {
|
|
161
|
+
if (confirm(`卸载 ${plugin.displayName}?`)) {
|
|
162
|
+
await request(`/api/plugins/${encodeURIComponent(plugin.id)}`, "DELETE");
|
|
163
|
+
await load();
|
|
164
|
+
}
|
|
165
|
+
}));
|
|
166
|
+
item.append(main, controls);
|
|
167
|
+
pluginsNode.append(item);
|
|
168
|
+
function controlsUpgrade(id) { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
|
|
169
|
+
}
|
|
170
|
+
if (!items.length)
|
|
171
|
+
pluginsNode.textContent = "暂无插件";
|
|
172
|
+
}
|
|
173
|
+
function renderMessages(messages) {
|
|
174
|
+
messagesNode.replaceChildren();
|
|
175
|
+
messagesEmpty.hidden = messages.length > 0;
|
|
176
|
+
for (const message of messages) {
|
|
177
|
+
const row = document.createElement("tr");
|
|
178
|
+
if (message.direction === "error")
|
|
179
|
+
row.className = "message-error";
|
|
180
|
+
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 : "—"]) {
|
|
181
|
+
const cell = document.createElement("td");
|
|
182
|
+
cell.textContent = value;
|
|
183
|
+
row.append(cell);
|
|
184
|
+
}
|
|
185
|
+
messagesNode.append(row);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
|
|
189
|
+
node(`#${tabId}`).onclick = () => {
|
|
190
|
+
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
|
|
191
|
+
const selected = otherTab === tabId;
|
|
192
|
+
node(`#${otherTab}`).setAttribute("aria-selected", String(selected));
|
|
193
|
+
node(`#${otherPanel}`).hidden = !selected;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
node("#refresh").onclick = () => void load();
|
|
198
|
+
node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(load).catch((cause) => error(String(cause)));
|
|
199
|
+
picker.onchange = () => renderPlugins(plugins);
|
|
200
|
+
node("#save-token").onclick = () => { sessionStorage.setItem("mab-token", tokenNode.value); void load(); };
|
|
201
|
+
node("#pair-button").onclick = () => {
|
|
202
|
+
const code = pairCodeNode.value.trim();
|
|
203
|
+
if (!/^\d{6}$/.test(code))
|
|
204
|
+
return error("请输入 6 位配置码");
|
|
205
|
+
void request("/api/pair", "POST", { code }).then((result) => {
|
|
206
|
+
tokenNode.value = result.token;
|
|
207
|
+
sessionStorage.setItem("mab-token", result.token);
|
|
208
|
+
pairCodeNode.value = "";
|
|
209
|
+
return load();
|
|
210
|
+
}).catch((cause) => error(String(cause)));
|
|
211
|
+
};
|
|
212
|
+
node("#install-package").onclick = () => {
|
|
213
|
+
if (!packageNode.value)
|
|
214
|
+
return error("请先选择插件");
|
|
215
|
+
void request("/api/plugins/install", "POST", { packageName: packageNode.value })
|
|
216
|
+
.then(() => load()).catch((cause) => error(String(cause)));
|
|
217
|
+
};
|
|
218
|
+
node("#install-name-form").onsubmit = (event) => {
|
|
219
|
+
event.preventDefault();
|
|
220
|
+
const packageName = node("#manual-package-name").value.trim();
|
|
221
|
+
void request("/api/plugins/install", "POST", { packageName }).then(() => load()).catch((cause) => error(String(cause)));
|
|
222
|
+
};
|
|
223
|
+
node("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
|
|
224
|
+
packageSearchNode.onkeydown = (event) => { if (event.key === "Enter")
|
|
225
|
+
void loadCatalog().catch((cause) => error(String(cause))); };
|
|
226
|
+
node("#install-local-form").onsubmit = (event) => {
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
|
|
229
|
+
.then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
|
|
230
|
+
};
|
|
231
|
+
node("#create-aid-form").onsubmit = (event) => {
|
|
232
|
+
event.preventDefault();
|
|
233
|
+
const aid = node("#new-aid").value.trim();
|
|
234
|
+
const ownerAid = node("#owner-aid").value.trim();
|
|
235
|
+
const result = node("#aid-result");
|
|
236
|
+
result.textContent = "正在创建 AID…";
|
|
237
|
+
void request("/api/aids", "POST", { aid, ownerAid }).then(() => {
|
|
238
|
+
result.textContent = `${aid} 已创建;连接到 AUN 后会出现在上方列表。`;
|
|
239
|
+
return load();
|
|
240
|
+
}).catch((cause) => { result.textContent = ""; error(String(cause)); });
|
|
241
|
+
};
|
|
242
|
+
void load();
|
|
243
|
+
void loadCatalog().catch((cause) => error(String(cause)));
|
|
244
|
+
})();
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
type Plugin = { id: string; displayName: string; version: string; status: string; type?: string; capabilities?: 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 packageSearchNode = node<HTMLInputElement>("#package-search");
|
|
12
|
+
const catalogNode = node<HTMLDivElement>("#catalog");
|
|
13
|
+
const pluginPathNode = node<HTMLInputElement>("#plugin-path");
|
|
14
|
+
const picker = node<HTMLSelectElement>("#plugin-picker");
|
|
15
|
+
const aidList = node<HTMLDivElement>("#aids");
|
|
16
|
+
const versionsNode = node<HTMLDivElement>("#versions");
|
|
17
|
+
tokenNode.value = sessionStorage.getItem("mab-token") ?? "";
|
|
18
|
+
let aids: string[] = [];
|
|
19
|
+
let plugins: Plugin[] = [];
|
|
20
|
+
let pluginConfigs: Record<string, { aid?: string }> = {};
|
|
21
|
+
let pluginVersions: Record<string, any> = {};
|
|
22
|
+
|
|
23
|
+
function error(message: string): void { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
24
|
+
async function loadCatalog(): Promise<void> {
|
|
25
|
+
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
26
|
+
const items: Array<{ name: string; version: string; description: string; type: string }> = result.plugins ?? [];
|
|
27
|
+
packageNode.replaceChildren(...[["Agent", "agent"], ["Channel", "channel"], ["管理", "management"]].flatMap(([label, type]) => {
|
|
28
|
+
const group = document.createElement("optgroup"); group.label = label;
|
|
29
|
+
group.append(...items.filter((item) => item.type === type).map((item) => new Option(`${item.name} · v${item.version}`, item.name)));
|
|
30
|
+
return group.children.length ? [group] : [];
|
|
31
|
+
}));
|
|
32
|
+
catalogNode.replaceChildren(...items.map((plugin) => { const item = document.createElement("div"); item.className = "catalog-row"; item.textContent = `${plugin.type} · ${plugin.name} · ${plugin.description || "无描述"}`; return item; }));
|
|
33
|
+
if (!(result.plugins ?? []).length) catalogNode.textContent = "未找到已声明类型的 npm 插件";
|
|
34
|
+
}
|
|
35
|
+
async function request(path: string, method = "GET", body?: unknown): Promise<any> {
|
|
36
|
+
const response = await fetch(path, { method, headers: {
|
|
37
|
+
accept: "application/json", ...(body ? { "content-type": "application/json" } : {}),
|
|
38
|
+
...(tokenNode.value ? { authorization: `Bearer ${tokenNode.value}` } : {}),
|
|
39
|
+
}, ...(body ? { body: JSON.stringify(body) } : {}) });
|
|
40
|
+
const data = await response.json();
|
|
41
|
+
if (!response.ok) throw new Error(data.error ?? `请求失败:${response.status}`);
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
function button(label: string, title: string, run: () => Promise<void>): HTMLButtonElement {
|
|
45
|
+
const item = document.createElement("button"); item.type = "button"; item.textContent = label; item.title = title;
|
|
46
|
+
item.onclick = () => { void run().catch((cause) => error(String(cause))); }; return item;
|
|
47
|
+
}
|
|
48
|
+
async function load(): Promise<void> {
|
|
49
|
+
try {
|
|
50
|
+
error("");
|
|
51
|
+
const [health, plugins, identities, messages, versions] = await Promise.all([
|
|
52
|
+
request("/api/health"), request("/api/plugins"), request("/api/aids"), request("/api/messages"), request("/api/versions"),
|
|
53
|
+
]);
|
|
54
|
+
renderVersions(versions);
|
|
55
|
+
pluginVersions = Object.fromEntries((versions.plugins ?? []).map((plugin: any) => [plugin.id, plugin.versionStatus]));
|
|
56
|
+
node<HTMLElement>("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
57
|
+
node<HTMLElement>("#access-panel").hidden = true;
|
|
58
|
+
aids = identities.aids ?? [];
|
|
59
|
+
const installed: Plugin[] = plugins.plugins ?? [];
|
|
60
|
+
pluginConfigs = Object.fromEntries(await Promise.all(installed.map(async (plugin) => [plugin.id, await request(`/api/plugins/${encodeURIComponent(plugin.id)}/config`)] as const)));
|
|
61
|
+
renderPlugins(installed);
|
|
62
|
+
const groups = identities.groups ?? { evolcore: aids, muAgentBridge: [] };
|
|
63
|
+
aidList.replaceChildren(...[ ["EvolCore", groups.evolcore], ["mu-agent-bridge", groups.muAgentBridge] ].flatMap(([label, values]) => {
|
|
64
|
+
const title = document.createElement("strong"); title.textContent = String(label);
|
|
65
|
+
const items = (values as string[]).map((aid) => {
|
|
66
|
+
const item = document.createElement("div"); item.className = "aid-row";
|
|
67
|
+
const name = document.createElement("span"); name.textContent = aid;
|
|
68
|
+
const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
|
|
69
|
+
const current = document.createElement("small"); current.className = "muted"; current.textContent = linked.length ? `已关联:${linked.join("、")}` : "未关联插件";
|
|
70
|
+
const select = document.createElement("select"); select.setAttribute("aria-label", `${aid} 关联插件`);
|
|
71
|
+
select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
|
|
72
|
+
select.onchange = () => { if (select.value) void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
|
|
73
|
+
item.append(name, current, select); return item;
|
|
74
|
+
});
|
|
75
|
+
return [title, ...items];
|
|
76
|
+
}));
|
|
77
|
+
if (!aids.length) aidList.textContent = "当前没有已连接的 AID";
|
|
78
|
+
node<HTMLElement>("#aid-count").textContent = `${aids.length} 个`;
|
|
79
|
+
renderMessages(messages.messages ?? []);
|
|
80
|
+
} catch (cause) { node<HTMLElement>("#access-panel").hidden = false; error(cause instanceof Error ? cause.message : String(cause)); }
|
|
81
|
+
}
|
|
82
|
+
function renderVersions(data: any): void {
|
|
83
|
+
const entries = [data.core, ...(data.plugins ?? []).map((plugin: any) => plugin.versionStatus)].filter(Boolean);
|
|
84
|
+
versionsNode.replaceChildren(...entries.map((entry: any) => {
|
|
85
|
+
const row = document.createElement("div"); row.className = "version-row";
|
|
86
|
+
row.textContent = `${entry.packageName} · 当前 ${entry.currentVersion} · ${entry.latestVersion ? `最新 ${entry.latestVersion}` : "最新版本未知"} · ${entry.state}${entry.error ? ` · ${entry.error}` : ""}`;
|
|
87
|
+
return row;
|
|
88
|
+
}));
|
|
89
|
+
if (!entries.length) versionsNode.textContent = "尚未检查版本";
|
|
90
|
+
}
|
|
91
|
+
function renderPlugins(items: Plugin[]): void {
|
|
92
|
+
plugins = items;
|
|
93
|
+
const selected = picker.value || items[0]?.id;
|
|
94
|
+
const groups = [["Agent", items.filter((plugin) => plugin.type === "agent" || (!plugin.type && plugin.capabilities?.includes("chat")))], ["Channel", items.filter((plugin) => plugin.type === "channel" || plugin.capabilities?.includes("channel"))], ["管理", items.filter((plugin) => plugin.type === "management" || (!plugin.type && plugin.capabilities?.includes("management")))]] as const;
|
|
95
|
+
picker.replaceChildren(...groups.flatMap(([label, group]) => {
|
|
96
|
+
const options = document.createElement("optgroup"); options.label = label;
|
|
97
|
+
options.append(...group.map((plugin) => new Option(`${plugin.displayName || plugin.id} · v${plugin.version} · ${plugin.status}`, plugin.id)));
|
|
98
|
+
return group.length ? [options] : [];
|
|
99
|
+
}));
|
|
100
|
+
picker.value = items.some((plugin) => plugin.id === selected) ? selected : items[0]?.id ?? "";
|
|
101
|
+
pluginsNode.replaceChildren();
|
|
102
|
+
for (const plugin of items.filter((item) => item.id === picker.value)) {
|
|
103
|
+
const item = document.createElement("article"); item.className = "plugin-row";
|
|
104
|
+
const main = document.createElement("div"); main.className = "plugin-main";
|
|
105
|
+
const name = document.createElement("strong"); name.textContent = plugin.displayName || plugin.id;
|
|
106
|
+
const meta = document.createElement("span"); meta.className = "muted"; meta.textContent = `${plugin.id} · 当前版本 v${plugin.version}`;
|
|
107
|
+
const status = document.createElement("span"); status.className = `status status-${plugin.status}`; status.textContent = plugin.status;
|
|
108
|
+
main.append(name, meta, status);
|
|
109
|
+
if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text"; detail.textContent = plugin.error; main.append(detail); }
|
|
110
|
+
const controls = document.createElement("div"); controls.className = "controls";
|
|
111
|
+
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) controlsUpgrade(plugin.id);
|
|
112
|
+
const select = document.createElement("select"); select.setAttribute("aria-label", `${plugin.id} 的 AID`);
|
|
113
|
+
select.append(new Option("不绑定 AID", ""), ...aids.map((aid) => new Option(aid, aid)));
|
|
114
|
+
select.value = pluginConfigs[plugin.id]?.aid ?? "";
|
|
115
|
+
if (plugin.id !== "abweb") controls.append(select, button("关联 AID", "保存此插件使用的 AID", async () => {
|
|
116
|
+
await request(`/api/plugins/${encodeURIComponent(plugin.id)}/aid`, "PATCH", { aid: select.value }); await load();
|
|
117
|
+
}));
|
|
118
|
+
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
119
|
+
controls.append(button(label, `${label} ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/${action}`, "POST"); await load(); }));
|
|
120
|
+
}
|
|
121
|
+
if (plugin.path.includes(".agent-bridge") && plugin.path.includes("node_modules")) controls.append(button("卸载", `卸载 ${plugin.id}`, async () => {
|
|
122
|
+
if (confirm(`卸载 ${plugin.displayName}?`)) { await request(`/api/plugins/${encodeURIComponent(plugin.id)}`, "DELETE"); await load(); }
|
|
123
|
+
}));
|
|
124
|
+
item.append(main, controls); pluginsNode.append(item);
|
|
125
|
+
function controlsUpgrade(id: string): void { controls.append(button("升级", `升级 ${id}`, async () => { await request(`/api/plugins/${encodeURIComponent(id)}/update`, "POST", {}); await load(); })); }
|
|
126
|
+
}
|
|
127
|
+
if (!items.length) pluginsNode.textContent = "暂无插件";
|
|
128
|
+
}
|
|
129
|
+
function renderMessages(messages: Array<{ time: string; pluginId: string; direction: "request" | "reply" | "error"; text: string; peer?: string; agentAid?: string }>): void {
|
|
130
|
+
messagesNode.replaceChildren();
|
|
131
|
+
messagesEmpty.hidden = messages.length > 0;
|
|
132
|
+
for (const message of messages) {
|
|
133
|
+
const row = document.createElement("tr");
|
|
134
|
+
if (message.direction === "error") row.className = "message-error";
|
|
135
|
+
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 : "—"]) {
|
|
136
|
+
const cell = document.createElement("td"); cell.textContent = value; row.append(cell);
|
|
137
|
+
}
|
|
138
|
+
messagesNode.append(row);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
for (const [tabId, panelId] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
|
|
142
|
+
node<HTMLButtonElement>(`#${tabId}`).onclick = () => {
|
|
143
|
+
for (const [otherTab, otherPanel] of [["settings-tab", "settings-panel"], ["messages-tab", "messages-panel"]]) {
|
|
144
|
+
const selected = otherTab === tabId;
|
|
145
|
+
node<HTMLButtonElement>(`#${otherTab}`).setAttribute("aria-selected", String(selected));
|
|
146
|
+
node<HTMLDivElement>(`#${otherPanel}`).hidden = !selected;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
node<HTMLButtonElement>("#refresh").onclick = () => void load();
|
|
151
|
+
node<HTMLButtonElement>("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(load).catch((cause) => error(String(cause)));
|
|
152
|
+
picker.onchange = () => renderPlugins(plugins);
|
|
153
|
+
node<HTMLButtonElement>("#save-token").onclick = () => { sessionStorage.setItem("mab-token", tokenNode.value); void load(); };
|
|
154
|
+
node<HTMLButtonElement>("#pair-button").onclick = () => {
|
|
155
|
+
const code = pairCodeNode.value.trim();
|
|
156
|
+
if (!/^\d{6}$/.test(code)) return error("请输入 6 位配置码");
|
|
157
|
+
void request("/api/pair", "POST", { code }).then((result) => {
|
|
158
|
+
tokenNode.value = result.token; sessionStorage.setItem("mab-token", result.token); pairCodeNode.value = ""; return load();
|
|
159
|
+
}).catch((cause) => error(String(cause)));
|
|
160
|
+
};
|
|
161
|
+
node<HTMLButtonElement>("#install-package").onclick = () => {
|
|
162
|
+
if (!packageNode.value) return error("请先选择插件");
|
|
163
|
+
void request("/api/plugins/install", "POST", { packageName: packageNode.value })
|
|
164
|
+
.then(() => load()).catch((cause) => error(String(cause)));
|
|
165
|
+
};
|
|
166
|
+
node<HTMLFormElement>("#install-name-form").onsubmit = (event) => {
|
|
167
|
+
event.preventDefault();
|
|
168
|
+
const packageName = node<HTMLInputElement>("#manual-package-name").value.trim();
|
|
169
|
+
void request("/api/plugins/install", "POST", { packageName }).then(() => load()).catch((cause) => error(String(cause)));
|
|
170
|
+
};
|
|
171
|
+
node<HTMLButtonElement>("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
|
|
172
|
+
packageSearchNode.onkeydown = (event) => { if (event.key === "Enter") void loadCatalog().catch((cause) => error(String(cause))); };
|
|
173
|
+
node<HTMLFormElement>("#install-local-form").onsubmit = (event) => {
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
void request("/api/plugins/install", "POST", { source: pluginPathNode.value.trim() })
|
|
176
|
+
.then(() => { pluginPathNode.value = ""; return load(); }).catch((cause) => error(String(cause)));
|
|
177
|
+
};
|
|
178
|
+
node<HTMLFormElement>("#create-aid-form").onsubmit = (event) => {
|
|
179
|
+
event.preventDefault();
|
|
180
|
+
const aid = node<HTMLInputElement>("#new-aid").value.trim();
|
|
181
|
+
const ownerAid = node<HTMLInputElement>("#owner-aid").value.trim();
|
|
182
|
+
const result = node<HTMLElement>("#aid-result");
|
|
183
|
+
result.textContent = "正在创建 AID…";
|
|
184
|
+
void request("/api/aids", "POST", { aid, ownerAid }).then(() => {
|
|
185
|
+
result.textContent = `${aid} 已创建;连接到 AUN 后会出现在上方列表。`;
|
|
186
|
+
return load();
|
|
187
|
+
}).catch((cause) => { result.textContent = ""; error(String(cause)); });
|
|
188
|
+
};
|
|
189
|
+
void load(); void loadCatalog().catch((cause) => error(String(cause)));
|
|
190
|
+
})();
|
|
@@ -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><div class="plugin-catalog"><div class="section-heading"><h3>npm 插件目录</h3><button id="search-packages" type="button">刷新目录</button></div><input id="package-search" placeholder="搜索插件名称或功能" aria-label="搜索 npm 插件"><select id="package-name" aria-label="选择 npm 插件"></select><button id="install-package" type="button">安装选中插件</button><form id="install-name-form"><input id="manual-package-name" required pattern="agent-bridge-[a-z0-9-]+" placeholder="输入 npm 包名,例如 agent-bridge-workbuddy" aria-label="直接输入 npm 包名"><button type="submit">按包名安装</button></form><div id="catalog" class="catalog"></div></div><form id="install-local-form"><input id="plugin-path" required placeholder="Windows 上的插件目录路径" aria-label="本地插件目录"><button type="submit">安装本地插件</button></form></section>
|
|
17
|
+
<section><div class="section-heading"><h2>已连接 AID</h2><span id="aid-count" class="muted"></span></div><div id="aids" class="aid-list"></div><form id="create-aid-form"><input id="new-aid" required placeholder="新 AID,例如 new.agentid.cn" aria-label="新 AID"><input id="owner-aid" required placeholder="owner AID" aria-label="owner AID"><button type="submit">创建 AID</button></form><p id="aid-result" class="muted" role="status"></p><p class="muted">在插件列表中选择插件,再用“关联 AID”下拉框保存对应身份。</p></section>
|
|
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,49 @@
|
|
|
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
|
+
.plugin-catalog { border-top: 1px solid #e0e5e2; margin-top: 18px; padding-top: 14px; }
|
|
33
|
+
.plugin-catalog h3 { font-size: 14px; margin: 0; }
|
|
34
|
+
.plugin-catalog input { width: min(100%, 420px); margin: 10px 0; }
|
|
35
|
+
.catalog { margin-top: 10px; }
|
|
36
|
+
.catalog-row { padding: 7px 0; color: #566761; font-size: 13px; border-bottom: 1px solid #edf1ef; }
|
|
37
|
+
.plugin-catalog form { margin-top: 8px; }
|
|
38
|
+
.muted { color: #65736e; font-size: 13px; }
|
|
39
|
+
.status { font-size: 12px; padding: 3px 7px; background: #e7ebea; border-radius: 4px; }
|
|
40
|
+
.status-ready { color: #136a44; background: #def3e5; }
|
|
41
|
+
.status-failed { color: #a83427; background: #fce9e5; }
|
|
42
|
+
.error, .error-text { color: #a83427; }
|
|
43
|
+
.table-scroll { overflow-x: auto; margin-top: 13px; }
|
|
44
|
+
table { width: 100%; border-collapse: collapse; table-layout: fixed; min-width: 900px; font-size: 13px; }
|
|
45
|
+
th, td { padding: 10px 9px; text-align: left; vertical-align: top; border-bottom: 1px solid #dce3df; overflow-wrap: anywhere; white-space: pre-wrap; }
|
|
46
|
+
th { color: #566761; font-weight: 600; background: #edf1ef; }
|
|
47
|
+
th:nth-child(1) { width: 145px; } th:nth-child(2) { width: 100px; } th:nth-child(3), th:nth-child(4) { width: 145px; } th:nth-child(5) { width: 62px; } th:nth-child(7) { width: 140px; }
|
|
48
|
+
.message-error td:last-child { color: #a83427; }
|
|
49
|
+
@media (max-width: 620px) { .shell { padding: 16px; } .section-heading { align-items: flex-start; flex-direction: column; } form, form input { width: 100%; } .controls select { max-width: 100%; } }
|
package/package.json
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-bridge-abweb",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
}
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "agent-bridge 的网页管理插件。",
|
|
5
|
+
"keywords": ["mu-agent-bridge-plugin", "mu-agent-bridge-management"],
|
|
6
|
+
"repository": { "type": "git", "url": "https://codeup.aliyun.com/6808da56fb3da14b899afd54/agent_bridge.git", "directory": "packages/abweb" },
|
|
7
|
+
"main": "dist/src-ts/plugins/abweb/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/",
|
|
10
|
+
"plugin.json",
|
|
11
|
+
"config.schema.json",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"agentBridge": {
|
|
15
|
+
"plugin": "./plugin.json",
|
|
16
|
+
"type": "management"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"registry": "https://registry.npmjs.org"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=22"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/plugin.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "abweb",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"type": "management",
|
|
5
|
+
"displayName": "Agent Bridge Web",
|
|
6
|
+
"entry": "dist/src-ts/plugins/abweb/index.js",
|
|
7
|
+
"apiVersion": "1",
|
|
8
|
+
"process": "in-process",
|
|
9
|
+
"capabilities": ["web", "management", "health"],
|
|
10
|
+
"configSchema": "config.schema.json"
|
|
11
|
+
}
|