mu-agent-bridge 1.0.23 → 1.0.24
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 -0
- package/dist/src-ts/cli.js +64 -4
- package/dist/src-ts/control-server.js +6 -0
- package/dist/src-ts/plugin-manager.js +144 -5
- package/dist/src-ts/plugins/abweb/public/app.js +74 -2
- package/dist/src-ts/plugins/abweb/public/index.html +1 -0
- package/dist/src-ts/plugins/abweb/public/style.css +3 -0
- package/dist/src-ts/plugins/chatgpt/cdp-client.js +47 -0
- package/dist/src-ts/plugins/chatgpt/index.js +110 -6
- package/dist/src-ts/plugins/chatgpt/session-store.js +91 -0
- package/dist/tests-ts/abweb-plugin.test.js +3 -0
- package/dist/tests-ts/chatgpt-plugin.test.js +209 -0
- package/dist/tests-ts/control-server.test.js +36 -0
- package/dist/tests-ts/plugin-manager.test.js +112 -0
- package/package.json +1 -1
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.js +74 -2
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/app.ts +39 -3
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/index.html +1 -0
- package/plugins/abweb/dist/src-ts/plugins/abweb/public/style.css +3 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/cdp-client.js +47 -0
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/index.js +110 -6
- package/plugins/chatgpt/dist/src-ts/plugins/chatgpt/session-store.js +91 -0
- package/plugins/chatgpt/plugin.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,8 @@ OpenAI 兼容 API 为 `/v1/models` 和 `/v1/chat/completions`。管理页面位
|
|
|
20
20
|
安装后使用 `mab start` 启动。旧桥仍可使用 `17893`,不会与管理页面冲突。局域网访问时设置
|
|
21
21
|
`AGENT_BRIDGE_HOST=0.0.0.0` 和 `AGENT_BRIDGE_TOKEN`,然后访问
|
|
22
22
|
`http://<Windows 主机 IP>:26107/abweb/`。页面使用一次性配置码配对后管理插件;请只在可信网络使用。
|
|
23
|
+
如果 26107 被旧的 mab 进程占用,可执行 `mab restart`;命令会结束旧 daemon 并重新启动。`mab stop` 只停止 daemon,
|
|
24
|
+
`mab update` 会结束旧 daemon、安装最新核心包并启动新版本。不会把自己的旧进程误当成新的 daemon,也不会因此默认改用临时端口。
|
|
23
25
|
已有 AID 的连接状态由 AUN 插件自己建立和查询,只有当前已连接的身份才能关联 Agent。
|
|
24
26
|
AUN 插件通过 SDK 枚举 EvolCore 的本地 AID,并逐个建立连接;ABWeb 只展示已连接 AID,并按 EvolCore 与 mu-agent-bridge 分组,未连接身份不显示。状态检测不调用 `ec agent list/show`。
|
|
25
27
|
创建新 AID 必须填写 Owner AID,并安装、启动 AUN 插件;创建与通信由该插件完成。
|
|
@@ -60,6 +62,8 @@ mab start
|
|
|
60
62
|
|
|
61
63
|
插件更新策略可在 ABWeb 的“版本管理”中选择:默认“自动下载并安装”;“自动下载,手动安装”会把新版本缓存到状态目录并等待页面操作;“手动下载和安装”只检查版本。策略保存在 `state/update-policy.json`,插件下载缓存位于 `state/downloads`。
|
|
62
64
|
|
|
65
|
+
自动升级信任可在 ABWeb 的“自动升级信任”中设置。npm 提供者 `m2lihui` 默认可信,其他提供者默认不可信;用户可以取消提供者信任,也可以对单个插件设置允许或禁止。插件单独设置优先于提供者设置。信任配置保存在 `state/plugin-trust.json`,npm 发布者信息以 registry 元数据为准;查询失败时插件保持发现状态,但不会自动升级。旧的 `AGENT_BRIDGE_TRUSTED_PACKAGES` 包名名单继续兼容,直到对应提供者或插件存在明确设置。
|
|
66
|
+
|
|
63
67
|
运行时会发现核心包依赖和 `AGENT_BRIDGE_PLUGIN_PACKAGES` 中声明的 npm 包。插件包在自己的
|
|
64
68
|
`package.json` 中声明 `agentBridge.plugin`,平台复用统一的 `activate`、`deactivate`、`health`、
|
|
65
69
|
`handle` 生命周期。已加载插件可以通过管理 API 重载;新增 npm 包后重启 daemon 最可靠。
|
package/dist/src-ts/cli.js
CHANGED
|
@@ -114,9 +114,63 @@ async function update() {
|
|
|
114
114
|
console.error("当前平台未配置后台升级器");
|
|
115
115
|
return 2;
|
|
116
116
|
}
|
|
117
|
+
await stopDaemon();
|
|
117
118
|
await execFileAsync("npm.cmd", ["install", "-g", "mu-agent-bridge@latest", "--no-fund", "--no-audit"], { timeout: 120_000 });
|
|
118
|
-
await
|
|
119
|
-
console.log("
|
|
119
|
+
await startDaemon();
|
|
120
|
+
console.log("核心已更新,旧 daemon 已结束并启动新版本");
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
async function stopDaemon(stopTask = true) {
|
|
124
|
+
let pid;
|
|
125
|
+
try {
|
|
126
|
+
const address = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(stateDirectory(), "daemon-address.json"), "utf8"));
|
|
127
|
+
if (Number.isInteger(address.pid) && address.pid > 0)
|
|
128
|
+
pid = address.pid;
|
|
129
|
+
}
|
|
130
|
+
catch { /* 地址文件不存在时继续尝试计划任务 */ }
|
|
131
|
+
if (pid && process.platform === "win32") {
|
|
132
|
+
try {
|
|
133
|
+
await execFileAsync("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { timeout: 15_000 });
|
|
134
|
+
}
|
|
135
|
+
catch { /* 进程已退出 */ }
|
|
136
|
+
}
|
|
137
|
+
else if (pid) {
|
|
138
|
+
try {
|
|
139
|
+
process.kill(pid);
|
|
140
|
+
}
|
|
141
|
+
catch { /* 进程已退出 */ }
|
|
142
|
+
}
|
|
143
|
+
if (stopTask && process.platform === "win32") {
|
|
144
|
+
try {
|
|
145
|
+
await execFileAsync("schtasks.exe", ["/End", "/TN", "mab"], { timeout: 15_000 });
|
|
146
|
+
}
|
|
147
|
+
catch { /* 任务不存在或已停止 */ }
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
await (0, promises_1.rm)((0, node_path_1.resolve)(stateDirectory(), "daemon-address.json"), { force: true });
|
|
151
|
+
}
|
|
152
|
+
catch { /* 忽略清理失败 */ }
|
|
153
|
+
}
|
|
154
|
+
async function startDaemon() {
|
|
155
|
+
if (process.platform === "win32") {
|
|
156
|
+
try {
|
|
157
|
+
await execFileAsync("schtasks.exe", ["/Run", "/TN", "mab"], { timeout: 15_000 });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
catch { /* 计划任务不存在时回退到直接启动 */ }
|
|
161
|
+
}
|
|
162
|
+
const child = (0, node_child_process_1.spawn)(process.execPath, [(0, node_path_1.resolve)(__dirname, "cli.js"), "start"], { detached: true, stdio: "ignore", windowsHide: true });
|
|
163
|
+
child.unref();
|
|
164
|
+
}
|
|
165
|
+
async function stop() {
|
|
166
|
+
await stopDaemon();
|
|
167
|
+
console.log("旧 daemon 已停止");
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
async function restart() {
|
|
171
|
+
await stopDaemon();
|
|
172
|
+
await startDaemon();
|
|
173
|
+
console.log("旧 daemon 已结束,已启动新 daemon");
|
|
120
174
|
return 0;
|
|
121
175
|
}
|
|
122
176
|
async function status() {
|
|
@@ -269,14 +323,20 @@ async function main() {
|
|
|
269
323
|
return pair();
|
|
270
324
|
if (args[0] === "status")
|
|
271
325
|
return status();
|
|
326
|
+
if (args[0] === "stop")
|
|
327
|
+
return stop();
|
|
328
|
+
if (args[0] === "restart")
|
|
329
|
+
return restart();
|
|
272
330
|
if (args[0] === "update")
|
|
273
331
|
return update();
|
|
274
332
|
const selfTestIndex = args.indexOf("--self-test");
|
|
275
333
|
if (selfTestIndex >= 0)
|
|
276
334
|
return selfTest(args[selfTestIndex + 1] ?? "hi");
|
|
277
|
-
if (args.includes("--serve") || args.includes("start"))
|
|
335
|
+
if (args.includes("--serve") || args.includes("start")) {
|
|
336
|
+
await stopDaemon(false);
|
|
278
337
|
return serve();
|
|
279
|
-
|
|
338
|
+
}
|
|
339
|
+
console.error("用法:mab start | mab stop | mab restart | mab update | mab status | mab --self-test <任务>");
|
|
280
340
|
return 2;
|
|
281
341
|
}
|
|
282
342
|
main().then((code) => { process.exitCode = code; }).catch((error) => {
|
|
@@ -149,6 +149,12 @@ class ControlServer {
|
|
|
149
149
|
return this.json(res, 200, await this.options.manager.updatePolicy());
|
|
150
150
|
if (method === "PATCH" && url.pathname === "/api/update-policy")
|
|
151
151
|
return this.json(res, 200, await this.options.manager.saveUpdatePolicy(await this.body(req)).then(() => ({ ok: true })));
|
|
152
|
+
if (method === "GET" && url.pathname === "/api/plugin-trust")
|
|
153
|
+
return this.json(res, 200, await this.options.manager.pluginTrust());
|
|
154
|
+
if (method === "PATCH" && url.pathname === "/api/plugin-trust") {
|
|
155
|
+
await this.options.manager.savePluginTrust(await this.body(req));
|
|
156
|
+
return this.json(res, 200, { ok: true, ...(await this.options.manager.pluginTrust()) });
|
|
157
|
+
}
|
|
152
158
|
if (method === "POST" && url.pathname === "/api/versions/check")
|
|
153
159
|
return this.json(res, 200, { versions: await this.options.manager.checkPackageUpdates() });
|
|
154
160
|
if (method === "POST" && url.pathname === "/api/update") {
|
|
@@ -7,6 +7,7 @@ exports.PluginManager = void 0;
|
|
|
7
7
|
const promises_1 = require("node:fs/promises");
|
|
8
8
|
const node_fs_1 = require("node:fs");
|
|
9
9
|
const node_module_1 = require("node:module");
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
11
|
const node_child_process_1 = require("node:child_process");
|
|
11
12
|
const node_util_1 = require("node:util");
|
|
12
13
|
const node_path_1 = require("node:path");
|
|
@@ -25,6 +26,10 @@ class PluginManager {
|
|
|
25
26
|
httpRoutes = new Map();
|
|
26
27
|
root;
|
|
27
28
|
messages = [];
|
|
29
|
+
trustConfig = defaultPluginTrustConfig();
|
|
30
|
+
trustLoaded = false;
|
|
31
|
+
publisherLookups = new Map();
|
|
32
|
+
pendingPublisherLookups = new Set();
|
|
28
33
|
versions;
|
|
29
34
|
logs;
|
|
30
35
|
sessions;
|
|
@@ -45,6 +50,35 @@ class PluginManager {
|
|
|
45
50
|
} }
|
|
46
51
|
async saveUpdatePolicy(policy) { if (!isUpdatePolicy(policy))
|
|
47
52
|
throw new agent_1.AgentError("更新策略无效"); await (0, promises_1.mkdir)(this.options.stateDir, { recursive: true }); await (0, promises_1.writeFile)((0, node_path_1.join)(this.options.stateDir, "update-policy.json"), JSON.stringify(policy, null, 2)); this.log("info", `插件更新策略已设置:${policy.mode}`, undefined, "升级策略"); }
|
|
53
|
+
async pluginTrust() {
|
|
54
|
+
await this.loadPluginTrust();
|
|
55
|
+
const entries = this.list().filter((record) => record.packageName).map((record) => ({
|
|
56
|
+
id: record.id, packageName: record.packageName, publisher: record.publisher,
|
|
57
|
+
providerOverride: record.publisher ? this.trustConfig.providers[normalizeProvider(record.publisher)] : undefined,
|
|
58
|
+
pluginOverride: this.trustConfig.plugins[record.id], trusted: Boolean(record.trusted), autoUpdate: Boolean(record.autoUpdate),
|
|
59
|
+
}));
|
|
60
|
+
const providerNames = new Set([...Object.keys(this.trustConfig.providers), ...entries.map((entry) => entry.publisher ? normalizeProvider(entry.publisher) : "").filter(Boolean)]);
|
|
61
|
+
const providers = [...providerNames].sort().map((name) => ({ name, decision: this.trustConfig.providers[name] ?? "inherit", trusted: this.trustConfig.providers[name] === "allow" || (!this.trustConfig.providers[name] && name === "m2lihui") }));
|
|
62
|
+
return { config: structuredClone(this.trustConfig), providers, entries };
|
|
63
|
+
}
|
|
64
|
+
async savePluginTrust(value) {
|
|
65
|
+
const config = validatePluginTrustConfig(value);
|
|
66
|
+
await (0, promises_1.mkdir)(this.options.stateDir, { recursive: true });
|
|
67
|
+
const target = (0, node_path_1.join)(this.options.stateDir, "plugin-trust.json");
|
|
68
|
+
const temporary = `${target}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
69
|
+
try {
|
|
70
|
+
await (0, promises_1.writeFile)(temporary, JSON.stringify(config, null, 2));
|
|
71
|
+
await (0, promises_1.rename)(temporary, target);
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
await (0, promises_1.rm)(temporary, { force: true });
|
|
75
|
+
}
|
|
76
|
+
this.trustConfig = mergeDefaultPluginTrustConfig(config);
|
|
77
|
+
this.trustLoaded = true;
|
|
78
|
+
for (const { record } of this.plugins.values())
|
|
79
|
+
this.applyPluginTrust(record);
|
|
80
|
+
this.log("info", "插件自动升级信任设置已保存", { providers: Object.keys(config.providers).length, plugins: Object.keys(config.plugins).length }, "插件信任");
|
|
81
|
+
}
|
|
48
82
|
async downloadPackage(packageName, version) {
|
|
49
83
|
if (!/^agent-bridge-[a-z0-9-]+$/.test(packageName))
|
|
50
84
|
throw new agent_1.AgentError("仅支持 agent-bridge-* npm 包名");
|
|
@@ -79,6 +113,7 @@ class PluginManager {
|
|
|
79
113
|
}
|
|
80
114
|
async cachedPackage(packageName, version) { const target = (0, node_path_1.join)(this.options.stateDir, "downloads", packageName, version ?? "latest"); return (0, node_fs_1.existsSync)((0, node_path_1.join)(target, "node_modules", packageName)) ? target : undefined; }
|
|
81
115
|
async discover() {
|
|
116
|
+
await this.loadPluginTrust();
|
|
82
117
|
await (0, promises_1.mkdir)(this.root, { recursive: true });
|
|
83
118
|
await this.discoverNpmPackages();
|
|
84
119
|
await this.discoverManagedPackages();
|
|
@@ -99,6 +134,7 @@ class PluginManager {
|
|
|
99
134
|
await this.discoverDirectory(dir, `${id.name}/${version.name}`);
|
|
100
135
|
}
|
|
101
136
|
}
|
|
137
|
+
await this.resolvePendingPublisherLookups();
|
|
102
138
|
return this.list();
|
|
103
139
|
}
|
|
104
140
|
list() { return [...this.plugins.values()].map(({ record }) => ({ ...record, capabilities: [...record.capabilities], dependencyStatus: this.dependencyStatus(record) })); }
|
|
@@ -114,6 +150,7 @@ class PluginManager {
|
|
|
114
150
|
}
|
|
115
151
|
async checkPackageUpdates() {
|
|
116
152
|
await this.versions.load();
|
|
153
|
+
await this.refreshPluginPublishers();
|
|
117
154
|
const core = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.resolve)(__dirname, "../../package.json"), "utf8"));
|
|
118
155
|
return Promise.all([this.versions.checkPackage("mu-agent-bridge", core.version), ...this.list().map(async (record) => {
|
|
119
156
|
const info = this.packageInfo(record.id);
|
|
@@ -219,6 +256,7 @@ class PluginManager {
|
|
|
219
256
|
throw new agent_1.AgentError("npm 包与插件清单不匹配");
|
|
220
257
|
this.validateCandidate(manifest, meta.agentBridge);
|
|
221
258
|
await this.discoverDirectory(directory, name, manifestFile);
|
|
259
|
+
await this.resolvePendingPublisherLookups();
|
|
222
260
|
const record = this.plugins.get(manifest.id)?.record;
|
|
223
261
|
if (!record)
|
|
224
262
|
throw new agent_1.AgentError("插件发现失败");
|
|
@@ -608,6 +646,7 @@ class PluginManager {
|
|
|
608
646
|
}
|
|
609
647
|
catch { /* 无效包由安装时报告 */ }
|
|
610
648
|
}
|
|
649
|
+
await this.resolvePendingPublisherLookups();
|
|
611
650
|
}
|
|
612
651
|
async discoverDirectory(directory, label, manifestFile = (0, node_path_1.join)(directory, "plugin.json")) {
|
|
613
652
|
try {
|
|
@@ -615,12 +654,10 @@ class PluginManager {
|
|
|
615
654
|
if (this.plugins.has(manifest.id))
|
|
616
655
|
return;
|
|
617
656
|
let packageName;
|
|
618
|
-
let publisher;
|
|
619
657
|
let coreConstraint;
|
|
620
658
|
try {
|
|
621
659
|
const packageJson = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(directory, "package.json"), "utf8"));
|
|
622
660
|
packageName = packageJson.name;
|
|
623
|
-
publisher = packageJson._npmUser?.name ?? packageJson.maintainers?.[0]?.name;
|
|
624
661
|
coreConstraint = packageJson.agentBridge?.requires?.["mu-agent-bridge"];
|
|
625
662
|
const declared = packageJson.agentBridge?.requires?.plugins;
|
|
626
663
|
if (declared && JSON.stringify(declared) !== JSON.stringify(manifest.dependencies ?? {}))
|
|
@@ -632,14 +669,78 @@ class PluginManager {
|
|
|
632
669
|
}
|
|
633
670
|
const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
|
|
634
671
|
const error = coreConstraint && !(0, version_manager_1.satisfiesVersion)(coreVersion, coreConstraint) ? `核心版本 ${coreVersion} 不满足插件要求 ${coreConstraint}` : undefined;
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
this.plugins.set(manifest.id, { record
|
|
672
|
+
const record = { id: manifest.id, version: manifest.version, type: manifest.type ?? "other", displayName: manifest.displayName ?? manifest.id, status: error ? "failed" : "discovered", capabilities: manifest.capabilities, manifest, path: directory, ...(packageName ? { packageName } : {}), ...(coreConstraint ? { coreConstraint } : {}), ...(error ? { error } : {}) };
|
|
673
|
+
this.applyPluginTrust(record);
|
|
674
|
+
this.plugins.set(manifest.id, { record });
|
|
675
|
+
if (packageName && /^agent-bridge-[a-z0-9-]+$/.test(packageName)) {
|
|
676
|
+
const pending = this.lookupPublisher(packageName, manifest.version).then((name) => {
|
|
677
|
+
if (!name)
|
|
678
|
+
return;
|
|
679
|
+
record.publisher = name;
|
|
680
|
+
this.applyPluginTrust(record);
|
|
681
|
+
}).catch((lookupError) => {
|
|
682
|
+
this.log("warn", `无法查询插件 npm 提供者:${manifest.id}`, { packageName, error: lookupError instanceof Error ? lookupError.message : String(lookupError) }, "插件信任");
|
|
683
|
+
}).finally(() => this.pendingPublisherLookups.delete(pending));
|
|
684
|
+
this.pendingPublisherLookups.add(pending);
|
|
685
|
+
}
|
|
638
686
|
}
|
|
639
687
|
catch (error) {
|
|
640
688
|
throw new agent_1.AgentError(`invalid plugin ${label}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
641
689
|
}
|
|
642
690
|
}
|
|
691
|
+
async loadPluginTrust() {
|
|
692
|
+
if (this.trustLoaded)
|
|
693
|
+
return;
|
|
694
|
+
try {
|
|
695
|
+
const value = JSON.parse(await (0, promises_1.readFile)((0, node_path_1.join)(this.options.stateDir, "plugin-trust.json"), "utf8"));
|
|
696
|
+
this.trustConfig = mergeDefaultPluginTrustConfig(validatePluginTrustConfig(value));
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
if (error.code !== "ENOENT")
|
|
700
|
+
this.log("warn", "插件信任配置无效,已使用默认设置", { error: error instanceof Error ? error.message : String(error) }, "插件信任");
|
|
701
|
+
this.trustConfig = defaultPluginTrustConfig();
|
|
702
|
+
}
|
|
703
|
+
this.trustLoaded = true;
|
|
704
|
+
}
|
|
705
|
+
applyPluginTrust(record) {
|
|
706
|
+
if (!record.packageName)
|
|
707
|
+
return;
|
|
708
|
+
const provider = record.publisher ? normalizeProvider(record.publisher) : undefined;
|
|
709
|
+
const legacyTrusted = (process.env.AGENT_BRIDGE_TRUSTED_PACKAGES ?? "").split(",").map((name) => name.trim()).includes(record.packageName);
|
|
710
|
+
const decision = this.trustConfig.plugins[record.id]
|
|
711
|
+
?? (provider ? this.trustConfig.providers[provider] : undefined)
|
|
712
|
+
?? (legacyTrusted ? "allow" : undefined)
|
|
713
|
+
?? "deny";
|
|
714
|
+
record.trusted = decision === "allow";
|
|
715
|
+
record.autoUpdate = decision === "allow";
|
|
716
|
+
}
|
|
717
|
+
async lookupPublisher(packageName, version) {
|
|
718
|
+
const cacheKey = `${packageName}@${version}`;
|
|
719
|
+
const cached = this.publisherLookups.get(cacheKey);
|
|
720
|
+
if (cached)
|
|
721
|
+
return cached;
|
|
722
|
+
const lookup = (this.options.publisherResolver ?? resolveNpmPublisher)(packageName, version).then((name) => name?.trim() || undefined).catch((error) => {
|
|
723
|
+
this.publisherLookups.delete(cacheKey);
|
|
724
|
+
throw error;
|
|
725
|
+
});
|
|
726
|
+
this.publisherLookups.set(cacheKey, lookup);
|
|
727
|
+
return lookup;
|
|
728
|
+
}
|
|
729
|
+
async resolvePendingPublisherLookups() { await Promise.all([...this.pendingPublisherLookups]); }
|
|
730
|
+
async refreshPluginPublishers() {
|
|
731
|
+
const pending = [];
|
|
732
|
+
for (const { record } of this.plugins.values()) {
|
|
733
|
+
if (!record.packageName || !/^agent-bridge-[a-z0-9-]+$/.test(record.packageName))
|
|
734
|
+
continue;
|
|
735
|
+
pending.push(this.lookupPublisher(record.packageName, record.version).then((publisher) => {
|
|
736
|
+
if (publisher && publisher !== record.publisher) {
|
|
737
|
+
record.publisher = publisher;
|
|
738
|
+
this.applyPluginTrust(record);
|
|
739
|
+
}
|
|
740
|
+
}).catch(() => undefined));
|
|
741
|
+
}
|
|
742
|
+
await Promise.all(pending);
|
|
743
|
+
}
|
|
643
744
|
checkCompatibility(record) {
|
|
644
745
|
const coreVersion = require((0, node_path_1.resolve)(__dirname, "../../package.json")).version;
|
|
645
746
|
if (record.coreConstraint && !(0, version_manager_1.satisfiesVersion)(coreVersion, record.coreConstraint))
|
|
@@ -702,3 +803,41 @@ class PluginManager {
|
|
|
702
803
|
}
|
|
703
804
|
exports.PluginManager = PluginManager;
|
|
704
805
|
function isUpdatePolicy(value) { return Boolean(value && typeof value === "object" && ["auto-install", "download-only", "manual"].includes(value.mode)); }
|
|
806
|
+
function defaultPluginTrustConfig() { return { providers: { m2lihui: "allow" }, plugins: {} }; }
|
|
807
|
+
function mergeDefaultPluginTrustConfig(value) { return { providers: { m2lihui: "allow", ...value.providers }, plugins: { ...value.plugins } }; }
|
|
808
|
+
function normalizeProvider(value) { return value.trim().toLowerCase(); }
|
|
809
|
+
function validatePluginTrustConfig(value) {
|
|
810
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
811
|
+
throw new agent_1.AgentError("插件信任配置必须是对象");
|
|
812
|
+
const source = value;
|
|
813
|
+
if (Object.keys(value).some((key) => key !== "providers" && key !== "plugins"))
|
|
814
|
+
throw new agent_1.AgentError("插件信任配置包含未知字段");
|
|
815
|
+
const readDecisions = (input, label, normalize) => {
|
|
816
|
+
if (input === undefined)
|
|
817
|
+
return {};
|
|
818
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
819
|
+
throw new agent_1.AgentError(`${label}设置必须是对象`);
|
|
820
|
+
const entries = Object.entries(input);
|
|
821
|
+
if (entries.length > 500)
|
|
822
|
+
throw new agent_1.AgentError(`${label}设置数量超出限制`);
|
|
823
|
+
const result = {};
|
|
824
|
+
for (const [rawName, decision] of entries) {
|
|
825
|
+
const name = normalize ? normalizeProvider(rawName) : rawName;
|
|
826
|
+
if (!/^[a-z0-9][a-z0-9._-]{0,127}$/i.test(name))
|
|
827
|
+
throw new agent_1.AgentError(`${label}名称无效:${rawName}`);
|
|
828
|
+
if (decision !== "allow" && decision !== "deny")
|
|
829
|
+
throw new agent_1.AgentError(`${label}信任值只能是 allow 或 deny`);
|
|
830
|
+
result[name] = decision;
|
|
831
|
+
}
|
|
832
|
+
return result;
|
|
833
|
+
};
|
|
834
|
+
return { providers: readDecisions(source.providers, "提供者", true), plugins: readDecisions(source.plugins, "插件", false) };
|
|
835
|
+
}
|
|
836
|
+
async function resolveNpmPublisher(packageName, version) {
|
|
837
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, { signal: AbortSignal.timeout(5_000) });
|
|
838
|
+
if (!response.ok)
|
|
839
|
+
throw new Error(`npm 查询失败:HTTP ${response.status}`);
|
|
840
|
+
const metadata = await response.json();
|
|
841
|
+
const publisher = metadata.versions?.[version]?._npmUser?.name ?? metadata.maintainers?.[0]?.name;
|
|
842
|
+
return typeof publisher === "string" ? publisher : undefined;
|
|
843
|
+
}
|
|
@@ -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,6 +28,8 @@
|
|
|
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;
|
|
@@ -117,8 +122,8 @@
|
|
|
117
122
|
loading = true;
|
|
118
123
|
try {
|
|
119
124
|
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"),
|
|
125
|
+
const [health, plugins, messages, versions, policy, sessions, trust] = await Promise.all([
|
|
126
|
+
request("/api/health"), request("/api/plugins"), request("/api/messages"), request("/api/versions"), request("/api/update-policy"), request("/api/sessions"), request("/api/plugin-trust"),
|
|
122
127
|
]);
|
|
123
128
|
policyNode.value = policy.mode;
|
|
124
129
|
policyStatusNode.textContent = `当前:${policyNode.options[policyNode.selectedIndex]?.text ?? policy.mode}`;
|
|
@@ -127,6 +132,7 @@
|
|
|
127
132
|
node("#health").textContent = `服务运行中 · ${health.plugins} 个插件`;
|
|
128
133
|
node("#access-panel").hidden = true;
|
|
129
134
|
const installed = plugins.plugins ?? [];
|
|
135
|
+
renderPluginTrust(trust);
|
|
130
136
|
renderPlugins(installed);
|
|
131
137
|
renderChannels(installed);
|
|
132
138
|
renderMessages(messages.messages ?? []);
|
|
@@ -199,6 +205,71 @@
|
|
|
199
205
|
if (!entries.length)
|
|
200
206
|
versionsNode.textContent = "尚未检查版本";
|
|
201
207
|
}
|
|
208
|
+
function renderPluginTrust(data) {
|
|
209
|
+
if (pluginTrustDirty)
|
|
210
|
+
return;
|
|
211
|
+
pluginTrustConfig = structuredClone(data.config ?? { providers: {}, plugins: {} });
|
|
212
|
+
trustProvidersNode.replaceChildren();
|
|
213
|
+
for (const provider of data.providers ?? []) {
|
|
214
|
+
const row = document.createElement("tr");
|
|
215
|
+
const name = document.createElement("td");
|
|
216
|
+
name.textContent = provider.name;
|
|
217
|
+
const current = document.createElement("td");
|
|
218
|
+
current.textContent = provider.trusted ? "可信" : "不可信";
|
|
219
|
+
const action = document.createElement("td");
|
|
220
|
+
const select = document.createElement("select");
|
|
221
|
+
select.setAttribute("aria-label", `${provider.name} 提供者信任策略`);
|
|
222
|
+
for (const [value, label] of [["inherit", "使用默认"], ["allow", "信任并自动升级"], ["deny", "取消信任"]]) {
|
|
223
|
+
const option = document.createElement("option");
|
|
224
|
+
option.value = value;
|
|
225
|
+
option.textContent = label;
|
|
226
|
+
select.append(option);
|
|
227
|
+
}
|
|
228
|
+
select.value = provider.decision;
|
|
229
|
+
select.onchange = () => { pluginTrustDirty = true; if (select.value === "inherit")
|
|
230
|
+
delete pluginTrustConfig.providers[provider.name];
|
|
231
|
+
else
|
|
232
|
+
pluginTrustConfig.providers[provider.name] = select.value; };
|
|
233
|
+
action.append(select);
|
|
234
|
+
row.append(name, current, action);
|
|
235
|
+
trustProvidersNode.append(row);
|
|
236
|
+
}
|
|
237
|
+
trustPluginsNode.replaceChildren();
|
|
238
|
+
for (const entry of data.entries ?? []) {
|
|
239
|
+
const row = document.createElement("tr");
|
|
240
|
+
for (const value of [entry.id, entry.packageName ?? "非 npm 插件", entry.publisher ?? "未知提供者", entry.autoUpdate ? "允许" : "禁止"]) {
|
|
241
|
+
const cell = document.createElement("td");
|
|
242
|
+
cell.textContent = value;
|
|
243
|
+
row.append(cell);
|
|
244
|
+
}
|
|
245
|
+
const action = document.createElement("td");
|
|
246
|
+
const select = document.createElement("select");
|
|
247
|
+
select.setAttribute("aria-label", `${entry.id} 插件自动升级策略`);
|
|
248
|
+
for (const [value, label] of [["inherit", "继承提供者"], ["allow", "允许自动升级"], ["deny", "禁止自动升级"]]) {
|
|
249
|
+
const option = document.createElement("option");
|
|
250
|
+
option.value = value;
|
|
251
|
+
option.textContent = label;
|
|
252
|
+
select.append(option);
|
|
253
|
+
}
|
|
254
|
+
select.value = entry.pluginOverride ?? "inherit";
|
|
255
|
+
select.onchange = () => { pluginTrustDirty = true; if (select.value === "inherit")
|
|
256
|
+
delete pluginTrustConfig.plugins[entry.id];
|
|
257
|
+
else
|
|
258
|
+
pluginTrustConfig.plugins[entry.id] = select.value; };
|
|
259
|
+
action.append(select);
|
|
260
|
+
row.append(action);
|
|
261
|
+
trustPluginsNode.append(row);
|
|
262
|
+
}
|
|
263
|
+
trustStatusNode.textContent = "";
|
|
264
|
+
if (!(data.entries ?? []).length) {
|
|
265
|
+
const row = document.createElement("tr");
|
|
266
|
+
const cell = document.createElement("td");
|
|
267
|
+
cell.colSpan = 5;
|
|
268
|
+
cell.textContent = "暂无可管理的 npm 插件";
|
|
269
|
+
row.append(cell);
|
|
270
|
+
trustPluginsNode.append(row);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
202
273
|
function renderPlugins(items) {
|
|
203
274
|
plugins = items;
|
|
204
275
|
pluginsNode.replaceChildren();
|
|
@@ -328,6 +399,7 @@
|
|
|
328
399
|
node("#refresh").onclick = () => void load();
|
|
329
400
|
node("#check-versions").onclick = () => void request("/api/versions/check", "POST", {}).then(() => load()).catch((cause) => error(String(cause)));
|
|
330
401
|
node("#save-update-policy").onclick = () => void request("/api/update-policy", "PATCH", { mode: policyNode.value }).then(() => load()).catch((cause) => error(String(cause)));
|
|
402
|
+
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; }); };
|
|
331
403
|
node("#update-core").onclick = () => { if (confirm("确认升级后台并自动重启?"))
|
|
332
404
|
void request("/api/update", "POST", {}).then(() => { error("后台正在升级,页面将在几秒后恢复"); }).catch((cause) => error(String(cause))); };
|
|
333
405
|
node("#pair-button").onclick = () => {
|
|
@@ -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>
|
|
@@ -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; }
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ChatgptCdpClient = exports.userBubbleSelector = exports.turnSelector = exports.editorSelector = void 0;
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const session_store_1 = require("./session-store");
|
|
5
6
|
exports.editorSelector = '[data-codex-composer="true"][contenteditable="true"]';
|
|
6
7
|
exports.turnSelector = '[data-chatgpt-conversation-turn="true"]';
|
|
7
8
|
exports.userBubbleSelector = "[data-user-message-bubble]";
|
|
@@ -11,6 +12,7 @@ const contentUnitSelector = "[data-content-search-unit-key]";
|
|
|
11
12
|
const sendButtonSelector = 'button[aria-label="Send"], button[aria-label="发送"]';
|
|
12
13
|
const stopButtonSelector = 'button[aria-label="Stop"], button[aria-label="停止"]';
|
|
13
14
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
15
|
+
const sidebarSelector = "[data-app-action-sidebar-thread-id]";
|
|
14
16
|
class ChatgptCdpClient {
|
|
15
17
|
socket;
|
|
16
18
|
tunnel;
|
|
@@ -29,6 +31,51 @@ class ChatgptCdpClient {
|
|
|
29
31
|
this.ssh = options.ssh;
|
|
30
32
|
}
|
|
31
33
|
connected() { return Boolean(this.socket); }
|
|
34
|
+
async listSessions() {
|
|
35
|
+
const entries = await this.evaluate(`(() => {
|
|
36
|
+
return [...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].map(item => ({
|
|
37
|
+
id: item.getAttribute('data-app-action-sidebar-thread-id'),
|
|
38
|
+
host: item.getAttribute('data-app-action-sidebar-thread-host-id'),
|
|
39
|
+
title: item.getAttribute('data-app-action-sidebar-thread-title')
|
|
40
|
+
}));
|
|
41
|
+
})()`);
|
|
42
|
+
if (!Array.isArray(entries))
|
|
43
|
+
throw new Error("ChatGPT 侧栏会话列表格式无效");
|
|
44
|
+
return entries.filter(session_store_1.validSession);
|
|
45
|
+
}
|
|
46
|
+
async activeSession() {
|
|
47
|
+
const entry = await this.evaluate(`(() => {
|
|
48
|
+
const item = document.querySelector('[data-app-action-sidebar-thread-selected="true"]');
|
|
49
|
+
return item ? {
|
|
50
|
+
id: item.getAttribute('data-app-action-sidebar-thread-id'),
|
|
51
|
+
host: item.getAttribute('data-app-action-sidebar-thread-host-id'),
|
|
52
|
+
title: item.getAttribute('data-app-action-sidebar-thread-title')
|
|
53
|
+
} : null;
|
|
54
|
+
})()`);
|
|
55
|
+
return (0, session_store_1.validSession)(entry) ? entry : undefined;
|
|
56
|
+
}
|
|
57
|
+
async switchSession(session) {
|
|
58
|
+
const locate = `[...document.querySelectorAll(${JSON.stringify(sidebarSelector)})].find(item =>
|
|
59
|
+
item.getAttribute('data-app-action-sidebar-thread-id') === ${JSON.stringify(session.id)}
|
|
60
|
+
&& item.getAttribute('data-app-action-sidebar-thread-host-id') === ${JSON.stringify(session.host)})`;
|
|
61
|
+
const clicked = await this.evaluate(`(() => {
|
|
62
|
+
const editor = document.querySelector(${JSON.stringify(exports.editorSelector)});
|
|
63
|
+
if (editor?.innerText.trim() || document.querySelector(${JSON.stringify(stopButtonSelector)})) {
|
|
64
|
+
throw new Error('当前会话有未发送内容或正在生成回复,不能切换');
|
|
65
|
+
}
|
|
66
|
+
const item = ${locate};
|
|
67
|
+
if (!item) return false;
|
|
68
|
+
item.click();
|
|
69
|
+
return true;
|
|
70
|
+
})()`);
|
|
71
|
+
if (!clicked)
|
|
72
|
+
throw new Error("ChatGPT 侧栏中找不到目标会话");
|
|
73
|
+
await this.waitFor(`(() => {
|
|
74
|
+
const item = ${locate};
|
|
75
|
+
return item?.getAttribute('data-app-action-sidebar-thread-selected') === 'true'
|
|
76
|
+
&& Boolean(document.querySelector(${JSON.stringify(exports.editorSelector)}));
|
|
77
|
+
})()`, "ChatGPT 会话切换未确认,未发送消息");
|
|
78
|
+
}
|
|
32
79
|
async connect() {
|
|
33
80
|
if (this.socket)
|
|
34
81
|
return;
|