agent-bridge-aun 1.0.45-sdk0531.4 → 1.0.46

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
@@ -6,6 +6,7 @@
6
6
  mab directory list --self agentrouter.agentid.cn --page 1 --limit 20 --format json
7
7
  mab directory show test001.agentid.cn --self agentrouter.agentid.cn --format json
8
8
  mab aun repair-owner old.agentid.cn --owner owner.agentid.cn --json
9
+ mab aun repair-old-aids --json
9
10
  ```
10
11
 
11
12
  `mab aun directory` 与 `mab directory` 等价;新核心提供快捷入口,旧核心可使用前者。`show` 支持完整 AID(可带 `@`)或完整名称,`info` 与 `show` 等价。`--format json` 与 `--json` 等价,输出 `{ok,message,data}`;失败码在 `data.code`,SDK 拒绝读取时附带 `data.sdkCode`。只有一个连接时可省略 `--self`,多连接必须指定。
@@ -14,6 +15,8 @@ mab aun repair-owner old.agentid.cn --owner owner.agentid.cn --json
14
15
 
15
16
  旧 AID 修复不重新注册、不替换密钥、不改变 Agent 关联、不重启桌面。拥有本地 Owner 私钥时走 SDK 双边修复,否则提交正式请求等待合法 Owner 确认。仅新鲜共识核验成功才返回 `data.status: repaired`;SDK 任务返回 completed 而共识未完成时仍返回 `pending`。`data.directorySync` 单独报告名录同步结果;用户已有的 PA 限制与屏蔽项会保留。
16
17
 
18
+ `mab aun repair-old-aids --json` 会串行扫描已连接且关联桌面 Agent 的 AID,按每个 AID 的新鲜签名 Owner 声明执行同一修复流程。只会处理同 Owner 旧关系;无签名 Owner 声明的 AID 会跳过,Owner 需要确认时返回操作号,单项失败不会中断整批操作。CLI 连接关闭后不会启动后续 AID 的修复;已经进入的 SDK 操作不支持取消,仍可能完成当前 AID。
19
+
17
20
  `agent-bridge-aun` 为 mu-agent-bridge 提供 AUN 消息通道,支持使用已有 AID 或通过管理 API 注册 AID,并将收到的文本交给指定 Agent 插件处理。
18
21
 
19
22
  来源白名单默认留空,表示接受所有 AUN Peer。需要限制来源时,可在 ABWeb 的 AUN 通道设置中填写一个或多个 AID,并用英文逗号分隔;清空白名单也会恢复接受所有 Peer。
@@ -26,6 +29,16 @@ mab aun repair-owner old.agentid.cn --owner owner.agentid.cn --json
26
29
 
27
30
  该插件依赖 `@agentunion/fastaun`,要求 Node.js 22.16 或更高版本。
28
31
 
32
+ ## Owner 名片资料同步
33
+
34
+ 已连接的 Agent 身份在连接前订阅 SDK 的 `owned_agentmd.changed`。子 AID 使用 SDK `AIDStore.uploadAgentMd()` 更新名片后,SDK 自动通知其当前 Owner;插件以 Owner 身份刷新 `/data/directories/v2/contacts.json` 中的资料,保留用户自定义名称、备注、屏蔽项、PA 权限和未知字段。签名时间及 `agent_md_etag` 表示已同步版本;更新后的能力说明仍由对应 AID 的 `agent.md` 提供,不把名片正文混入联系人权限。
35
+
36
+ Owner 没有关联 Agent 时,可在 AUN 插件配置增加 `"owner_watch_aids": ["owner.agentid.cn"]`,重载 AUN 插件生效。默认空列表;仅能加载本地已有身份,不下载私钥。专用监听使用独立稳定 slot,不注册普通聊天处理器,不出现在 Agent 的已连接 AID 列表。健康详情的 `ownerWatchAids` 显示已连接的监听身份,初次连接失败每 30 秒重试。
37
+
38
+ 变更事件先原子写入插件数据目录的 `owned-profiles.json`,完成名录更新后标记处理成功;重复事件不重复执行。失败每 30 秒重试,重载后恢复未完成项;每次处理读取最新验签名片和归属,归属转移后不再写入旧 Owner 名录。账本损坏时停止资料同步并保留原文件,普通聊天继续运行。日志包含 Owner、子 AID、事件编号、阶段和具体错误;健康详情提供 `ownedProfileSync` 与 `ownedProfilePending`。
39
+
40
+ 名片上传成功仅表示提交成功,不能据此声称 Owner 已收到或名录已刷新。SDK 当前没有公开的未发送通知 flush 接口;插件只能重试已经收到的应用事件,不能保证补发未进入服务端持久消息链路的通知。本轮没有增加名片编辑器。
41
+
29
42
  ## 原生文件命令
30
43
 
31
44
  `mab fs` 与 `mab aun fs` 共用 AUN 插件当前连接,不要求安装或运行 EvolCore。`--as` 只能选择后台已连接的身份;只有一个连接时可省略,多连接时必须指定。远端后端根据经过验证的名片类型选择,不依赖 Owner 名录同步。
@@ -33,6 +33,13 @@
33
33
  "type": "string",
34
34
  "minLength": 1
35
35
  },
36
+ "owner_watch_aids": {
37
+ "type": "array",
38
+ "default": [],
39
+ "uniqueItems": true,
40
+ "items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9_-]{2,63}\\.[a-z0-9-]+(?:\\.[a-z0-9-]+)+$" },
41
+ "description": "显式监听名片变更的本地 Owner AID。独立连接不转发聊天;修改后重载 AUN 插件生效。"
42
+ },
36
43
  "agent_plugin": {
37
44
  "type": "string",
38
45
  "description": "兼容旧配置;消息只按 AID 的显式关联路由。"
@@ -28,8 +28,9 @@ const owner_directory_1 = require("./owner-directory");
28
28
  const directory_1 = require("./directory");
29
29
  const ownership_1 = require("./ownership");
30
30
  const sdk_loader_1 = require("./sdk-loader");
31
+ const owned_profiles_1 = require("./owned-profiles");
31
32
  const manifest = {
32
- id: "aun", version: "1.0.45-sdk0531.4", type: "channel", displayName: "AUN 通道",
33
+ id: "aun", version: "1.0.46", type: "channel", displayName: "AUN 通道",
33
34
  entry: "dist/src-ts/plugins/aun/index.js", apiVersion: "1", process: "in-process",
34
35
  capabilities: ["channel", "channel.identity.create", "channel.receive", "channel.send", "health", "cli"],
35
36
  configSchema: "config.schema.json",
@@ -74,6 +75,7 @@ const cliCommands = [
74
75
  { name: "unbind", summary: "解除指定 AID 与 Agent 的关联", usage: "mab aun unbind <aid>", examples: ["mab aun unbind old.agentid.cn"] },
75
76
  { name: "directory", summary: "只读查询当前身份的 Owner 名录", usage: directory_1.AUN_DIRECTORY_USAGE },
76
77
  { name: "repair-owner", summary: "显式修复旧 AID 的同 Owner 归属及名录", usage: "mab aun repair-owner <已连接 AID> --owner <Owner AID> [--json]" },
78
+ { name: "repair-old-aids", summary: "检查并修复已关联桌面 Agent 的旧 AID", usage: "mab aun repair-old-aids [--json]" },
77
79
  { name: "fs", summary: "使用当前 AUN 连接访问原生文件系统", usage: fs_commands_1.AUN_FS_USAGE, examples: ["mab fs stat remote.agentid.cn:/readme.txt --as local.agentid.cn --json", "mab aun fs cat remote.agentid.cn:/readme.txt --as local.agentid.cn"] },
78
80
  ];
79
81
  class AunCommandError extends Error {
@@ -92,6 +94,11 @@ class AunPlugin {
92
94
  sdk;
93
95
  store;
94
96
  clients = new Map();
97
+ ownerClients = new Map();
98
+ ownerStore;
99
+ ownedProfiles;
100
+ ownedProfileTimer;
101
+ ownedProfileRetry;
95
102
  identities = new Set();
96
103
  legacyCreatedAids = new Set();
97
104
  createdAids = new Set();
@@ -387,6 +394,7 @@ class AunPlugin {
387
394
  connection.subscription = undefined;
388
395
  }
389
396
  if (!connection) {
397
+ this.clients.get(aid)?.ownedSubscription?.unsubscribe();
390
398
  try {
391
399
  await this.clients.get(aid)?.client.close();
392
400
  }
@@ -432,6 +440,7 @@ class AunPlugin {
432
440
  }
433
441
  const connection = this.clients.get(aid);
434
442
  connection?.subscription?.unsubscribe();
443
+ connection?.ownedSubscription?.unsubscribe();
435
444
  try {
436
445
  await connection?.client.close();
437
446
  }
@@ -451,7 +460,7 @@ class AunPlugin {
451
460
  async syncOwnerDirectory(aid, ownerAid) {
452
461
  if (aid === ownerAid)
453
462
  throw new Error("新 AID 与 Owner AID 相同,未写入 Owner 名录");
454
- let client = this.clients.get(ownerAid)?.client;
463
+ let client = this.ownerClients.get(ownerAid)?.client ?? this.clients.get(ownerAid)?.client;
455
464
  let temporary = false;
456
465
  if (!client) {
457
466
  const loaded = this.store?.load(ownerAid);
@@ -531,6 +540,8 @@ class AunPlugin {
531
540
  }
532
541
  if (this.config.verify_ssl !== undefined && typeof this.config.verify_ssl !== "boolean")
533
542
  throw new Error("verify_ssl 必须是布尔值");
543
+ if (this.config.owner_watch_aids !== undefined && (!Array.isArray(this.config.owner_watch_aids) || !this.config.owner_watch_aids.every(aid => typeof aid === "string" && aidPattern.test(aid))))
544
+ throw new Error("owner_watch_aids 必须是有效 Owner AID 数组");
534
545
  if (this.config.delivery_mode !== undefined && !["fanout", "queue"].includes(String(this.config.delivery_mode)))
535
546
  throw new Error("delivery_mode 只能是 fanout 或 queue");
536
547
  if (this.config.aid && !aidPattern.test(String(this.config.aid)))
@@ -560,6 +571,12 @@ class AunPlugin {
560
571
  context.logger.error("AUN 对端消息账本不可读,已停止对端发送与查询;普通聊天仍可用", { error: safeError(error) });
561
572
  }
562
573
  this.store = this.newStore();
574
+ try {
575
+ this.ownedProfiles = await owned_profiles_1.OwnedProfileSync.open((0, node_path_1.join)(context.dataDir, "owned-profiles.json"), (ownerAid, event) => this.refreshOwnedProfile(ownerAid, event));
576
+ }
577
+ catch (error) {
578
+ context.logger.error("Owner 资料同步账本不可读,已停用资料同步;普通聊天仍可用", { error: safeError(error) });
579
+ }
563
580
  for (const [method, path, handler] of [
564
581
  ["POST", "/api/plugins/aun/aid", this.createAid],
565
582
  ["GET", "/api/plugins/aun/identities", this.listIdentities],
@@ -612,6 +629,107 @@ class AunPlugin {
612
629
  if (aidPattern.test(aid))
613
630
  await this.connectAid(aid);
614
631
  }
632
+ if (this.ownedProfiles) {
633
+ try {
634
+ await this.retryOwnedProfiles();
635
+ }
636
+ catch (error) {
637
+ context.logger.warn("Owner 资料同步恢复失败,将继续重试;普通聊天仍可用", { stage: "恢复资料同步账本", error: safeError(error) });
638
+ }
639
+ this.ownedProfileTimer = setInterval(() => {
640
+ void this.retryOwnedProfiles().catch(error => context.logger.warn("Owner 资料同步重试失败", { stage: "持久化资料同步结果", error: safeError(error) }));
641
+ }, 30_000);
642
+ this.ownedProfileTimer.unref();
643
+ }
644
+ }
645
+ async retryOwnedProfiles() {
646
+ if (this.ownedProfileRetry)
647
+ return this.ownedProfileRetry;
648
+ const run = (async () => {
649
+ for (const ownerAid of new Set(this.config.owner_watch_aids)) {
650
+ if (!this.acceptingMessages)
651
+ return;
652
+ if (!this.ownerClients.has(ownerAid))
653
+ await this.connectOwner(ownerAid);
654
+ }
655
+ if (this.acceptingMessages)
656
+ await this.ownedProfiles?.retry();
657
+ })();
658
+ this.ownedProfileRetry = run;
659
+ try {
660
+ await run;
661
+ }
662
+ finally {
663
+ this.ownedProfileRetry = undefined;
664
+ }
665
+ }
666
+ async connectOwner(ownerAid) {
667
+ try {
668
+ await this.prepareTrustRoots(ownerAid);
669
+ this.ownerStore ??= this.newStore("mab-owned-profiles");
670
+ const loaded = this.ownerStore.load(ownerAid);
671
+ if (!loaded.ok || !loaded.data)
672
+ throw new Error("Owner AID 不在本地密钥库,无法监听资料变更");
673
+ const client = new this.sdk.AUNClient(loaded.data.aid);
674
+ const subscription = client.on("owned_agentmd.changed", payload => this.onOwnedProfile(ownerAid, payload));
675
+ this.ownerClients.set(ownerAid, { client, subscription });
676
+ try {
677
+ await client.connect({ auto_reconnect: true });
678
+ }
679
+ catch (error) {
680
+ this.ownerClients.delete(ownerAid);
681
+ subscription.unsubscribe();
682
+ try {
683
+ await client.close();
684
+ }
685
+ catch (closeError) {
686
+ this.context?.logger.warn("Owner 资料监听连接失败后的关闭也失败", { ownerAid, error: safeError(closeError) });
687
+ }
688
+ throw error;
689
+ }
690
+ this.context?.logger.info("Owner 资料监听已连接", { ownerAid });
691
+ }
692
+ catch (error) {
693
+ this.context?.logger.warn("Owner 资料监听连接失败", { ownerAid, stage: "连接 Owner", error: safeError(error) });
694
+ }
695
+ }
696
+ async onOwnedProfile(ownerAid, payload) {
697
+ if (!this.acceptingMessages || !this.ownedProfiles)
698
+ throw new Error("Owner 资料同步尚未就绪");
699
+ try {
700
+ await this.ownedProfiles.handle(ownerAid, payload);
701
+ }
702
+ catch (error) {
703
+ this.context?.logger.warn("Owner 资料事件处理失败,未确认完成", { ownerAid, stage: "持久化并处理资料事件", error: safeError(error) });
704
+ throw error;
705
+ }
706
+ }
707
+ async refreshOwnedProfile(ownerAid, event) {
708
+ let stage = "读取最新名片及归属";
709
+ try {
710
+ const client = this.ownerClients.get(ownerAid)?.client ?? this.clients.get(ownerAid)?.client;
711
+ if (!client?.storage || !this.store)
712
+ throw new Error("Owner 当前未连接或 Storage 不可用,保留待同步事件");
713
+ const verified = await (0, ownership_1.readAunOwnership)(this.store, client, event.aid);
714
+ if (verified.ownership.status !== "consensus")
715
+ throw new Error(`归属尚未确认:${verified.ownership.status}`);
716
+ if (verified.ownership.owner_aid !== ownerAid) {
717
+ this.context?.logger.info("名片已不属于原 Owner,取消旧名录同步", { ownerAid, aid: event.aid, eventId: event.event_id });
718
+ return;
719
+ }
720
+ const card = verified.agentMd;
721
+ const signedAt = Number(card.signature?.timestamp);
722
+ if (!card.etag || !Number.isSafeInteger(signedAt) || signedAt < 0)
723
+ throw new Error("已验证名片缺少有效 ETag 或签名时间");
724
+ const document = (0, yaml_1.parseDocument)(card.content.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)[1], { uniqueKeys: true }).toJS({ maxAliasCount: 0 });
725
+ stage = "写入 Owner 名录";
726
+ await (0, owner_directory_1.addOwnerContact)(client.storage, event.aid, ownerAid, Date.now(), { name: typeof document.name === "string" ? document.name : undefined, type: verified.type, signedAt, etag: card.etag });
727
+ this.context?.logger.info("Owner 名片资料已同步", { ownerAid, aid: event.aid, eventId: event.event_id, etag: card.etag });
728
+ }
729
+ catch (error) {
730
+ this.context?.logger.warn("Owner 名片资料同步失败,将重试", { ownerAid, aid: event.aid, eventId: event.event_id, stage, error: safeError(error) });
731
+ throw error;
732
+ }
615
733
  }
616
734
  subscribeAid(aid) {
617
735
  const connection = this.clients.get(aid);
@@ -626,18 +744,22 @@ class AunPlugin {
626
744
  if (!loaded.ok || !loaded.data)
627
745
  throw new Error(loaded.error?.message ?? "本地密钥库中不存在该 AID");
628
746
  const client = new this.sdk.AUNClient(loaded.data.aid);
747
+ const ownedSubscription = this.ownedProfiles && !this.config.owner_watch_aids?.includes(aid)
748
+ ? client.on("owned_agentmd.changed", payload => this.onOwnedProfile(aid, payload)) : undefined;
629
749
  const subscription = this.bindings[aid]
630
750
  ? client.on("message.received", (payload) => this.onMessage(payload, aid))
631
751
  : undefined;
752
+ this.clients.set(aid, { client, subscription, ownedSubscription });
632
753
  try {
633
754
  await client.connect();
634
755
  }
635
756
  catch (error) {
757
+ this.clients.delete(aid);
636
758
  subscription?.unsubscribe();
759
+ ownedSubscription?.unsubscribe();
637
760
  await client.close();
638
761
  throw error;
639
762
  }
640
- this.clients.set(aid, { client, subscription });
641
763
  this.status = "ready";
642
764
  this.healthMessage = "已连接";
643
765
  }
@@ -651,9 +773,35 @@ class AunPlugin {
651
773
  async deactivate(_reason) {
652
774
  this.status = "degraded";
653
775
  this.acceptingMessages = false;
776
+ if (this.ownedProfileTimer)
777
+ clearInterval(this.ownedProfileTimer);
778
+ this.ownedProfileTimer = undefined;
654
779
  this.ownerProof.clear();
655
- for (const { subscription } of this.clients.values())
780
+ for (const { subscription, ownedSubscription } of this.clients.values()) {
656
781
  subscription?.unsubscribe();
782
+ ownedSubscription?.unsubscribe();
783
+ }
784
+ for (const { subscription } of this.ownerClients.values())
785
+ subscription.unsubscribe();
786
+ await this.ownedProfileRetry?.catch(() => undefined);
787
+ await this.ownedProfiles?.close();
788
+ this.ownedProfiles = undefined;
789
+ for (const [ownerAid, { client }] of this.ownerClients) {
790
+ try {
791
+ await client.close();
792
+ }
793
+ catch (error) {
794
+ this.context?.logger.warn("关闭 Owner 资料监听失败,继续清理", { ownerAid, error: safeError(error) });
795
+ }
796
+ }
797
+ this.ownerClients.clear();
798
+ try {
799
+ await this.ownerStore?.close();
800
+ }
801
+ catch (error) {
802
+ this.context?.logger.warn("关闭 Owner 资料密钥库失败,继续清理", { error: safeError(error) });
803
+ }
804
+ this.ownerStore = undefined;
657
805
  await this.abilityOperations?.interruptExecuting();
658
806
  await Promise.allSettled(this.inboundMessages);
659
807
  await Promise.allSettled(this.pendingMessages.values());
@@ -671,7 +819,7 @@ class AunPlugin {
671
819
  this.context = undefined;
672
820
  }
673
821
  async health() {
674
- return { status: this.clients.size ? "ready" : this.status, message: this.healthMessage, details: { aid: this.aid || undefined, connected: this.clients.size > 0, aids: [...this.clients.keys()], identities: [...this.identities], bindings: this.bindings } };
822
+ return { status: this.clients.size || this.ownerClients.size ? "ready" : this.status, message: this.healthMessage, details: { aid: this.aid || undefined, connected: this.clients.size > 0, aids: [...this.clients.keys()], identities: [...this.identities], bindings: this.bindings, ownerWatchAids: [...this.ownerClients.keys()], ownedProfileSync: this.ownedProfiles ? "ready" : "unavailable", ownedProfilePending: this.ownedProfiles?.pending } };
675
823
  }
676
824
  async handle(_request) { throw new Error("AUN 是入站通道插件"); }
677
825
  path() {
@@ -680,9 +828,9 @@ class AunPlugin {
680
828
  return configured;
681
829
  return (0, node_path_1.resolve)(process.env.AUN_HOME ?? process.env.EVOLCORE_HOME ?? (0, node_path_1.join)((0, node_os_1.homedir)(), ".evolcore"));
682
830
  }
683
- newStore() {
831
+ newStore(slotId) {
684
832
  const rootCaPath = this.rootCaPath();
685
- return new this.sdk.AIDStore({ aunPath: this.path(), encryptionSeed: typeof this.config.encryption_seed === "string" && this.config.encryption_seed ? this.config.encryption_seed : "evol", verifySsl: typeof this.config.verify_ssl === "boolean" ? this.config.verify_ssl : true, rootCaPath: (0, node_fs_1.existsSync)(rootCaPath) ? rootCaPath : undefined });
833
+ return new this.sdk.AIDStore({ aunPath: this.path(), encryptionSeed: typeof this.config.encryption_seed === "string" && this.config.encryption_seed ? this.config.encryption_seed : "evol", verifySsl: typeof this.config.verify_ssl === "boolean" ? this.config.verify_ssl : true, rootCaPath: (0, node_fs_1.existsSync)(rootCaPath) ? rootCaPath : undefined, ...(slotId ? { slotId } : {}) });
686
834
  }
687
835
  rootCaPath() {
688
836
  return typeof this.config.root_ca_path === "string" && this.config.root_ca_path
@@ -1092,6 +1240,11 @@ class AunPlugin {
1092
1240
  this.json(response, 200, { protocolVersion: 1, plugin: "aun", version: manifest.version, commands: cliCommands });
1093
1241
  };
1094
1242
  runCommand = async (request, response) => {
1243
+ const controller = new AbortController();
1244
+ const onClose = () => { if (!response.writableEnded)
1245
+ controller.abort(); };
1246
+ if (typeof response.on === "function")
1247
+ response.on("close", onClose);
1095
1248
  try {
1096
1249
  const chunks = [];
1097
1250
  let size = 0;
@@ -1112,22 +1265,31 @@ class AunPlugin {
1112
1265
  if (!input || typeof input !== "object" || input.protocolVersion !== 1 || !Array.isArray(input.argv) || !input.argv.every((arg) => typeof arg === "string")) {
1113
1266
  throw new AunCommandError("命令请求格式无效");
1114
1267
  }
1115
- if (["fs", "directory", "repair-owner"].includes(input.argv[0]) && !this.context?.managementActor?.(request))
1268
+ const command = input.argv[0];
1269
+ const legacyLocalCommand = !this.context?.managementActor && ["directory", "repair-owner", "repair-old-aids"].includes(command) && isLoopback(request.socket?.remoteAddress);
1270
+ if (["fs", "directory", "repair-owner", "repair-old-aids"].includes(command) && !this.context?.managementActor?.(request) && !legacyLocalCommand)
1116
1271
  throw new AunCommandError("AUN 文件、名录及归属命令需要本机或已认证的管理身份", 403, { code: "MANAGEMENT_AUTH_REQUIRED" });
1117
- const result = await this.executeCommand(input.argv);
1118
- this.commandResult(response, 200, true, result.message, result.data);
1272
+ const result = await this.executeCommand(input.argv, controller.signal);
1273
+ const batchFailed = command === "repair-old-aids" && !!result.data && typeof result.data === "object" && Number(result.data.failed) > 0;
1274
+ this.commandResult(response, batchFailed ? 502 : 200, !batchFailed, result.message, result.data);
1119
1275
  }
1120
1276
  catch (error) {
1121
1277
  const failure = error instanceof AunCommandError ? error : error instanceof fs_1.AunFsError ? new AunCommandError(safeError(error), 400, { code: error.code, ...(error.sdkCode === undefined ? {} : { sdkCode: error.sdkCode }) }) : new AunCommandError(safeError(error), 502);
1122
1278
  this.commandResult(response, failure.statusCode, false, failure.message, failure.data);
1123
1279
  }
1280
+ finally {
1281
+ if (typeof response.off === "function")
1282
+ response.off("close", onClose);
1283
+ }
1124
1284
  };
1125
- async executeCommand(argv) {
1285
+ async executeCommand(argv, signal) {
1126
1286
  const [command, ...args] = argv;
1127
1287
  if (command === "directory")
1128
1288
  return this.directoryCommand(args);
1129
1289
  if (command === "repair-owner")
1130
1290
  return this.repairOwnerCommand(args);
1291
+ if (command === "repair-old-aids")
1292
+ return this.repairOldAidsCommand(args, signal);
1131
1293
  if (command === "fs") {
1132
1294
  if (!args.length || args.length === 1 && ["help", "--help"].includes(args[0]))
1133
1295
  return { message: fs_commands_1.AUN_FS_USAGE };
@@ -1289,6 +1451,51 @@ class AunPlugin {
1289
1451
  this.repairingOwners.delete(aid);
1290
1452
  }
1291
1453
  }
1454
+ async repairOldAidsCommand(args, signal) {
1455
+ if (args.length)
1456
+ throw new AunCommandError("用法:mab aun repair-old-aids [--json]");
1457
+ const aids = Object.keys(this.bindings).filter(aid => this.clients.has(aid)).sort();
1458
+ const results = [];
1459
+ let scanned = 0;
1460
+ for (const aid of aids) {
1461
+ if (signal?.aborted)
1462
+ break;
1463
+ scanned++;
1464
+ try {
1465
+ const client = this.clients.get(aid).client;
1466
+ const verified = await (0, ownership_1.readAunOwnership)(this.store, client, aid);
1467
+ if (signal?.aborted) {
1468
+ results.push({ aid, status: "cancelled", reason: "CLI 连接已关闭,未开始归属修复" });
1469
+ break;
1470
+ }
1471
+ if (verified.type !== "agent" || !verified.declaredOwner || !["consensus", "single_sided", "unbound"].includes(verified.ownership.status)) {
1472
+ results.push({ aid, status: "skipped", reason: "没有可验证的同 Owner 旧关系,未执行修复" });
1473
+ continue;
1474
+ }
1475
+ const repaired = await this.repairOwnerCommand([aid, "--owner", verified.declaredOwner]);
1476
+ results.push({ aid, ...repaired.data, message: repaired.message });
1477
+ }
1478
+ catch (error) {
1479
+ const details = error instanceof AunCommandError && error.data && typeof error.data === "object" ? error.data : undefined;
1480
+ const ownerAid = typeof details?.ownerAid === "string" ? details.ownerAid : undefined;
1481
+ results.push({
1482
+ aid,
1483
+ ...(ownerAid ? { ownerAid } : {}),
1484
+ nextAction: ownerAid ? "请将 AID、Owner AID 和错误码交给对应 Owner 或主机管理员" : "请将 AID 和错误码交给本机 AUN 或主机管理员核查签名归属;不要猜测 Owner",
1485
+ status: "failed",
1486
+ code: details?.code ?? (error instanceof fs_1.AunFsError ? error.code : "OWNERSHIP_REPAIR_FAILED"),
1487
+ ...(details?.stage ? { stage: details.stage } : {}),
1488
+ ...(details?.operationId ? { operationId: details.operationId } : {}),
1489
+ ...(error instanceof fs_1.AunFsError && error.sdkCode !== undefined ? { sdkCode: error.sdkCode } : {}),
1490
+ message: safeError(error),
1491
+ });
1492
+ }
1493
+ }
1494
+ const count = (status) => results.filter(result => result.status === status).length;
1495
+ const summary = { scanned, repaired: count("repaired"), pending: count("pending"), skipped: count("skipped"), failed: count("failed"), cancelled: signal?.aborted ?? false, results };
1496
+ this.context?.logger.info(signal?.aborted ? "AUN 批量旧身份修复因 CLI 连接关闭而停止" : "AUN 批量旧身份修复完成", { scanned: summary.scanned, repaired: summary.repaired, pending: summary.pending, skipped: summary.skipped, failed: summary.failed, cancelled: summary.cancelled });
1497
+ return { message: `已检查 ${summary.scanned} 个已连接并关联桌面 Agent 的 AID:修复 ${summary.repaired},等待 Owner ${summary.pending},跳过 ${summary.skipped},失败 ${summary.failed}`, data: summary };
1498
+ }
1292
1499
  async defaultOwnerCommand(args) {
1293
1500
  const owner = typeof this.config.owner_aid === "string" ? this.config.owner_aid.trim() : "";
1294
1501
  if (!args.length)
@@ -1981,6 +2188,9 @@ exports.default = AunPlugin;
1981
2188
  function safeError(error) {
1982
2189
  return redact(error instanceof Error ? error.message : String(error));
1983
2190
  }
2191
+ function isLoopback(address) {
2192
+ return address === "127.0.0.1" || address === "::1" || address === "::ffff:127.0.0.1";
2193
+ }
1984
2194
  function fsOutput(result, management = false) {
1985
2195
  return JSON.parse(JSON.stringify(result, (key, value) => /previewBase64|private[_-]?key/i.test(key) || !management && /token|share_id/i.test(key) ? "[已隐藏]" : !management && typeof value === "string" && key !== "text" ? redact(value) : value));
1986
2196
  }
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OwnedProfileSync = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
8
+ function valid(value) {
9
+ if (!value || typeof value !== "object" || Array.isArray(value))
10
+ return false;
11
+ const entry = value;
12
+ return [entry.ownerAid, entry.aid].every(aid => typeof aid === "string" && aidPattern.test(aid))
13
+ && [entry.etag, entry.event_id].every(text => typeof text === "string" && text.length > 0 && text.length <= 512) && typeof entry.pending === "boolean";
14
+ }
15
+ class OwnedProfileSync {
16
+ file;
17
+ apply;
18
+ entries = new Map();
19
+ work = Promise.resolve();
20
+ closed = false;
21
+ constructor(file, apply) {
22
+ this.file = file;
23
+ this.apply = apply;
24
+ }
25
+ static async open(file, apply) {
26
+ const store = new OwnedProfileSync(file, apply);
27
+ let raw;
28
+ try {
29
+ raw = await (0, promises_1.readFile)(file, "utf8");
30
+ }
31
+ catch (error) {
32
+ if (error.code === "ENOENT")
33
+ return store;
34
+ throw error;
35
+ }
36
+ let snapshot;
37
+ try {
38
+ snapshot = JSON.parse(raw);
39
+ }
40
+ catch {
41
+ throw new Error("Owner 资料同步账本损坏");
42
+ }
43
+ if (!snapshot || snapshot.version !== 1 || !Array.isArray(snapshot.entries) || !snapshot.entries.every(valid))
44
+ throw new Error("Owner 资料同步账本格式无效");
45
+ for (const entry of snapshot.entries) {
46
+ const key = JSON.stringify([entry.ownerAid, entry.aid]);
47
+ if (store.entries.has(key))
48
+ throw new Error("Owner 资料同步账本存在重复身份");
49
+ store.entries.set(key, entry);
50
+ }
51
+ return store;
52
+ }
53
+ get pending() { return [...this.entries.values()].filter(entry => entry.pending).length; }
54
+ serial(run) {
55
+ if (this.closed)
56
+ return Promise.reject(new Error("Owner 资料同步已关闭"));
57
+ const next = this.work.catch(() => undefined).then(run);
58
+ this.work = next;
59
+ return next;
60
+ }
61
+ async persist(key, entry) {
62
+ const next = new Map(this.entries).set(key, entry);
63
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(this.file), { recursive: true, mode: 0o700 });
64
+ const temp = `${this.file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
65
+ try {
66
+ const handle = await (0, promises_1.open)(temp, "wx", 0o600);
67
+ try {
68
+ await handle.writeFile(`${JSON.stringify({ version: 1, entries: [...next.values()] })}\n`, "utf8");
69
+ await handle.sync();
70
+ }
71
+ finally {
72
+ await handle.close();
73
+ }
74
+ await (0, promises_1.rename)(temp, this.file);
75
+ this.entries = next;
76
+ }
77
+ finally {
78
+ await (0, promises_1.rm)(temp, { force: true });
79
+ }
80
+ }
81
+ handle(ownerAid, value) {
82
+ if (!value || typeof value !== "object" || !valid({ ...value, ownerAid, pending: true }))
83
+ return Promise.reject(new Error("Owner 名片变更事件格式无效"));
84
+ const event = value;
85
+ return this.serial(async () => {
86
+ const key = JSON.stringify([ownerAid, event.aid]), previous = this.entries.get(key);
87
+ if (previous?.event_id === event.event_id && !previous.pending)
88
+ return;
89
+ const entry = { aid: event.aid, etag: event.etag, event_id: event.event_id, ownerAid, pending: true };
90
+ this.entries.set(key, entry);
91
+ await this.persist(key, entry);
92
+ await this.apply(ownerAid, event);
93
+ await this.persist(key, { ...entry, pending: false });
94
+ });
95
+ }
96
+ retry() {
97
+ return this.serial(async () => {
98
+ for (const [key, entry] of this.entries) {
99
+ if (!entry.pending)
100
+ continue;
101
+ await this.persist(key, entry);
102
+ try {
103
+ await this.apply(entry.ownerAid, entry);
104
+ }
105
+ catch {
106
+ continue;
107
+ }
108
+ await this.persist(key, { ...entry, pending: false });
109
+ }
110
+ });
111
+ }
112
+ async close() { this.closed = true; await this.work.catch(() => undefined); }
113
+ }
114
+ exports.OwnedProfileSync = OwnedProfileSync;
@@ -124,7 +124,9 @@ async function readOwnerDirectory(storage, ownerAid) {
124
124
  throw new fs_1.AunFsError("DIRECTORY_INVALID", `Owner 名录校验失败:${error instanceof Error ? error.message : String(error)}`);
125
125
  }
126
126
  }
127
- async function addOwnerContact(storage, aid, ownerAid, now = Date.now()) {
127
+ async function addOwnerContact(storage, aid, ownerAid, now = Date.now(), profile) {
128
+ if (profile && (!Number.isSafeInteger(profile.signedAt) || profile.signedAt < 0 || !profile.etag || typeof profile.etag !== "string" || !profile.type || typeof profile.type !== "string" || profile.name !== undefined && typeof profile.name !== "string"))
129
+ throw new Error("名片资料格式无效,未更新 Owner 名录");
128
130
  for (let attempt = 0; attempt < 3; attempt++) {
129
131
  let version = 0;
130
132
  let directory = newDirectory(ownerAid, now);
@@ -152,12 +154,31 @@ async function addOwnerContact(storage, aid, ownerAid, now = Date.now()) {
152
154
  }
153
155
  validate(directory, ownerAid);
154
156
  }
155
- if (directory.entries.some((entry) => entry.aid === aid))
157
+ const existing = directory.entries.find((entry) => entry.aid === aid);
158
+ if (existing && !profile)
156
159
  return { status: "synced" };
157
160
  if (directory.groups.some((group) => group.group_aid === aid)
158
- || directory.blocked_aids.some((item) => item.target_id === aid))
161
+ || directory.blocked_aids.some((item) => item.target_id === aid)) {
162
+ if (profile)
163
+ return { status: "synced" };
159
164
  throw new Error("新 AID 已被 Owner 名录中的群组或屏蔽项占用");
160
- directory.entries.push({ aid, name: aid, remark: "", agent_md_signed_at: null, owner_aid: ownerAid, type: "agent", allow_pa_use: true });
165
+ }
166
+ const entry = existing ?? { aid, name: aid, remark: "", agent_md_signed_at: null, owner_aid: ownerAid, type: "agent", allow_pa_use: true };
167
+ if (profile) {
168
+ const updated = { ...entry, owner_aid: ownerAid, type: profile.type, agent_md_signed_at: profile.signedAt, agent_md_etag: profile.etag };
169
+ if ((!entry.name || entry.name === aid || entry.name === entry.agent_md_name) && profile.name)
170
+ updated.name = profile.name;
171
+ if (profile.name)
172
+ updated.agent_md_name = profile.name;
173
+ if (existing && canonical(updated) === canonical(existing))
174
+ return { status: "synced" };
175
+ if (existing)
176
+ Object.assign(existing, updated);
177
+ else
178
+ directory.entries.push(updated);
179
+ }
180
+ else
181
+ directory.entries.push(entry);
161
182
  directory.total = directory.entries.length;
162
183
  directory.group_total = directory.groups.length;
163
184
  directory.updated_at_ms = now;
@@ -181,7 +202,7 @@ async function addOwnerContact(storage, aid, ownerAid, now = Date.now()) {
181
202
  await storage.stat(exports.OWNER_CONTACTS_PATH, { owner: ownerAid, bucket: "default" });
182
203
  const latest = JSON.parse(Buffer.from(await storage.readBytes(exports.OWNER_CONTACTS_PATH, { owner: ownerAid, bucket: "default" })).toString("utf8"));
183
204
  validate(latest, ownerAid);
184
- if (latest.entries.some((entry) => entry.aid === aid))
205
+ if (latest.entries.some((entry) => entry.aid === aid && (!profile || entry.agent_md_etag === profile.etag && entry.agent_md_signed_at === profile.signedAt)))
185
206
  return { status: "synced" };
186
207
  }
187
208
  catch { /* 写入结果仍未知,向调用方报告原始错误。 */ }
@@ -39,5 +39,5 @@ async function readAunOwnership(store, client, aid) {
39
39
  }
40
40
  if (ownership.status === "consensus" && (!declared || ownership.owner_aid !== declared))
41
41
  throw new fs_1.AunFsError("OWNERSHIP_UNVERIFIED", "归属核验失败:Owner 共识与签名声明不一致");
42
- return { type: meta.type, declaredOwner: typeof declared === "string" ? declared : undefined, ownership };
42
+ return { type: meta.type, declaredOwner: typeof declared === "string" ? declared : undefined, ownership, agentMd: card };
43
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-bridge-aun",
3
- "version": "1.0.45-sdk0531.4",
3
+ "version": "1.0.46",
4
4
  "description": "mu-agent-bridge 的 AUN 通道插件。",
5
5
  "keywords": [
6
6
  "mu-agent-bridge-plugin",
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "aun",
3
- "version": "1.0.45-sdk0531.4",
3
+ "version": "1.0.46",
4
4
  "type": "channel",
5
5
  "displayName": "AUN 通道",
6
6
  "entry": "dist/src-ts/plugins/aun/index.js",