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 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 最可靠。
@@ -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 execFileAsync("schtasks.exe", ["/Run", "/TN", "mab"]);
119
- console.log("核心已更新,已请求 mab 计划任务重新启动");
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
- console.error("用法:mab start | mab status | mab --self-test <任务>");
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 trustedNames = new Set((process.env.AGENT_BRIDGE_TRUSTED_PACKAGES ?? "").split(",").map((name) => name.trim()).filter(Boolean));
636
- const trusted = Boolean(packageName && trustedNames.has(packageName));
637
- this.plugins.set(manifest.id, { 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, publisher, trusted, autoUpdate: trusted } : {}), ...(coreConstraint ? { coreConstraint } : {}), ...(error ? { error } : {}) } });
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;