mu-agent-bridge 1.0.3 → 1.0.5

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.
@@ -84,7 +84,7 @@ class PluginManager {
84
84
  try {
85
85
  const spec = version ? `${packageName}@${version}` : packageName;
86
86
  if (process.platform === "win32")
87
- await execFileAsync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `npm install --prefix "${staging}" --no-save --ignore-scripts ${spec}`]);
87
+ await execFileAsync("npm.cmd", ["install", "--prefix", staging, "--no-save", "--ignore-scripts", spec]);
88
88
  else
89
89
  await execFileAsync("npm", ["install", "--prefix", staging, "--no-save", "--ignore-scripts", spec]);
90
90
  const candidate = (0, node_path_1.join)(staging, "node_modules", packageName);
@@ -141,7 +141,7 @@ class PluginManager {
141
141
  await (0, promises_1.mkdir)(prefix, { recursive: true });
142
142
  try {
143
143
  if (process.platform === "win32")
144
- await execFileAsync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `npm install --prefix "${prefix}" --no-save --ignore-scripts ${packageName}`]);
144
+ await execFileAsync("npm.cmd", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName]);
145
145
  else
146
146
  await execFileAsync("npm", ["install", "--prefix", prefix, "--no-save", "--ignore-scripts", packageName]);
147
147
  const directory = (0, node_path_1.join)(prefix, "node_modules", name);
@@ -6,7 +6,6 @@
6
6
  const errorNode = node("#error");
7
7
  const tokenNode = node("#token");
8
8
  const pairCodeNode = node("#pair-code");
9
- const packageNode = node("#package-name");
10
9
  const packageSearchNode = node("#package-search");
11
10
  const catalogNode = node("#catalog");
12
11
  const pluginPathNode = node("#plugin-path");
@@ -18,19 +17,63 @@
18
17
  let plugins = [];
19
18
  let pluginConfigs = {};
20
19
  let pluginVersions = {};
20
+ const installingPackages = new Set();
21
21
  function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
22
22
  async function loadCatalog() {
23
23
  const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
24
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)
25
+ catalogNode.replaceChildren();
26
+ if (!items.length) {
33
27
  catalogNode.textContent = "未找到已声明类型的 npm 插件";
28
+ return;
29
+ }
30
+ const typeNames = { agent: "Agent", channel: "通道", management: "管理" };
31
+ const table = document.createElement("table");
32
+ table.className = "catalog-table";
33
+ const head = document.createElement("thead");
34
+ const headRow = document.createElement("tr");
35
+ for (const label of ["名称", "版本", "类型", "描述", "操作"]) {
36
+ const cell = document.createElement("th");
37
+ cell.scope = "col";
38
+ cell.textContent = label;
39
+ headRow.append(cell);
40
+ }
41
+ head.append(headRow);
42
+ const body = document.createElement("tbody");
43
+ for (const plugin of items) {
44
+ const installed = plugins.find((item) => item.id === plugin.name.replace(/^agent-bridge-/, ""));
45
+ const versionStatus = installed ? pluginVersions[installed.id] : undefined;
46
+ const isUpgrade = Boolean(installed && versionStatus?.packageName === plugin.name && newerVersion(plugin.version, installed.version));
47
+ const row = document.createElement("tr");
48
+ for (const value of [plugin.name, `v${plugin.version}`, typeNames[plugin.type] ?? plugin.type, plugin.description || "无描述"]) {
49
+ const cell = document.createElement("td");
50
+ cell.textContent = value;
51
+ row.append(cell);
52
+ }
53
+ const actionCell = document.createElement("td");
54
+ const action = isUpgrade ? "升级" : installed ? "已安装" : "安装";
55
+ const install = document.createElement("button");
56
+ install.type = "button";
57
+ install.className = "catalog-action";
58
+ install.title = `${action} ${plugin.name}`;
59
+ install.textContent = installingPackages.has(plugin.name) ? "处理中…" : action;
60
+ install.disabled = Boolean(installed && !isUpgrade) || installingPackages.has(plugin.name);
61
+ if (!install.disabled)
62
+ install.onclick = () => {
63
+ installingPackages.add(plugin.name);
64
+ install.disabled = true;
65
+ install.textContent = isUpgrade ? "升级中…" : "安装中…";
66
+ void (isUpgrade ? request(`/api/plugins/${encodeURIComponent(installed.id)}/update`, "POST", { packageName: plugin.name, version: plugin.version }) : request("/api/plugins/install", "POST", { packageName: plugin.name })).then(() => load()).catch((cause) => error(String(cause))).finally(() => { installingPackages.delete(plugin.name); if (install.isConnected) {
67
+ install.disabled = false;
68
+ install.textContent = action;
69
+ } });
70
+ };
71
+ actionCell.append(install);
72
+ row.append(actionCell);
73
+ body.append(row);
74
+ }
75
+ table.append(head, body);
76
+ catalogNode.append(table);
34
77
  }
35
78
  async function request(path, method = "GET", body) {
36
79
  const response = await fetch(path, { method, headers: {
@@ -66,31 +109,41 @@
66
109
  renderPlugins(installed);
67
110
  const groups = identities.groups ?? { evolcore: aids, muAgentBridge: [] };
68
111
  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
112
  const items = values.map((aid) => {
72
- const item = document.createElement("div");
113
+ const item = document.createElement("tr");
73
114
  item.className = "aid-row";
74
- const name = document.createElement("span");
115
+ const name = document.createElement("td");
75
116
  name.textContent = aid;
117
+ const source = document.createElement("td");
118
+ source.textContent = String(label);
76
119
  const linked = installed.filter((plugin) => pluginConfigs[plugin.id]?.aid === aid).map((plugin) => plugin.displayName || plugin.id);
77
- const current = document.createElement("small");
120
+ const current = document.createElement("td");
78
121
  current.className = "muted";
79
- current.textContent = linked.length ? `已关联:${linked.join("、")}` : "未关联插件";
122
+ current.textContent = linked.length ? linked.join("、") : "未关联插件";
123
+ const action = document.createElement("td");
80
124
  const select = document.createElement("select");
81
125
  select.setAttribute("aria-label", `${aid} 关联插件`);
82
126
  select.append(new Option("关联插件", ""), ...installed.filter((plugin) => plugin.id !== "abweb").map((plugin) => new Option(plugin.displayName || plugin.id, plugin.id)));
83
127
  select.onchange = () => { if (select.value)
84
128
  void request(`/api/plugins/${encodeURIComponent(select.value)}/aid`, "PATCH", { aid }).then(() => load()).catch((cause) => error(String(cause))); };
85
- item.append(name, current, select);
129
+ action.append(select);
130
+ item.append(name, source, current, action);
86
131
  return item;
87
132
  });
88
- return [title, ...items];
133
+ return items;
89
134
  }));
90
- if (!aids.length)
91
- aidList.textContent = "当前没有已连接的 AID";
135
+ if (!aids.length) {
136
+ const row = document.createElement("tr");
137
+ const cell = document.createElement("td");
138
+ cell.colSpan = 4;
139
+ cell.className = "muted";
140
+ cell.textContent = "当前没有已连接的 AID";
141
+ row.append(cell);
142
+ aidList.append(row);
143
+ }
92
144
  node("#aid-count").textContent = `${aids.length} 个`;
93
145
  renderMessages(messages.messages ?? []);
146
+ void loadCatalog().catch((cause) => error(String(cause)));
94
147
  }
95
148
  catch (cause) {
96
149
  node("#access-panel").hidden = false;
@@ -209,17 +262,6 @@
209
262
  return load();
210
263
  }).catch((cause) => error(String(cause)));
211
264
  };
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
265
  node("#search-packages").onclick = () => void loadCatalog().catch((cause) => error(String(cause)));
224
266
  packageSearchNode.onkeydown = (event) => { if (event.key === "Enter")
225
267
  void loadCatalog().catch((cause) => error(String(cause))); };
@@ -240,5 +282,11 @@
240
282
  }).catch((cause) => { result.textContent = ""; error(String(cause)); });
241
283
  };
242
284
  void load();
243
- void loadCatalog().catch((cause) => error(String(cause)));
285
+ function newerVersion(remote, current) {
286
+ const parse = (value) => { const match = /^(\d+)\.(\d+)\.(\d+)/.exec(value); return match ? match.slice(1).map(Number) : undefined; };
287
+ const a = parse(remote), b = parse(current);
288
+ if (!a || !b)
289
+ return false;
290
+ return a.some((part, index) => part !== b[index] && part > b[index] && a.slice(0, index).every((item, i) => item === b[i]));
291
+ }
244
292
  })();
@@ -13,8 +13,8 @@
13
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
14
  <div id="settings-panel" role="tabpanel" aria-labelledby="settings-tab">
15
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>
16
+ <section><div class="section-heading"><h2>插件</h2><select id="plugin-picker" aria-label="选择插件"></select></div><div id="plugins" class="plugins"></div><div class="plugin-catalog"><div class="section-heading"><h3>npm 插件目录</h3><button id="search-packages" type="button">刷新目录</button></div><input id="package-search" placeholder="搜索插件名称或功能" aria-label="搜索 npm 插件"><div id="catalog" class="catalog" aria-live="polite"></div></div><form id="install-local-form"><input id="plugin-path" required placeholder="Windows 上的插件目录路径" aria-label="本地插件目录"><button type="submit">安装本地插件</button></form></section>
17
+ <section><div class="section-heading"><h2>已连接 AID</h2><span id="aid-count" class="muted"></span></div><div class="aid-list"><table class="aid-table"><thead><tr><th scope="col">AID</th><th scope="col">身份来源</th><th scope="col">已关联插件</th><th scope="col">操作</th></tr></thead><tbody id="aids" aria-live="polite"></tbody></table></div><form id="create-aid-form"><input id="new-aid" required placeholder="新 AID,例如 new.agentid.cn" aria-label="新 AID"><input id="owner-aid" required placeholder="owner AID" aria-label="owner AID"><button type="submit">创建 AID</button></form><p id="aid-result" class="muted" role="status"></p></section>
18
18
  </div>
19
19
  <div id="messages-panel" role="tabpanel" aria-labelledby="messages-tab" hidden><section><div class="section-heading"><h2>最近通信</h2></div><div class="table-scroll"><table><thead><tr><th scope="col">时间</th><th scope="col">插件</th><th scope="col">接收 AID</th><th scope="col">来源 Peer</th><th scope="col">方向</th><th scope="col">消息</th><th scope="col">错误</th></tr></thead><tbody id="messages"></tbody></table></div><p id="messages-empty" class="muted" hidden>暂无通信记录</p></section></div>
20
20
  <p id="error" class="error" role="alert" hidden></p>
@@ -19,11 +19,12 @@ button { cursor: pointer; white-space: nowrap; }
19
19
  button:hover { background: #e5f2ea; }
20
20
  form { display: flex; gap: 8px; }
21
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; }
22
+ .aid-list { overflow-x: auto; margin-top: 12px; }
23
+ .aid-table { width: 100%; min-width: 720px; border-collapse: collapse; table-layout: fixed; font-size: 13px; }
24
+ .aid-table th, .aid-table td { padding: 9px 8px; text-align: left; vertical-align: middle; border-bottom: 1px solid #dce3df; overflow-wrap: anywhere; }
25
+ .aid-table th { color: #566761; font-weight: 600; background: #edf1ef; }
26
+ .aid-table th:nth-child(1) { width: 230px; } .aid-table th:nth-child(2) { width: 145px; } .aid-table th:nth-child(4) { width: 180px; }
27
+ .aid-row select { max-width: 165px; }
27
28
  .plugins, .messages { margin-top: 13px; }
28
29
  .plugin-row { justify-content: space-between; border-bottom: 1px solid #e0e5e2; padding: 12px 0; flex-wrap: wrap; }
29
30
  .plugin-main { flex-wrap: wrap; }
@@ -32,8 +33,14 @@ form { margin-top: 14px; flex-wrap: wrap; }
32
33
  .plugin-catalog { border-top: 1px solid #e0e5e2; margin-top: 18px; padding-top: 14px; }
33
34
  .plugin-catalog h3 { font-size: 14px; margin: 0; }
34
35
  .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; }
36
+ .catalog { overflow-x: auto; margin-top: 10px; }
37
+ .catalog-table { width: 100%; min-width: 680px; border-collapse: collapse; font-size: 13px; }
38
+ .catalog-table th, .catalog-table td { padding: 9px 8px; text-align: left; vertical-align: top; border-bottom: 1px solid #dce3df; overflow-wrap: anywhere; }
39
+ .catalog-table th { color: #566761; font-weight: 600; background: #edf1ef; }
40
+ .catalog-table th:nth-child(1) { width: 210px; } .catalog-table th:nth-child(2) { width: 100px; } .catalog-table th:nth-child(3) { width: 90px; } .catalog-table th:nth-child(5) { width: 88px; }
41
+ .catalog-action { min-height: 30px; padding: 4px 10px; }
42
+ .catalog-action:disabled { cursor: wait; opacity: .65; }
43
+ .catalog:empty { color: #566761; font-size: 13px; }
37
44
  .plugin-catalog form { margin-top: 8px; }
38
45
  .muted { color: #65736e; font-size: 13px; }
39
46
  .status { font-size: 12px; padding: 3px 7px; background: #e7ebea; border-radius: 4px; }
@@ -49,6 +49,16 @@ function createContext(config = {}) {
49
49
  strict_1.default.match(String(response.body), /插件设置/);
50
50
  strict_1.default.match(String(response.body), /通信记录/);
51
51
  strict_1.default.match(String(response.body), /<th scope="col">来源 Peer<\/th>/);
52
+ strict_1.default.match(String(response.body), /id="catalog" class="catalog" aria-live="polite"/);
53
+ strict_1.default.match(String(response.body), /<th scope="col">身份来源<\/th>/);
54
+ strict_1.default.match(String(response.body), /<tbody id="aids" aria-live="polite"><\/tbody>/);
55
+ strict_1.default.doesNotMatch(String(response.body), /id="package-name"/);
56
+ const scriptHandler = routes.get("/abweb/app.js");
57
+ strict_1.default.ok(scriptHandler);
58
+ const script = await scriptHandler({ method: "GET", path: "/abweb/app.js" });
59
+ strict_1.default.match(String(script.body), /"升级"/);
60
+ strict_1.default.match(String(script.body), /"已安装"/);
61
+ strict_1.default.match(String(script.body), /\/update/);
52
62
  strict_1.default.equal(response.headers["content-type"], "text/html; charset=utf-8");
53
63
  });
54
64
  (0, node_test_1.default)("abweb returns 404 for unknown static assets", async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mu-agent-bridge",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "将 AUN Agent 连接到本地桌面 AI 客户端的插件平台。",
5
5
  "main": "dist/src-ts/cli.js",
6
6
  "bin": { "mab": "dist/src-ts/cli.js", "agent-bridge": "dist/src-ts/cli.js" },