agent-bridge-aun 1.0.50 → 1.0.58

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
@@ -72,3 +72,10 @@ AUN 独立包支持核心后台每五分钟检测更新;可信提供者在自
72
72
  ## Ability Owner 确认
73
73
 
74
74
  能力发布和远程调用审批由配置的 `ability_owners[providerAid]` 对应 Owner 在 AUN 加密私聊中管理。发送 `/ability help` 查看命令;`/ability list` 查看能力,`/ability publish <路径...>` 发布,`/ability revoke` 撤销,`/ability pending` 查看待审批清单。逐项处理使用通知中的 `/ability approve <操作键> <确认指纹>` 或 `/ability reject <操作键> <确认指纹>`。只有一项已送达申请时可直接回复“确认”或“拒绝”;多项需先送达固定清单,再用这两个词批量处理。新申请不会加入已展示清单,插件重载后需重新发送 `/ability pending`。Owner 首次发送有效管理命令时,AUN 验证其已配置 AID 并持久保存验证记录;远端消息不能设置或认领 Owner。能力的网页发布、Owner 验证和待审批区域已从 ABWeb 移除;已认证的本机管理 API 保留用于兼容旧管理客户端。
75
+
76
+
77
+ ### 按 Peer 持续授权
78
+
79
+ Owner 在完整调用申请通知下回复“允许12小时”,或使用 `/ability approve <操作键> <确认指纹> 12h`,可允许所示 Peer 对此 Provider 的此项能力连续调用12小时,覆盖该契约的所有合法参数。另可回复“永久允许”或 `/ability approve <操作键> <确认指纹> permanent`,授权同一范围直到撤销或配置改变。普通“确认”仍只批准本次。多项时先 `/ability pending` 核对固定清单。通知里的完整授权标签也可直接回复;缺少完整清单时会重新展示,核对后再回复,不会将审批标签交给桌面执行。
80
+
81
+ `/ability grants` 查看到期及撤销命令,`/ability revoke-grant <授权ID>` 随时撤销。重载不会续期,其他 Peer 或能力不能复用;取消发布、解绑或更换 Owner 会撤销。已过 TTL 的旧调用不会因授权恢复执行。详见本工程 `docs/aun-ability-call-flow.md`。
@@ -1,17 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isAbilitySelectionText = isAbilitySelectionText;
3
4
  exports.parseAbilityControl = parseAbilityControl;
4
5
  exports.parseAbilityControlMessage = parseAbilityControlMessage;
5
6
  const aidPattern = /^[a-z0-9][a-z0-9_-]{2,63}\.[a-z0-9-]+(?:\.[a-z0-9-]+)+$/;
6
7
  const pathPattern = /^(?!protocol\/|definitions\/)[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?(?:\/[a-z0-9]+(?:[a-z0-9._-]*[a-z0-9])?)+$/;
7
8
  const fingerprintPattern = /^[0-9a-f]{64}$/;
8
9
  const maxControlTextLength = 4096;
10
+ const selectionPattern = /^\d+(?:[,\s]+\d+)*$/;
11
+ function isAbilitySelectionText(text) {
12
+ return selectionPattern.test(text.trim());
13
+ }
9
14
  function parseAbilityControl(text) {
10
15
  if (text === "确认")
11
16
  return { action: "confirm-view" };
12
17
  if (text === "拒绝")
13
18
  return { action: "reject-view" };
19
+ if (text === "允许12小时" || text === "允许该 Peer 使用此能力12小时")
20
+ return { action: "grant-view", grantDurationMs: 43_200_000 };
21
+ if (text === "永久允许" || text === "永久允许该 Peer 使用此能力")
22
+ return { action: "grant-view", grantDurationMs: null };
14
23
  const trimmed = text.replace(/^ +| +$/g, "");
24
+ if (isAbilitySelectionText(trimmed)) {
25
+ const indexes = trimmed.replace(/,/g, " ").split(/\s+/).map(value => Number(value));
26
+ if (indexes.some(value => !Number.isSafeInteger(value) || value < 1))
27
+ throw new Error("Ability 清单编号格式无效:请使用从 1 开始的编号");
28
+ if (new Set(indexes).size !== indexes.length)
29
+ throw new Error("Ability 清单编号不能重复");
30
+ return { action: "select-view", indexes };
31
+ }
15
32
  if (!/^\/ability(?:\s|$)/.test(trimmed))
16
33
  return undefined;
17
34
  if (text.length > maxControlTextLength)
@@ -19,7 +36,7 @@ function parseAbilityControl(text) {
19
36
  if (!/^[a-z0-9./ _-]+$/.test(trimmed))
20
37
  throw new Error("Ability 管理命令格式无效:请发送单行完整命令,仅使用空格分隔参数");
21
38
  const [, action, ...args] = trimmed.split(/ +/);
22
- if (["help", "list", "revoke", "pending"].includes(action) && args.length === 0)
39
+ if (["help", "list", "revoke", "pending", "grants"].includes(action) && args.length === 0)
23
40
  return { action: action };
24
41
  if (action === "publish") {
25
42
  if (!args.length)
@@ -30,10 +47,15 @@ function parseAbilityControl(text) {
30
47
  throw new Error("Ability 发布路径不能重复");
31
48
  return { action, paths: args };
32
49
  }
33
- if ((action === "approve" || action === "reject") && args.length === 2) {
34
- if (!args.every(value => fingerprintPattern.test(value)))
50
+ if ((action === "approve" || action === "reject") && (args.length === 2 || args.length === 3)) {
51
+ if (!args.slice(0, 2).every(value => fingerprintPattern.test(value)))
35
52
  throw new Error("Ability 操作键与确认指纹格式无效:必须是完整的 64 位小写十六进制摘要");
36
- return { action, key: args[0], confirmationFingerprint: args[1] };
53
+ if (args.length === 3 && (action !== "approve" || !["12h", "permanent"].includes(args[2])))
54
+ throw new Error("持续授权只支持批准命令的 12h 或 permanent 选项");
55
+ return { action, key: args[0], confirmationFingerprint: args[1], ...(args.length === 3 ? { grantDurationMs: args[2] === "permanent" ? null : 43_200_000 } : {}) };
56
+ }
57
+ if (action === "revoke-grant" && args.length === 1 && /^[0-9a-f-]{16,128}$/.test(args[0])) {
58
+ return { action, grantId: args[0] };
37
59
  }
38
60
  throw new Error("Ability 管理命令或参数数量不正确,请发送 /ability help 查看用法");
39
61
  }
@@ -90,10 +90,22 @@ function validEntry(value) {
90
90
  return false;
91
91
  }
92
92
  }
93
+ function validGrant(value) {
94
+ if (!value || typeof value !== "object" || Array.isArray(value))
95
+ return false;
96
+ const grant = value;
97
+ return [grant.id, grant.providerAid, grant.authenticatedCaller, grant.path, grant.definitionId, grant.issuedByAid].every(value => typeof value === "string" && value.length > 0 && value.length <= 512)
98
+ && Array.isArray(grant.effects) && grant.effects.length > 0 && grant.effects.every(value => typeof value === "string")
99
+ && !!grant.executionTarget && typeof grant.executionTarget === "object" && !Array.isArray(grant.executionTarget) && grant.executionTarget.ownerAid === grant.issuedByAid
100
+ && Number.isSafeInteger(grant.createdAt) && grant.createdAt >= 0
101
+ && (grant.expiresAt === null || Number.isSafeInteger(grant.expiresAt) && grant.expiresAt - grant.createdAt === 43_200_000)
102
+ && (grant.revokedAt === undefined || Number.isSafeInteger(grant.revokedAt) && grant.revokedAt >= grant.createdAt);
103
+ }
93
104
  class AbilityOperationStore {
94
105
  file;
95
106
  now;
96
107
  entries = new Map();
108
+ grants = new Map();
97
109
  writes = Promise.resolve();
98
110
  constructor(file, now) {
99
111
  this.file = file;
@@ -117,7 +129,9 @@ class AbilityOperationStore {
117
129
  catch {
118
130
  throw failure("operation_store_corrupt", "操作账本损坏,已停止新的执行");
119
131
  }
120
- if (!snapshot || snapshot.version !== 1 || !Array.isArray(snapshot.entries) || !snapshot.entries.every(validEntry))
132
+ if (!snapshot || ![1, 2].includes(snapshot.version) || !Array.isArray(snapshot.entries) || !snapshot.entries.every(validEntry)
133
+ || (snapshot.version === 2 && !Array.isArray(snapshot.grants))
134
+ || (snapshot.grants !== undefined && (!Array.isArray(snapshot.grants) || !snapshot.grants.every(validGrant))))
121
135
  throw failure("operation_store_corrupt", "操作账本格式无效,已停止新的执行");
122
136
  let migrated = false;
123
137
  for (const saved of snapshot.entries) {
@@ -137,6 +151,12 @@ class AbilityOperationStore {
137
151
  store.entries.set(entry.key, entry);
138
152
  migrated ||= legacy;
139
153
  }
154
+ for (const saved of (snapshot.grants ?? [])) {
155
+ if (store.grants.has(saved.id))
156
+ throw failure("operation_store_corrupt", "授权账本含重复授权 ID");
157
+ store.grants.set(saved.id, saved);
158
+ }
159
+ migrated ||= snapshot.version === 1;
140
160
  if (migrated)
141
161
  await store.persist(new Map(store.entries));
142
162
  await store.interruptExecuting();
@@ -147,11 +167,11 @@ class AbilityOperationStore {
147
167
  this.writes = next;
148
168
  return next;
149
169
  }
150
- async persist(next) {
170
+ async persist(next, grants = this.grants) {
151
171
  const directory = (0, node_path_1.dirname)(this.file);
152
172
  await (0, promises_1.mkdir)(directory, { recursive: true, mode: 0o700 });
153
173
  const temp = `${this.file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
154
- const data = `${JSON.stringify({ version: 1, entries: [...next.values()] })}\n`;
174
+ const data = `${JSON.stringify({ version: 2, entries: [...next.values()], grants: [...grants.values()] })}\n`;
155
175
  try {
156
176
  const handle = await (0, promises_1.open)(temp, "wx", 0o600);
157
177
  try {
@@ -163,6 +183,7 @@ class AbilityOperationStore {
163
183
  }
164
184
  await (0, promises_1.rename)(temp, this.file);
165
185
  this.entries = next;
186
+ this.grants = grants;
166
187
  }
167
188
  finally {
168
189
  await (0, promises_1.rm)(temp, { force: true });
@@ -209,14 +230,16 @@ class AbilityOperationStore {
209
230
  return { created: true, entry: structuredClone(entry) };
210
231
  });
211
232
  }
212
- approve(key, fingerprint) {
213
- return this.confirm(key, fingerprint, true);
233
+ approve(key, fingerprint, options = {}) {
234
+ return this.confirm(key, fingerprint, true, options.grantDurationMs, options.grantId);
214
235
  }
215
236
  reject(key, fingerprint) {
216
237
  return this.confirm(key, fingerprint, false);
217
238
  }
218
- confirm(key, fingerprint, approve) {
239
+ confirm(key, fingerprint, approve, grantDurationMs, grantId) {
219
240
  return this.serial(async () => {
241
+ if (grantDurationMs !== undefined && (!approve || grantDurationMs !== null && grantDurationMs !== 43_200_000))
242
+ throw failure("grant_invalid", "持续授权只支持批准 12 小时或永久");
220
243
  const expired = this.expirePending(this.entries, this.now());
221
244
  if (expired.changed)
222
245
  await this.persist(expired.entries);
@@ -227,6 +250,8 @@ class AbilityOperationStore {
227
250
  throw failure("confirmation_mismatch", "确认内容与原请求不一致");
228
251
  if (existing.state !== "pending_confirmation")
229
252
  return { started: false, entry: structuredClone(existing) };
253
+ if (grantId && !this.grantActive(grantId, existing))
254
+ throw failure("grant_unavailable", "持续授权已到期、撤销或范围不匹配");
230
255
  const now = this.now();
231
256
  const confirmationExpired = now >= existing.confirmationExpiresAt;
232
257
  const messageExpired = now >= existing.expiresAt;
@@ -236,8 +261,56 @@ class AbilityOperationStore {
236
261
  ...(!started ? { result: confirmationExpired ? response(existing, "confirmation_expired", "Owner 确认申请已过期,不会执行") : approve && messageExpired ? response(existing, "message_expired", "调用已超过最晚启动时间") : response(existing, "user_denied", "Owner 已拒绝本次调用") } : {}) };
237
262
  const next = new Map(this.entries);
238
263
  next.set(key, entry);
239
- await this.persist(next);
240
- return { started, entry: structuredClone(entry) };
264
+ let grant = grantId ? this.grants.get(grantId) : undefined;
265
+ const ownerAid = typeof existing.executionTarget.ownerAid === "string" ? existing.executionTarget.ownerAid : "";
266
+ if (grantDurationMs !== undefined && !ownerAid)
267
+ throw failure("grant_invalid", "持续授权缺少明确 Owner");
268
+ if (grantDurationMs !== undefined && ownerAid && !existing.result) {
269
+ grant = { id: (0, node_crypto_1.randomUUID)(), providerAid: existing.providerAid, authenticatedCaller: existing.authenticatedCaller,
270
+ path: existing.path, definitionId: existing.definitionId, effects: [...existing.effects], executionTarget: structuredClone(existing.executionTarget),
271
+ issuedByAid: ownerAid, createdAt: now, expiresAt: grantDurationMs === null ? null : now + grantDurationMs };
272
+ const grants = new Map(this.grants);
273
+ grants.set(grant.id, grant);
274
+ await this.persist(next, grants);
275
+ }
276
+ else
277
+ await this.persist(next);
278
+ return { started, entry: structuredClone(entry), ...(grant ? { grant: structuredClone(grant) } : {}) };
279
+ });
280
+ }
281
+ grantActive(id, input) {
282
+ const grant = this.grants.get(id);
283
+ return Boolean(grant && grant.revokedAt === undefined && (grant.expiresAt === null || grant.expiresAt > this.now())
284
+ && grant.providerAid === input.providerAid && grant.authenticatedCaller === input.authenticatedCaller
285
+ && grant.path === input.path && grant.definitionId === input.definitionId
286
+ && grant.executionTarget.pluginId === input.executionTarget.pluginId
287
+ && grant.executionTarget.pluginVersion === input.executionTarget.pluginVersion
288
+ && grant.executionTarget.ownerAid === input.executionTarget.ownerAid);
289
+ }
290
+ activeGrant(input) {
291
+ return this.serial(async () => {
292
+ const grant = [...this.grants.values()].find(item => this.grantActive(item.id, input));
293
+ return grant ? structuredClone(grant) : undefined;
294
+ });
295
+ }
296
+ listGrants(providerAid) {
297
+ return this.serial(async () => [...this.grants.values()]
298
+ .filter(grant => providerAid === undefined || grant.providerAid === providerAid)
299
+ .map(grant => structuredClone(grant)));
300
+ }
301
+ revokeGrant(providerAid, grantId) {
302
+ return this.serial(async () => {
303
+ const grant = this.grants.get(grantId);
304
+ if (!grant || grant.providerAid !== providerAid)
305
+ throw failure("grant_not_found", "授权不存在");
306
+ if (grant.revokedAt === undefined) {
307
+ const next = new Map(this.grants);
308
+ const revoked = { ...grant, revokedAt: this.now() };
309
+ next.set(grantId, revoked);
310
+ await this.persist(this.entries, next);
311
+ return structuredClone(revoked);
312
+ }
313
+ return structuredClone(grant);
241
314
  });
242
315
  }
243
316
  finish(key, state, result) {
@@ -313,6 +386,13 @@ class AbilityOperationStore {
313
386
  const now = this.now();
314
387
  const expired = this.expirePending(this.entries, now);
315
388
  const next = expired.entries;
389
+ const grants = new Map(this.grants);
390
+ let grantsChanged = false;
391
+ for (const [id, grant] of grants)
392
+ if (grant.providerAid === providerAid && grant.revokedAt === undefined) {
393
+ grants.set(id, { ...grant, revokedAt: now });
394
+ grantsChanged = true;
395
+ }
316
396
  let invalidated = 0;
317
397
  for (const entry of next.values()) {
318
398
  if (entry.providerAid !== providerAid || entry.state !== "pending_confirmation")
@@ -322,8 +402,8 @@ class AbilityOperationStore {
322
402
  result: response(entry, code, message) });
323
403
  invalidated++;
324
404
  }
325
- if (invalidated || expired.changed)
326
- await this.persist(next);
405
+ if (invalidated || expired.changed || grantsChanged)
406
+ await this.persist(next, grants);
327
407
  return invalidated;
328
408
  });
329
409
  }
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseAgentMdCommand = parseAgentMdCommand;
4
+ /** 只识别整条名片命令;引号用于分隔参数,文本从不交给 shell。 */
5
+ function parseAgentMdCommand(text) {
6
+ const prefix = text.match(/^(?:ec[ \t]+aid|mab[ \t]+aun)[ \t]+agentmd(?:[ \t]+|$)/);
7
+ if (!prefix)
8
+ return undefined;
9
+ const input = text.slice(prefix[0].length);
10
+ const args = [];
11
+ let position = 0;
12
+ while (position < input.length) {
13
+ while (/\s/.test(input[position] ?? "") && position < input.length)
14
+ position++;
15
+ if (position === input.length)
16
+ break;
17
+ const quote = input[position];
18
+ if (quote === "'" || quote === '"') {
19
+ const start = position++;
20
+ if (quote === '"') {
21
+ while (position < input.length) {
22
+ if (input[position] === "\\") {
23
+ position += 2;
24
+ continue;
25
+ }
26
+ if (input[position++] === '"')
27
+ break;
28
+ }
29
+ try {
30
+ args.push(JSON.parse(input.slice(start, position)));
31
+ }
32
+ catch {
33
+ throw new Error("双引号参数必须是完整的 JSON 字符串;普通正文可使用单引号");
34
+ }
35
+ }
36
+ else {
37
+ const end = input.indexOf("'", position);
38
+ if (end < 0)
39
+ throw new Error("名片命令的单引号未闭合");
40
+ args.push(input.slice(position, end));
41
+ position = end + 1;
42
+ }
43
+ if (position < input.length && !/\s/.test(input[position]))
44
+ throw new Error("引号参数后需要空格或换行");
45
+ }
46
+ else {
47
+ const end = input.slice(position).search(/\s/);
48
+ const length = end < 0 ? input.length - position : end;
49
+ args.push(input.slice(position, position + length));
50
+ position += length;
51
+ }
52
+ }
53
+ let json = false;
54
+ // 格式选项只允许出现在尾部,正文中的选项字样保持原样。
55
+ if (args.at(-1) === "--json") {
56
+ json = true;
57
+ args.pop();
58
+ }
59
+ else if (args.at(-2) === "--format" && args.at(-1) === "json") {
60
+ json = true;
61
+ args.splice(-2);
62
+ }
63
+ const [verb = "", aid = "", ...values] = args;
64
+ if (!["get", "set", "put"].includes(verb) || !aid)
65
+ throw new Error("用法:ec aid agentmd get <当前AID> | set <当前AID> <字段> <值> | put <当前AID> <完整Markdown>");
66
+ return { verb, aid, values, json };
67
+ }
@@ -0,0 +1,222 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentMdMenu = exports.AGENTMD_PROTECTED_FIELDS = void 0;
4
+ const node_crypto_1 = require("node:crypto");
5
+ const node_util_1 = require("node:util");
6
+ const yaml_1 = require("yaml");
7
+ exports.AGENTMD_PROTECTED_FIELDS = ["aid", "type", "owner", "ownership", "ownership_proof", "ownership_release", "entity_cert_fingerprint"];
8
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
9
+ const idleTimeout = 30 * 24 * 60 * 60;
10
+ /** 仅实现 App 名片编辑器所需的 Menu 子集,不把控制消息交给模型。 */
11
+ class AgentMdMenu {
12
+ tokens = new Map();
13
+ queues = new Map();
14
+ clear() { this.tokens.clear(); }
15
+ /** 本机入口由宿主管理认证保护,与远端 Menu 共用写锁和发布校验。 */
16
+ async local(aid, verb, values, port) {
17
+ return this.command(aid, verb, values, port);
18
+ }
19
+ /** 手机文本入口保留原始认证信封,不伪造本机调用者或 Menu token。 */
20
+ async text(event, aid, verb, values, port, transportAid = aid) {
21
+ return this.command(aid, verb, values, port, event, transportAid);
22
+ }
23
+ async command(aid, verb, values, port, event, transportAid = aid) {
24
+ const previous = this.queues.get(aid) ?? Promise.resolve();
25
+ const pending = previous.catch(() => undefined).then(async () => {
26
+ const execute = (payload) => this.execute({ ...event, payload }, aid, port, event ? "text" : "local", transportAid);
27
+ if (verb === "get" && !values.length)
28
+ return execute({ type: "menu.query", name: "aid", args: { view: "agentmd" } });
29
+ // 先核验调用者,再读取用于字段更新的基线。
30
+ await execute({ type: "menu.query", name: "aid", args: { view: "agentmd" } });
31
+ const base = await port.read();
32
+ let content;
33
+ if (verb === "put" && values.length === 1)
34
+ content = values[0];
35
+ else if (verb === "set" && values.length === 2) {
36
+ const [field, value] = values;
37
+ if (!["name", "description", "version", "tags", "exttype", "body"].includes(field))
38
+ reject("INVALID_ARGUMENT", "仅支持 name/description/version/tags/exttype/body");
39
+ const parsed = port.parse(base.content);
40
+ const match = parsed.payload.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
41
+ if (!match)
42
+ reject("INVALID_ARGUMENT", "名片头部格式无效");
43
+ if (field === "body")
44
+ content = `${match[0].trimEnd()}\n${value}\n`;
45
+ else {
46
+ const document = (0, yaml_1.parseDocument)(match[1]);
47
+ let next = value;
48
+ if (field === "tags") {
49
+ try {
50
+ next = JSON.parse(value);
51
+ }
52
+ catch {
53
+ reject("INVALID_ARGUMENT", "tags 必须是 JSON 字符串数组");
54
+ }
55
+ if (!Array.isArray(next) || next.some(item => typeof item !== "string"))
56
+ reject("INVALID_ARGUMENT", "tags 必须是 JSON 字符串数组");
57
+ }
58
+ document.set(field, next);
59
+ content = `---\n${document.toString()}---\n${parsed.payload.slice(match[0].length)}`;
60
+ }
61
+ }
62
+ else
63
+ reject("INVALID_ARGUMENT", "用法:mab aun agentmd get <aid> | set <aid> <字段> <值> | put <aid> <完整Markdown>");
64
+ return execute({ type: "menu.action", name: "aid", action: "agentmd.publish", args: { content, expectedEtag: base.etag, expectedRelationVersion: base.ownership.relation_version } });
65
+ });
66
+ this.queues.set(aid, pending);
67
+ try {
68
+ return await pending;
69
+ }
70
+ finally {
71
+ if (this.queues.get(aid) === pending)
72
+ this.queues.delete(aid);
73
+ }
74
+ }
75
+ async handle(event, aid, port, authorized = false) {
76
+ if (!object(event) || !object(event.payload) || typeof event.payload.type !== "string" || !event.payload.type.startsWith("menu."))
77
+ return undefined;
78
+ const request = event.payload;
79
+ // 响应及事件也在此消费,避免其中的 text 被误当作桌面任务。
80
+ if (["menu.response", "menu.event"].includes(String(request.type)))
81
+ return undefined;
82
+ if (typeof request.id !== "string" || !request.id.trim() || request.id.length > 256)
83
+ return undefined;
84
+ const header = { type: "menu.response", id: request.id, ...(typeof request.name === "string" ? { name: request.name } : {}) };
85
+ const run = async () => {
86
+ try {
87
+ return { ...header, data: await this.execute(event, aid, port, authorized ? "text" : "menu") };
88
+ }
89
+ catch (error) {
90
+ const code = typeof error.code === "string" ? String(error.code) : "REQUEST_FAILED";
91
+ // SDK 异常可能带服务端或存储细节,不原样返回。
92
+ return { ...header, error: { code, message: error instanceof MenuError ? error.message : "名片操作未完成,请刷新后核对远端状态" } };
93
+ }
94
+ };
95
+ const previous = this.queues.get(aid) ?? Promise.resolve();
96
+ const pending = previous.catch(() => undefined).then(run);
97
+ this.queues.set(aid, pending);
98
+ try {
99
+ return await pending;
100
+ }
101
+ finally {
102
+ if (this.queues.get(aid) === pending)
103
+ this.queues.delete(aid);
104
+ }
105
+ }
106
+ async execute(event, aid, port, mode = "menu", transportAid = aid) {
107
+ const local = mode === "local";
108
+ const request = event.payload;
109
+ const envelope = object(event.envelope) ? event.envelope : {};
110
+ const peer = local ? (await port.read()).ownership.owner_aid : typeof event.from === "string" ? event.from : envelope.from;
111
+ if (!local && (typeof peer !== "string" || !peer || peer === aid || event.encrypted !== true
112
+ || (envelope.from !== undefined && envelope.from !== peer) || (envelope.to !== undefined && envelope.to !== transportAid)
113
+ || (event.to !== undefined && event.to !== transportAid) || event.group_id || envelope.group_id
114
+ || event.chat_type === "group" || envelope.chat_type === "group"))
115
+ reject("FORBIDDEN", "名片管理仅支持经过认证的加密私聊");
116
+ const device = typeof event.sender_device_id === "string" ? event.sender_device_id : "";
117
+ const slot = typeof event.sender_slot_id === "string" ? event.sender_slot_id : "";
118
+ const key = JSON.stringify([aid, peer, device, slot]);
119
+ const read = async () => {
120
+ if (!port.current())
121
+ reject("FORBIDDEN", "通道或关联已改变,请重新打开编辑器");
122
+ const card = await port.read();
123
+ if (!port.current())
124
+ reject("FORBIDDEN", "通道或关联已改变,请重新打开编辑器");
125
+ const owner = card.ownership;
126
+ if (owner.status !== "consensus" || owner.sub_aid !== aid || owner.owner_aid !== peer || owner.cached !== false || owner.lock !== "locked")
127
+ reject("FORBIDDEN", "仅当前协议 Owner 可编辑名片");
128
+ if (!Number.isSafeInteger(owner.relation_version) || Number(owner.relation_version) < 1 || !card.etag || owner.agent_md_etag !== card.etag)
129
+ reject("OWNERSHIP_CONFLICT", "名片与归属版本不一致,请刷新");
130
+ const parsed = port.parse(card.content);
131
+ if (["human", "group", "org", "host"].includes(String(parsed.frontmatter.type).trim().toLowerCase()))
132
+ reject("NOT_SUPPORTED", "此入口仅编辑普通 Agent 名片");
133
+ if (parsed.frontmatter.aid !== aid || parsed.frontmatter.owner !== peer)
134
+ reject("OWNERSHIP_CONFLICT", "名片身份与归属不一致");
135
+ return { ...card, ...parsed };
136
+ };
137
+ const base = await read();
138
+ const now = Date.now();
139
+ for (const [tokenKey, record] of this.tokens)
140
+ if (now - record.usedAt >= idleTimeout * 1000)
141
+ this.tokens.delete(tokenKey);
142
+ if (request.type === "menu.token.request") {
143
+ if (!this.tokens.has(key) && this.tokens.size >= 1000)
144
+ reject("MENU_TOKEN_REJECTED", "Menu 授权数量已达上限");
145
+ const token = `mt_${(0, node_crypto_1.randomBytes)(32).toString("base64url")}`;
146
+ this.tokens.set(key, { token, usedAt: now });
147
+ return { menu_token: token, idle_timeout: idleTimeout };
148
+ }
149
+ const token = this.tokens.get(key);
150
+ if (mode === "menu" && (!token || token.token !== request.menu_token))
151
+ reject("MENU_TOKEN_REQUIRED", "Menu token 已失效,请重新申请");
152
+ if (token)
153
+ token.usedAt = now;
154
+ const args = object(request.args) ? request.args : {};
155
+ if (request.type === "menu.list" && request.name === "menu" && args.view === "catalog") {
156
+ if (!object(args.context) || args.context.kind !== "agent" || Object.keys(args.context).some(key => key !== "kind"))
157
+ reject("INVALID_CONTEXT", "名片目录需要 Agent 上下文");
158
+ if (Object.keys(args).some(key => !["view", "context", "acceptSchemaVersions"].includes(key)))
159
+ reject("INVALID_ARGUMENT", "目录参数不受支持");
160
+ if (args.acceptSchemaVersions !== undefined && !(Array.isArray(args.acceptSchemaVersions) ? args.acceptSchemaVersions : [args.acceptSchemaVersions]).includes(1))
161
+ reject("SCHEMA_VERSION_UNSUPPORTED", "仅支持目录版本 1");
162
+ return { kind: "menu.catalog", $schema_version: 1, context: { kind: "agent", self: aid, actor: peer, requesterRole: "owner" }, entries: [{
163
+ id: "aid.agentmd", name: "aid", type: "setting", label: "能力介绍与名片", section: "agent", order: 71,
164
+ control: "markdown-editor", operations: {
165
+ query: { supported: true, allowed: true, cmd: "/aid", args: { view: "agentmd", protocolVersion: 1 } },
166
+ action: { supported: true, allowed: true, cmd: "/aid", action: "agentmd.publish", args: { protocolVersion: 1 } },
167
+ },
168
+ }] };
169
+ }
170
+ if (request.name !== "aid" && request.cmd !== "/aid")
171
+ reject("NOT_SUPPORTED", "当前仅支持名片编辑 Menu");
172
+ const publish = request.type === "menu.action" && request.action === "agentmd.publish";
173
+ const query = request.type === "menu.query" && args.view === "agentmd";
174
+ if (!publish && !query)
175
+ reject("NOT_SUPPORTED", "名片操作不受支持");
176
+ const allowed = query ? ["view", "protocolVersion", "aid", "self", "scope"] : ["content", "expectedEtag", "expectedRelationVersion", "protocolVersion", "aid", "self", "scope"];
177
+ if (Object.keys(args).some(key => !allowed.includes(key)) || (args.aid !== undefined && args.aid !== aid) || (args.self !== undefined && args.self !== aid) || (args.scope !== undefined && args.scope !== "agent"))
178
+ reject("INVALID_ARGUMENT", "名片参数或目标不受支持");
179
+ if (args.protocolVersion !== undefined && args.protocolVersion !== 1)
180
+ reject("SCHEMA_VERSION_UNSUPPORTED", "仅支持名片协议版本 1");
181
+ const data = (card) => ({
182
+ protocol: "evol.agentmd", protocolVersion: 1, aid, content: card.payload, etag: card.etag,
183
+ relationVersion: card.ownership.relation_version, verification: "verified", atomicVersionCheck: false,
184
+ });
185
+ if (query)
186
+ return { ...data(base), protectedFields: exports.AGENTMD_PROTECTED_FIELDS, limits: { maxPayloadBytes: 4096, maxDescriptionCharacters: 100 } };
187
+ if (typeof args.content !== "string" || Buffer.byteLength(args.content) > 6144 || typeof args.expectedEtag !== "string" || !args.expectedEtag.trim() || args.expectedEtag.length > 512 || /[\r\n]/.test(args.expectedEtag) || !Number.isSafeInteger(args.expectedRelationVersion) || Number(args.expectedRelationVersion) < 1)
188
+ reject("INVALID_ARGUMENT", "发布需要正文、ETag 和归属版本");
189
+ if (base.etag !== args.expectedEtag)
190
+ reject("REMOTE_VERSION_CONFLICT", "名片已改变,请重新读取后发布");
191
+ if (base.ownership.relation_version !== args.expectedRelationVersion)
192
+ reject("OWNERSHIP_CONFLICT", "归属已改变,请重新读取");
193
+ const candidate = port.parse(args.content);
194
+ for (const field of exports.AGENTMD_PROTECTED_FIELDS)
195
+ if (Object.hasOwn(base.frontmatter, field) !== Object.hasOwn(candidate.frontmatter, field) || !(0, node_util_1.isDeepStrictEqual)(base.frontmatter[field], candidate.frontmatter[field]))
196
+ reject("PROTECTED_FIELD", `不能修改身份字段:${field}`);
197
+ // SDK 不暴露条件上传,串行写入并在上传前重读,明确返回非原子版本检查。
198
+ const latest = await read();
199
+ if (latest.etag !== base.etag)
200
+ reject("REMOTE_VERSION_CONFLICT", "名片已改变,请重新读取");
201
+ if (latest.ownership.relation_version !== base.ownership.relation_version)
202
+ reject("OWNERSHIP_CONFLICT", "归属已改变,请重新读取");
203
+ const guard = () => { if (!port.current())
204
+ reject("FORBIDDEN", "通道或关联已失效"); };
205
+ guard();
206
+ await port.upload(candidate.payload, guard);
207
+ const accepted = await read();
208
+ const body = (payload) => payload.replace(/\r\n/g, "\n").replace(/^---\n[\s\S]*?\n---(?:\n|$)/, "").trim();
209
+ if (accepted.ownership.relation_version !== base.ownership.relation_version || !(0, node_util_1.isDeepStrictEqual)(accepted.frontmatter, candidate.frontmatter) || body(accepted.payload) !== body(candidate.payload))
210
+ reject("PUBLISH_NOT_CONFIRMED", "远端尚未确认本次发布,请刷新核对");
211
+ return { ...data(accepted), published: true, versionChecked: true, nameChanged: base.frontmatter.name !== accepted.frontmatter.name };
212
+ }
213
+ }
214
+ exports.AgentMdMenu = AgentMdMenu;
215
+ class MenuError extends Error {
216
+ code;
217
+ constructor(code, message) {
218
+ super(message);
219
+ this.code = code;
220
+ }
221
+ }
222
+ function reject(code, message) { throw new MenuError(code, message); }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseNaturalProfileCommand = parseNaturalProfileCommand;
4
+ /** 确定性的中文名片指令;不从模糊措辞推断新内容或授权。 */
5
+ function parseNaturalProfileCommand(text) {
6
+ const target = "([a-z0-9][a-z0-9_-]*(?:\\.[a-z0-9-]+)*)";
7
+ const change = new RegExp(`^(?:请)?(?:帮我)?(?:把|将)\\s*${target}\\s*的(?:名片)?(描述|简介|能力介绍|介绍正文|显示名称|名称)\\s*(?:修改为|设置为|改成|改为|设为)\\s*[::]?\\s*([\\s\\S]+)$`, "i").exec(text.trim());
8
+ if (change) {
9
+ let value = change[3].trim();
10
+ if ((value.startsWith("“") && value.endsWith("”")) || (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
11
+ value = value.slice(1, -1);
12
+ const field = ["描述", "简介"].includes(change[2]) ? "description" : ["显示名称", "名称"].includes(change[2]) ? "name" : "body";
13
+ return { target: change[1].toLowerCase(), verb: "set", values: [field, value] };
14
+ }
15
+ const read = new RegExp(`^(?:请)?(?:帮我)?(?:查看|看一下|读取)\\s*${target}\\s*的(?:名片|描述|简介|能力介绍)[。!!]?\\s*$`, "i").exec(text.trim());
16
+ return read ? { target: read[1].toLowerCase(), verb: "get", values: [] } : undefined;
17
+ }
@@ -134,7 +134,8 @@ async function readAunFsBytes(storage, path, options) {
134
134
  throw new AunFsError("OUTPUT_LIMIT", "FS 文件超过读取预算");
135
135
  if (expectedSize !== undefined && expectedSize !== size)
136
136
  throw new AunFsError("INVALID_RESPONSE", "FS 文件大小已变化");
137
- const limits = storage.lowlevel?.getLimits ? record(await storage.lowlevel.getLimits(readOptions)) : {};
137
+ // 分块上限查询当前连接,不能要求读取者拥有目标 AID 的配额管理权限。
138
+ const limits = storage.lowlevel?.getLimits ? record(await storage.lowlevel.getLimits()) : {};
138
139
  const inline = limits.max_inline_bytes ?? 64 * 1024;
139
140
  if (!Number.isSafeInteger(inline) || Number(inline) < 1)
140
141
  unsupported("Storage 内联读取上限无效");