agent-bridge-abweb 1.0.5 → 1.0.7
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/dist/src-ts/plugin.js +13 -0
- package/dist/src-ts/plugins/abweb/index.js +1 -1
- package/dist/src-ts/plugins/abweb/public/app.js +104 -4
- package/dist/src-ts/plugins/abweb/public/app.ts +61 -5
- package/dist/src-ts/plugins/abweb/public/index.html +4 -0
- package/dist/src-ts/plugins/abweb/public/style.css +7 -0
- package/package.json +1 -1
- package/plugin.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveRuntimePort = resolveRuntimePort;
|
|
4
|
+
function resolveRuntimePort(config, runtime, pluginId, envName, fallback) {
|
|
5
|
+
const injected = runtime?.ports?.[pluginId];
|
|
6
|
+
if (typeof injected === "number" && Number.isInteger(injected) && injected > 0)
|
|
7
|
+
return injected;
|
|
8
|
+
const configured = config.cdpPort;
|
|
9
|
+
if (typeof configured === "number" && Number.isInteger(configured) && configured > 0)
|
|
10
|
+
return configured;
|
|
11
|
+
const environment = Number(process.env[envName] ?? "");
|
|
12
|
+
return Number.isInteger(environment) && environment > 0 ? environment : fallback;
|
|
13
|
+
}
|
|
@@ -16,6 +16,9 @@
|
|
|
16
16
|
const versionsNode = node("#versions");
|
|
17
17
|
const policyNode = node("#update-policy");
|
|
18
18
|
const policyStatusNode = node("#update-policy-status");
|
|
19
|
+
const trustProvidersNode = node("#trust-providers");
|
|
20
|
+
const trustPluginsNode = node("#trust-plugins");
|
|
21
|
+
const trustStatusNode = node("#plugin-trust-status");
|
|
19
22
|
const loginExpiry = Number(localStorage.getItem("mab-login-expires-at") ?? 0);
|
|
20
23
|
if (loginExpiry <= Date.now()) {
|
|
21
24
|
localStorage.removeItem("mab-login-token");
|
|
@@ -25,11 +28,32 @@
|
|
|
25
28
|
let plugins = [];
|
|
26
29
|
let selectedChannelId = "";
|
|
27
30
|
let pluginVersions = {};
|
|
31
|
+
let pluginTrustConfig = { providers: {}, plugins: {} };
|
|
32
|
+
let pluginTrustDirty = false;
|
|
28
33
|
let logPage = 1;
|
|
29
34
|
let logHasNext = false;
|
|
30
35
|
let loading = false;
|
|
31
36
|
const installingPackages = new Set();
|
|
32
37
|
function error(message) { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
38
|
+
function showResult(title, message, failed) {
|
|
39
|
+
node("#operation-result-title").textContent = title;
|
|
40
|
+
const detail = node("#operation-result-message");
|
|
41
|
+
detail.textContent = message;
|
|
42
|
+
detail.className = failed ? "error" : "";
|
|
43
|
+
const dialog = node("#operation-result");
|
|
44
|
+
if (!dialog.open)
|
|
45
|
+
dialog.showModal();
|
|
46
|
+
}
|
|
47
|
+
node("#operation-result-close").onclick = () => node("#operation-result").close();
|
|
48
|
+
window.addEventListener("message", (event) => {
|
|
49
|
+
const frame = channelPanel.querySelector("iframe.channel-frame");
|
|
50
|
+
if (event.origin !== window.location.origin || !frame || event.source !== frame.contentWindow)
|
|
51
|
+
return;
|
|
52
|
+
const result = event.data;
|
|
53
|
+
if (!result || result.type !== "mab:operation-result" || typeof result.title !== "string" || typeof result.message !== "string" || typeof result.failed !== "boolean")
|
|
54
|
+
return;
|
|
55
|
+
showResult(result.title, result.message, result.failed);
|
|
56
|
+
});
|
|
33
57
|
async function loadCatalog() {
|
|
34
58
|
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
35
59
|
const items = result.plugins ?? [];
|
|
@@ -108,7 +132,7 @@
|
|
|
108
132
|
item.type = "button";
|
|
109
133
|
item.textContent = label;
|
|
110
134
|
item.title = title;
|
|
111
|
-
item.onclick = () => { void run().catch((cause) =>
|
|
135
|
+
item.onclick = () => { item.disabled = true; void run().catch((cause) => { const message = cause instanceof Error ? cause.message : String(cause); error(message); showResult(`${title}失败`, message, true); }).finally(() => { item.disabled = false; void load(false); }); };
|
|
112
136
|
return item;
|
|
113
137
|
}
|
|
114
138
|
async function load(refreshCatalog = true) {
|
|
@@ -117,8 +141,8 @@
|
|
|
117
141
|
loading = true;
|
|
118
142
|
try {
|
|
119
143
|
error("");
|
|
120
|
-
const [health, plugins, messages, versions, policy, sessions] = await Promise.all([
|
|
121
|
-
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"),
|
|
144
|
+
const [health, plugins, messages, versions, policy, sessions, trust] = await Promise.all([
|
|
145
|
+
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"), request("/api/plugin-trust"),
|
|
122
146
|
]);
|
|
123
147
|
policyNode.value = policy.mode;
|
|
124
148
|
policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
@@ -127,6 +151,7 @@
|
|
|
127
151
|
node("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
128
152
|
node("#access-panel").hidden = true;
|
|
129
153
|
const installed = plugins.plugins ?? [];
|
|
154
|
+
renderPluginTrust(trust);
|
|
130
155
|
renderPlugins(installed);
|
|
131
156
|
renderChannels(installed);
|
|
132
157
|
renderMessages(messages.messages ?? []);
|
|
@@ -145,7 +170,6 @@
|
|
|
145
170
|
void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
|
|
146
171
|
}
|
|
147
172
|
catch (cause) {
|
|
148
|
-
node("#access-panel").hidden = false;
|
|
149
173
|
error(cause instanceof Error ? cause.message : String(cause));
|
|
150
174
|
}
|
|
151
175
|
finally {
|
|
@@ -200,6 +224,71 @@
|
|
|
200
224
|
if (!entries.length)
|
|
201
225
|
versionsNode.textContent = "尚未检查版本";
|
|
202
226
|
}
|
|
227
|
+
function renderPluginTrust(data) {
|
|
228
|
+
if (pluginTrustDirty)
|
|
229
|
+
return;
|
|
230
|
+
pluginTrustConfig = structuredClone(data.config ?? { providers: {}, plugins: {} });
|
|
231
|
+
trustProvidersNode.replaceChildren();
|
|
232
|
+
for (const provider of data.providers ?? []) {
|
|
233
|
+
const row = document.createElement("tr");
|
|
234
|
+
const name = document.createElement("td");
|
|
235
|
+
name.textContent = provider.name;
|
|
236
|
+
const current = document.createElement("td");
|
|
237
|
+
current.textContent = provider.trusted ? "可信" : "不可信";
|
|
238
|
+
const action = document.createElement("td");
|
|
239
|
+
const select = document.createElement("select");
|
|
240
|
+
select.setAttribute("aria-label", `${provider.name} 提供者信任策略`);
|
|
241
|
+
for (const [value, label] of [["inherit", "使用默认"], ["allow", "信任并自动升级"], ["deny", "取消信任"]]) {
|
|
242
|
+
const option = document.createElement("option");
|
|
243
|
+
option.value = value;
|
|
244
|
+
option.textContent = label;
|
|
245
|
+
select.append(option);
|
|
246
|
+
}
|
|
247
|
+
select.value = provider.decision;
|
|
248
|
+
select.onchange = () => { pluginTrustDirty = true; if (select.value === "inherit")
|
|
249
|
+
delete pluginTrustConfig.providers[provider.name];
|
|
250
|
+
else
|
|
251
|
+
pluginTrustConfig.providers[provider.name] = select.value; };
|
|
252
|
+
action.append(select);
|
|
253
|
+
row.append(name, current, action);
|
|
254
|
+
trustProvidersNode.append(row);
|
|
255
|
+
}
|
|
256
|
+
trustPluginsNode.replaceChildren();
|
|
257
|
+
for (const entry of data.entries ?? []) {
|
|
258
|
+
const row = document.createElement("tr");
|
|
259
|
+
for (const value of [entry.id, entry.packageName ?? "非 npm 插件", entry.publisher ?? "未知提供者", entry.autoUpdate ? "允许" : "禁止"]) {
|
|
260
|
+
const cell = document.createElement("td");
|
|
261
|
+
cell.textContent = value;
|
|
262
|
+
row.append(cell);
|
|
263
|
+
}
|
|
264
|
+
const action = document.createElement("td");
|
|
265
|
+
const select = document.createElement("select");
|
|
266
|
+
select.setAttribute("aria-label", `${entry.id} 插件自动升级策略`);
|
|
267
|
+
for (const [value, label] of [["inherit", "继承提供者"], ["allow", "允许自动升级"], ["deny", "禁止自动升级"]]) {
|
|
268
|
+
const option = document.createElement("option");
|
|
269
|
+
option.value = value;
|
|
270
|
+
option.textContent = label;
|
|
271
|
+
select.append(option);
|
|
272
|
+
}
|
|
273
|
+
select.value = entry.pluginOverride ?? "inherit";
|
|
274
|
+
select.onchange = () => { pluginTrustDirty = true; if (select.value === "inherit")
|
|
275
|
+
delete pluginTrustConfig.plugins[entry.id];
|
|
276
|
+
else
|
|
277
|
+
pluginTrustConfig.plugins[entry.id] = select.value; };
|
|
278
|
+
action.append(select);
|
|
279
|
+
row.append(action);
|
|
280
|
+
trustPluginsNode.append(row);
|
|
281
|
+
}
|
|
282
|
+
trustStatusNode.textContent = "";
|
|
283
|
+
if (!(data.entries ?? []).length) {
|
|
284
|
+
const row = document.createElement("tr");
|
|
285
|
+
const cell = document.createElement("td");
|
|
286
|
+
cell.colSpan = 5;
|
|
287
|
+
cell.textContent = "暂无可管理的 npm 插件";
|
|
288
|
+
row.append(cell);
|
|
289
|
+
trustPluginsNode.append(row);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
203
292
|
function renderPlugins(items) {
|
|
204
293
|
plugins = items;
|
|
205
294
|
pluginsNode.replaceChildren();
|
|
@@ -239,6 +328,16 @@
|
|
|
239
328
|
detail.textContent = `依赖:${plugin.dependencyStatus.join(";")}`;
|
|
240
329
|
state.append(detail);
|
|
241
330
|
}
|
|
331
|
+
// 插件未就绪且声明了客户端下载地址时,给出获取桌面客户端的入口
|
|
332
|
+
if (plugin.manifest?.clientDownloadUrl && plugin.status !== "ready") {
|
|
333
|
+
const link = document.createElement("a");
|
|
334
|
+
link.className = "plugin-id";
|
|
335
|
+
link.href = plugin.manifest.clientDownloadUrl;
|
|
336
|
+
link.target = "_blank";
|
|
337
|
+
link.rel = "noreferrer noopener";
|
|
338
|
+
link.textContent = "下载客户端";
|
|
339
|
+
state.append(link);
|
|
340
|
+
}
|
|
242
341
|
const controls = document.createElement("td");
|
|
243
342
|
controls.className = "controls";
|
|
244
343
|
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) {
|
|
@@ -319,6 +418,7 @@
|
|
|
319
418
|
node("#refresh").onclick = () => void load();
|
|
320
419
|
node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
|
|
321
420
|
node("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
|
|
421
|
+
node("#save-plugin-trust").onclick = () => { const button = node("#save-plugin-trust"); button.disabled = true; trustStatusNode.textContent = "正在保存…"; void request("/api/plugin-trust", "PATCH", pluginTrustConfig).then(() => { pluginTrustDirty = false; trustStatusNode.textContent = "信任设置已保存"; return load(false); }).catch((cause) => { trustStatusNode.textContent = String(cause); }).finally(() => { button.disabled = false; }); };
|
|
322
422
|
node("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?"))
|
|
323
423
|
void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
|
|
324
424
|
node("#pair-button").onclick = () => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
(() => {
|
|
2
|
-
type Plugin = { id: string; displayName: string; version: string; status: string; type?: string; capabilities?: string[]; error?: string; path: string; packageName?: string; dependencyStatus?: string[]; manifest?: { dependencies?: Record<string, string>; managementPage?: string } };
|
|
2
|
+
type Plugin = { id: string; displayName: string; version: string; status: string; type?: string; capabilities?: string[]; error?: string; path: string; packageName?: string; publisher?: string; trusted?: boolean; autoUpdate?: boolean; dependencyStatus?: string[]; manifest?: { dependencies?: Record<string, string>; managementPage?: string; clientDownloadUrl?: string } };
|
|
3
3
|
const node = <T extends HTMLElement>(selector: string): T => document.querySelector<T>(selector)!;
|
|
4
4
|
const pluginsNode = node<HTMLTableSectionElement>("#plugins");
|
|
5
5
|
const messagesNode = node<HTMLTableSectionElement>("#messages");
|
|
@@ -17,18 +17,38 @@
|
|
|
17
17
|
const versionsNode = node<HTMLDivElement>("#versions");
|
|
18
18
|
const policyNode = node<HTMLSelectElement>("#update-policy");
|
|
19
19
|
const policyStatusNode = node<HTMLSpanElement>("#update-policy-status");
|
|
20
|
+
const trustProvidersNode = node<HTMLTableSectionElement>("#trust-providers");
|
|
21
|
+
const trustPluginsNode = node<HTMLTableSectionElement>("#trust-plugins");
|
|
22
|
+
const trustStatusNode = node<HTMLSpanElement>("#plugin-trust-status");
|
|
20
23
|
const loginExpiry = Number(localStorage.getItem("mab-login-expires-at") ?? 0);
|
|
21
24
|
if (loginExpiry <= Date.now()) { localStorage.removeItem("mab-login-token"); localStorage.removeItem("mab-login-expires-at"); }
|
|
22
25
|
let authToken = localStorage.getItem("mab-login-token") ?? "";
|
|
23
26
|
let plugins: Plugin[] = [];
|
|
24
27
|
let selectedChannelId = "";
|
|
25
28
|
let pluginVersions: Record<string, any> = {};
|
|
29
|
+
let pluginTrustConfig: { providers: Record<string, "allow" | "deny">; plugins: Record<string, "allow" | "deny"> } = { providers: {}, plugins: {} };
|
|
30
|
+
let pluginTrustDirty = false;
|
|
26
31
|
let logPage = 1;
|
|
27
32
|
let logHasNext = false;
|
|
28
33
|
let loading = false;
|
|
29
34
|
const installingPackages = new Set<string>();
|
|
30
35
|
|
|
31
36
|
function error(message: string): void { errorNode.textContent = message; errorNode.hidden = !message; }
|
|
37
|
+
function showResult(title: string, message: string, failed: boolean): void {
|
|
38
|
+
node<HTMLElement>("#operation-result-title").textContent = title;
|
|
39
|
+
const detail = node<HTMLElement>("#operation-result-message");
|
|
40
|
+
detail.textContent = message; detail.className = failed ? "error" : "";
|
|
41
|
+
const dialog = node<HTMLDialogElement>("#operation-result");
|
|
42
|
+
if (!dialog.open) dialog.showModal();
|
|
43
|
+
}
|
|
44
|
+
node<HTMLButtonElement>("#operation-result-close").onclick = () => node<HTMLDialogElement>("#operation-result").close();
|
|
45
|
+
window.addEventListener("message", (event: MessageEvent) => {
|
|
46
|
+
const frame = channelPanel.querySelector<HTMLIFrameElement>("iframe.channel-frame");
|
|
47
|
+
if (event.origin !== window.location.origin || !frame || event.source !== frame.contentWindow) return;
|
|
48
|
+
const result = event.data;
|
|
49
|
+
if (!result || result.type !== "mab:operation-result" || typeof result.title !== "string" || typeof result.message !== "string" || typeof result.failed !== "boolean") return;
|
|
50
|
+
showResult(result.title, result.message, result.failed);
|
|
51
|
+
});
|
|
32
52
|
async function loadCatalog(): Promise<void> {
|
|
33
53
|
const result = await request(`/api/plugins/catalog?q=${encodeURIComponent(packageSearchNode.value.trim())}`);
|
|
34
54
|
const items: Array<{ name: string; version: string; description: string; type: string }> = result.plugins ?? [];
|
|
@@ -75,15 +95,15 @@
|
|
|
75
95
|
}
|
|
76
96
|
function button(label: string, title: string, run: () => Promise<void>): HTMLButtonElement {
|
|
77
97
|
const item = document.createElement("button"); item.type = "button"; item.textContent = label; item.title = title;
|
|
78
|
-
item.onclick = () => { void run().catch((cause) =>
|
|
98
|
+
item.onclick = () => { item.disabled = true; void run().catch((cause) => { const message = cause instanceof Error ? cause.message : String(cause); error(message); showResult(`${title}失败`, message, true); }).finally(() => { item.disabled = false; void load(false); }); }; return item;
|
|
79
99
|
}
|
|
80
100
|
async function load(refreshCatalog = true): Promise<void> {
|
|
81
101
|
if (loading) return;
|
|
82
102
|
loading = true;
|
|
83
103
|
try {
|
|
84
104
|
error("");
|
|
85
|
-
const [health, plugins, messages, versions, policy, sessions] = await Promise.all([
|
|
86
|
-
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"),
|
|
105
|
+
const [health, plugins, messages, versions, policy, sessions, trust] = await Promise.all([
|
|
106
|
+
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"), request("/api/plugin-trust"),
|
|
87
107
|
]);
|
|
88
108
|
policyNode.value = policy.mode; policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
89
109
|
renderVersions(versions);
|
|
@@ -91,6 +111,7 @@
|
|
|
91
111
|
node<HTMLElement>("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
92
112
|
node<HTMLElement>("#access-panel").hidden = true;
|
|
93
113
|
const installed: Plugin[] = plugins.plugins ?? [];
|
|
114
|
+
renderPluginTrust(trust);
|
|
94
115
|
renderPlugins(installed);
|
|
95
116
|
renderChannels(installed);
|
|
96
117
|
renderMessages(messages.messages ?? []);
|
|
@@ -101,7 +122,7 @@
|
|
|
101
122
|
sessionTable.append(row);
|
|
102
123
|
}
|
|
103
124
|
if (refreshCatalog) void loadCatalog().catch((cause) => { catalogNode.textContent = `远程插件目录暂时不可用:${cause instanceof Error ? cause.message : String(cause)}`; });
|
|
104
|
-
} catch (cause) {
|
|
125
|
+
} catch (cause) { error(cause instanceof Error ? cause.message : String(cause)); }
|
|
105
126
|
finally { loading = false; }
|
|
106
127
|
}
|
|
107
128
|
function renderChannels(items: Plugin[]): void {
|
|
@@ -133,6 +154,35 @@
|
|
|
133
154
|
}));
|
|
134
155
|
if (!entries.length) versionsNode.textContent = "尚未检查版本";
|
|
135
156
|
}
|
|
157
|
+
function renderPluginTrust(data: any): void {
|
|
158
|
+
if (pluginTrustDirty) return;
|
|
159
|
+
pluginTrustConfig = structuredClone(data.config ?? { providers: {}, plugins: {} });
|
|
160
|
+
trustProvidersNode.replaceChildren();
|
|
161
|
+
for (const provider of data.providers ?? []) {
|
|
162
|
+
const row = document.createElement("tr");
|
|
163
|
+
const name = document.createElement("td"); name.textContent = provider.name;
|
|
164
|
+
const current = document.createElement("td"); current.textContent = provider.trusted ? "可信" : "不可信";
|
|
165
|
+
const action = document.createElement("td");
|
|
166
|
+
const select = document.createElement("select"); select.setAttribute("aria-label", `${provider.name} 提供者信任策略`);
|
|
167
|
+
for (const [value, label] of [["inherit", "使用默认"], ["allow", "信任并自动升级"], ["deny", "取消信任"]]) { const option = document.createElement("option"); option.value = value; option.textContent = label; select.append(option); }
|
|
168
|
+
select.value = provider.decision;
|
|
169
|
+
select.onchange = () => { pluginTrustDirty = true; if (select.value === "inherit") delete pluginTrustConfig.providers[provider.name]; else pluginTrustConfig.providers[provider.name] = select.value as "allow" | "deny"; };
|
|
170
|
+
action.append(select); row.append(name, current, action); trustProvidersNode.append(row);
|
|
171
|
+
}
|
|
172
|
+
trustPluginsNode.replaceChildren();
|
|
173
|
+
for (const entry of data.entries ?? []) {
|
|
174
|
+
const row = document.createElement("tr");
|
|
175
|
+
for (const value of [entry.id, entry.packageName ?? "非 npm 插件", entry.publisher ?? "未知提供者", entry.autoUpdate ? "允许" : "禁止"]) { const cell = document.createElement("td"); cell.textContent = value; row.append(cell); }
|
|
176
|
+
const action = document.createElement("td");
|
|
177
|
+
const select = document.createElement("select"); select.setAttribute("aria-label", `${entry.id} 插件自动升级策略`);
|
|
178
|
+
for (const [value, label] of [["inherit", "继承提供者"], ["allow", "允许自动升级"], ["deny", "禁止自动升级"]]) { const option = document.createElement("option"); option.value = value; option.textContent = label; select.append(option); }
|
|
179
|
+
select.value = entry.pluginOverride ?? "inherit";
|
|
180
|
+
select.onchange = () => { pluginTrustDirty = true; if (select.value === "inherit") delete pluginTrustConfig.plugins[entry.id]; else pluginTrustConfig.plugins[entry.id] = select.value as "allow" | "deny"; };
|
|
181
|
+
action.append(select); row.append(action); trustPluginsNode.append(row);
|
|
182
|
+
}
|
|
183
|
+
trustStatusNode.textContent = "";
|
|
184
|
+
if (!(data.entries ?? []).length) { const row = document.createElement("tr"); const cell = document.createElement("td"); cell.colSpan = 5; cell.textContent = "暂无可管理的 npm 插件"; row.append(cell); trustPluginsNode.append(row); }
|
|
185
|
+
}
|
|
136
186
|
function renderPlugins(items: Plugin[]): void {
|
|
137
187
|
plugins = items;
|
|
138
188
|
pluginsNode.replaceChildren();
|
|
@@ -149,6 +199,11 @@
|
|
|
149
199
|
state.append(status);
|
|
150
200
|
if (plugin.error) { const detail = document.createElement("small"); detail.className = "error-text plugin-id"; detail.textContent = plugin.error; state.append(detail); }
|
|
151
201
|
if (plugin.dependencyStatus?.length) { const detail = document.createElement("small"); detail.className = "error-text plugin-id"; detail.textContent = `依赖:${plugin.dependencyStatus.join(";")}`; state.append(detail); }
|
|
202
|
+
// 插件未就绪且声明了客户端下载地址时,给出获取桌面客户端的入口
|
|
203
|
+
if (plugin.manifest?.clientDownloadUrl && plugin.status !== "ready") {
|
|
204
|
+
const link = document.createElement("a"); link.className = "plugin-id"; link.href = plugin.manifest.clientDownloadUrl; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
205
|
+
link.textContent = "下载客户端"; state.append(link);
|
|
206
|
+
}
|
|
152
207
|
const controls = document.createElement("td"); controls.className = "controls";
|
|
153
208
|
if (pluginVersions[plugin.id]?.state === "update-available" && plugin.path.includes("node_modules")) { controlsUpgrade(plugin.id); controls.append(button("下载", `下载 ${plugin.id}`, async () => { await request(`/api/plugins/${encodeURIComponent(plugin.id)}/download`, "POST", { packageName: plugin.packageName, version: pluginVersions[plugin.id]?.latestVersion }); await load(); })); }
|
|
154
209
|
for (const [label, action] of [["启动", "start"], ["停止", "stop"], ["重载", "reload"]]) {
|
|
@@ -195,6 +250,7 @@
|
|
|
195
250
|
node<HTMLButtonElement>("#refresh").onclick = () => void load();
|
|
196
251
|
node<HTMLButtonElement>("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
|
|
197
252
|
node<HTMLButtonElement>("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
|
|
253
|
+
node<HTMLButtonElement>("#save-plugin-trust").onclick = () => { const button = node<HTMLButtonElement>("#save-plugin-trust"); button.disabled = true; trustStatusNode.textContent = "正在保存…"; void request("/api/plugin-trust", "PATCH", pluginTrustConfig).then(() => { pluginTrustDirty = false; trustStatusNode.textContent = "信任设置已保存"; return load(false); }).catch((cause) => { trustStatusNode.textContent = String(cause); }).finally(() => { button.disabled = false; }); };
|
|
198
254
|
node<HTMLButtonElement>("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?")) void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
|
|
199
255
|
node<HTMLButtonElement>("#pair-button").onclick = () => {
|
|
200
256
|
const code = pairCodeNode.value.trim();
|
|
@@ -13,6 +13,7 @@
|
|
|
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><button id="logs-tab" type="button" role="tab" aria-selected="false" aria-controls="logs-panel">运行日志</button></nav>
|
|
14
14
|
<div id="settings-panel" role="tabpanel" aria-labelledby="settings-tab">
|
|
15
15
|
<section><div class="section-heading"><h2>版本管理</h2><div><button id="check-versions" type="button">检查更新</button><button id="update-core" type="button">升级后台</button></div></div><label for="update-policy">插件更新策略</label><select id="update-policy"><option value="auto-install">自动下载并安装</option><option value="download-only">自动下载,手动安装</option><option value="manual">手动下载和安装</option></select><button id="save-update-policy" type="button">保存策略</button><span id="update-policy-status" class="muted"></span><div id="versions" class="muted"></div></section>
|
|
16
|
+
<section id="plugin-trust"><div class="section-heading"><h2>自动升级信任</h2><button id="save-plugin-trust" type="button">保存信任设置</button></div><p class="muted">默认信任 m2lihui;未知提供者默认禁止自动升级。插件设置优先于提供者设置。</p><h3>提供者</h3><div class="table-scroll"><table class="trust-table"><thead><tr><th scope="col">npm 提供者</th><th scope="col">当前信任</th><th scope="col">策略</th></tr></thead><tbody id="trust-providers"></tbody></table></div><h3>插件</h3><div class="table-scroll"><table class="trust-table"><thead><tr><th scope="col">插件</th><th scope="col">npm 包</th><th scope="col">提供者</th><th scope="col">自动升级</th><th scope="col">单插件设置</th></tr></thead><tbody id="trust-plugins"></tbody></table></div><span id="plugin-trust-status" class="muted" role="status"></span></section>
|
|
16
17
|
<section><details id="installed-plugins" open><summary><h2>已安装插件</h2></summary><div class="table-scroll"><table class="plugin-table"><thead><tr><th scope="col">名称</th><th scope="col">类型</th><th scope="col">版本</th><th scope="col">状态</th><th scope="col">操作</th></tr></thead><tbody id="plugins"></tbody></table></div><div class="plugin-catalog"><div class="section-heading"><h3>npm 插件目录</h3><button id="search-packages" type="button">刷新目录</button></div><input id="package-search" placeholder="搜索插件名称或功能" aria-label="搜索 npm 插件"><div id="catalog" class="catalog" aria-live="polite"></div></div><form id="install-local-form"><input id="plugin-path" required placeholder="Windows 上的插件目录路径" aria-label="本地插件目录"><button type="submit">安装本地插件</button></form></details></section>
|
|
17
18
|
<section><div class="section-heading"><h2>Channel 设置</h2></div><nav id="channel-tabs" class="tabs channel-tabs" role="tablist" aria-label="Channel 设置"></nav><div id="channel-panel" role="tabpanel" aria-live="polite"></div></section>
|
|
18
19
|
</div>
|
|
@@ -20,6 +21,9 @@
|
|
|
20
21
|
<div id="logs-panel" role="tabpanel" aria-labelledby="logs-tab" hidden><section><div class="section-heading"><h2>运行日志</h2><button id="logs-refresh" type="button">刷新日志</button></div><div class="table-scroll"><table><thead><tr><th>时间</th><th>级别</th><th>来源</th><th>消息</th><th>详细字段</th></tr></thead><tbody id="logs"></tbody></table></div><p id="logs-empty" class="muted" hidden>暂无运行日志</p><div class="section-heading"><button id="logs-prev" type="button">上一页</button><span id="logs-page" class="muted">第 1 页</span><button id="logs-next" type="button">下一页</button></div></section></div>
|
|
21
22
|
<p id="error" class="error" role="alert" hidden></p>
|
|
22
23
|
</main>
|
|
24
|
+
<dialog id="operation-result" aria-labelledby="operation-result-title" aria-describedby="operation-result-message">
|
|
25
|
+
<h2 id="operation-result-title">操作结果</h2><p id="operation-result-message"></p><button id="operation-result-close" type="button" autofocus>确定</button>
|
|
26
|
+
</dialog>
|
|
23
27
|
<script src="/abweb/app.js"></script>
|
|
24
28
|
</body>
|
|
25
29
|
</html>
|
|
@@ -29,6 +29,9 @@ form { margin-top: 14px; flex-wrap: wrap; }
|
|
|
29
29
|
.plugin-table .controls { white-space: nowrap; }
|
|
30
30
|
.plugin-table .controls button { margin: 0 5px 5px 0; }
|
|
31
31
|
.plugin-id { display: block; margin-top: 4px; }
|
|
32
|
+
.trust-table { min-width: 620px; }
|
|
33
|
+
.trust-table th:nth-child(1) { width: 190px; }
|
|
34
|
+
#plugin-trust h3 { font-size: 14px; margin: 16px 0 5px; }
|
|
32
35
|
.plugin-catalog { border-top: 1px solid #e0e5e2; margin-top: 18px; padding-top: 14px; }
|
|
33
36
|
.plugin-catalog h3 { font-size: 14px; margin: 0; }
|
|
34
37
|
.plugin-catalog input { width: min(100%, 420px); margin: 10px 0; }
|
|
@@ -53,3 +56,7 @@ th { color: #566761; font-weight: 600; background: #edf1ef; }
|
|
|
53
56
|
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; }
|
|
54
57
|
.message-error td:last-child { color: #a83427; }
|
|
55
58
|
@media (max-width: 620px) { .shell { padding: 16px; } .section-heading { align-items: flex-start; flex-direction: column; } form, form input { width: 100%; } }
|
|
59
|
+
|
|
60
|
+
#operation-result { width: min(520px, calc(100% - 32px)); border: 1px solid #bfc9c5; border-radius: 8px; padding: 24px; }
|
|
61
|
+
#operation-result::backdrop { background: rgb(0 0 0 / 35%); }
|
|
62
|
+
#operation-result-message { white-space: pre-wrap; overflow-wrap: anywhere; margin: 16px 0; }
|
package/package.json
CHANGED