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 +13 -0
- package/config.schema.json +7 -0
- package/dist/src-ts/plugins/aun/index.js +221 -11
- package/dist/src-ts/plugins/aun/owned-profiles.js +114 -0
- package/dist/src-ts/plugins/aun/owner-directory.js +26 -5
- package/dist/src-ts/plugins/aun/ownership.js +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
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 名录同步。
|
package/config.schema.json
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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