@agentunion/fastaun 0.5.11 → 0.5.12

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/dist/client.d.ts CHANGED
@@ -118,6 +118,9 @@ export declare class AUNClient {
118
118
  private _seqTrackerContext;
119
119
  /** 惰性群同步:已同步过的 group_id 集合 */
120
120
  private _groupSynced;
121
+ /** 群定向邀请按 group_id + invitee_aid 聚合去重。 */
122
+ private _groupInviteAggregates;
123
+ private _groupInviteAggregatesLoaded;
121
124
  /** P2P 撤回去重:原始 message_id -> 时间戳,保证应用层只回调一次 */
122
125
  _messageRecallSeen: Map<string, number>;
123
126
  /** 群撤回去重:group_id|sorted(message_ids) -> 时间戳,保证应用层只回调一次 */
@@ -272,6 +275,7 @@ export declare class AUNClient {
272
275
  private _checkIdentityAdmissionAfterConnect;
273
276
  private _runGroupIdentityOperation;
274
277
  createGroup(params?: RpcParams, options?: CreateGroupOptions): Promise<RpcResult>;
278
+ private _inviteInitialGroupMembers;
275
279
  private _groupAgentMdContent;
276
280
  private _uploadGroupAgentMd;
277
281
  bindGroupAid(params?: RpcParams, options?: BindGroupAidOptions): Promise<RpcResult>;
@@ -367,6 +371,19 @@ export declare class AUNClient {
367
371
  /** 将 SeqTracker 状态保存到 keystore */
368
372
  private _saveSeqTrackerState;
369
373
  private _persistSeq;
374
+ private static readonly GROUP_INVITE_METADATA_KEY;
375
+ private _groupInviteArray;
376
+ private _groupInvitePayload;
377
+ private _groupInviteKey;
378
+ private _loadGroupInviteAggregates;
379
+ private _persistGroupInviteAggregates;
380
+ private _onRawGroupInviteCreated;
381
+ private _onRawGroupInviteFinalized;
382
+ private _handleGroupInviteAggregate;
383
+ /** 连接建立/恢复后从事实列表补发尚未提示的群级邀请。 */
384
+ private _recoverGroupInvites;
385
+ /** 接受/拒绝/撤回成功后清理本地待处理聚合项。 */
386
+ private clearGroupInviteAggregate;
370
387
  private _commitSeqTrackerState;
371
388
  private _persistRepairedSeq;
372
389
  private _clampAckSeq;
package/dist/client.js CHANGED
@@ -124,7 +124,7 @@ const MAX_NOTIFY_PAYLOAD_SIZE = 64 * 1024;
124
124
  // P1-23: 非幂等方法使用更长超时(35s),避免 SDK 10s 超时 < gateway 30s 处理时间
125
125
  const NON_IDEMPOTENT_TIMEOUT_MS = 35_000;
126
126
  const NON_IDEMPOTENT_METHODS = new Set([
127
- 'message.send', 'mail.send', 'group.send', 'group.create', 'group.invite',
127
+ 'message.send', 'mail.send', 'group.send', 'group.create',
128
128
  'group.kick', 'group.remove_member', 'group.leave', 'group.dissolve',
129
129
  'group.set_settings',
130
130
  'group.update_announcement', 'group.update_rules',
@@ -150,6 +150,7 @@ const NON_IDEMPOTENT_METHODS = new Set([
150
150
  'auth.create_aid', 'auth.renew_cert', 'auth.rekey',
151
151
  'message.thought.put', 'group.thought.put',
152
152
  'group.add_member', 'group.bind_group_aid', 'group.complete_transfer',
153
+ 'group.accept_invite', 'group.reject_invite', 'group.revoke_invite',
153
154
  'collab.create', 'collab.commit', 'collab.clone',
154
155
  'collab.prune', 'collab.unregister',
155
156
  'collab.tag.create', 'collab.tag.restore',
@@ -174,6 +175,8 @@ const SIGNED_METHODS = new Set([
174
175
  'group.review_join_request',
175
176
  'group.batch_review_join_request',
176
177
  'group.request_join', 'group.use_invite_code',
178
+ 'group.invite_member', 'group.list_my_invites', 'group.accept_invite',
179
+ 'group.reject_invite', 'group.revoke_invite',
177
180
  'group.thought.put',
178
181
  'message.thought.put',
179
182
  'group.set_settings',
@@ -468,6 +471,9 @@ export class AUNClient {
468
471
  _seqTrackerContext = null;
469
472
  /** 惰性群同步:已同步过的 group_id 集合 */
470
473
  _groupSynced = new Set();
474
+ /** 群定向邀请按 group_id + invitee_aid 聚合去重。 */
475
+ _groupInviteAggregates = new Map();
476
+ _groupInviteAggregatesLoaded = false;
471
477
  /** P2P 撤回去重:原始 message_id -> 时间戳,保证应用层只回调一次 */
472
478
  _messageRecallSeen = new Map();
473
479
  /** 群撤回去重:group_id|sorted(message_ids) -> 时间戳,保证应用层只回调一次 */
@@ -680,6 +686,9 @@ export class AUNClient {
680
686
  this._dispatcher.subscribe('_raw.group.message_recalled', (data) => this._safeAsync(this._onRawGroupMessageRecalled(data)));
681
687
  // 群组变更事件:透传并触发 V2 state/SPK 维护
682
688
  this._dispatcher.subscribe('_raw.group.changed', (data) => this._onRawGroupChanged(data));
689
+ // 定向邀请底层事件:按群 + 目标 AID 聚合后向应用层只提示一次。
690
+ this._dispatcher.subscribe('_raw.group.invite_created', (data) => this._onRawGroupInviteCreated(data));
691
+ this._dispatcher.subscribe('_raw.group.invite_finalized', (data) => this._onRawGroupInviteFinalized(data));
683
692
  // V2 state proposal 服务平面事件:owner/admin 负责确认或重新提案
684
693
  this._dispatcher.subscribe('_raw.group.v2.state_proposed', (data) => this._safeAsync(this._onV2StateProposed(data)));
685
694
  this._dispatcher.subscribe('_raw.group.v2.state_retry_needed', (data) => this._safeAsync(this._onV2StateRetryNeeded(data)));
@@ -1153,6 +1162,8 @@ export class AUNClient {
1153
1162
  }
1154
1163
  this._aidStore = store;
1155
1164
  const payload = { ...params };
1165
+ const initialMembers = payload.members;
1166
+ delete payload.members;
1156
1167
  if (payload.group_name === undefined && payload.groupName !== undefined) {
1157
1168
  payload.group_name = payload.groupName;
1158
1169
  delete payload.groupName;
@@ -1180,7 +1191,8 @@ export class AUNClient {
1180
1191
  const result = { ...createdMap, ...boundMap, group: { ...createdGroup, ...boundGroup } };
1181
1192
  const postprocessParams = { ...payload };
1182
1193
  delete postprocessParams._defer_group_ready_postprocess;
1183
- return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1194
+ const processed = await this._groupState.postprocessResult('group.create', postprocessParams, result);
1195
+ return await this._inviteInitialGroupMembers(processed, groupId, initialMembers);
1184
1196
  }
1185
1197
  const pendingKey = `create:${String(payload.group_name).trim().toLowerCase()}`;
1186
1198
  const keystore = store._keystore;
@@ -1223,7 +1235,32 @@ export class AUNClient {
1223
1235
  }
1224
1236
  const postprocessParams = { ...payload };
1225
1237
  delete postprocessParams._defer_group_ready_postprocess;
1226
- return await this._groupState.postprocessResult('group.create', postprocessParams, result);
1238
+ const processed = await this._groupState.postprocessResult('group.create', postprocessParams, result);
1239
+ return await this._inviteInitialGroupMembers(processed, String(group.group_id ?? groupAid).trim(), initialMembers);
1240
+ }
1241
+ async _inviteInitialGroupMembers(result, groupId, members) {
1242
+ const invites = [];
1243
+ const inviteErrors = [];
1244
+ const seen = new Set();
1245
+ if (Array.isArray(members)) {
1246
+ for (const item of members) {
1247
+ const aid = typeof item === 'string' ? item.trim() : (isJsonObject(item) ? String(item.aid ?? '').trim() : '');
1248
+ const memberType = isJsonObject(item) ? String(item.member_type ?? '').trim() : '';
1249
+ if (!aid || aid === String(this._aid ?? '').trim() || seen.has(aid))
1250
+ continue;
1251
+ seen.add(aid);
1252
+ const params = { group_id: groupId, invitee_aid: aid };
1253
+ if (memberType)
1254
+ params.member_type = memberType;
1255
+ try {
1256
+ invites.push(await this.group.inviteMember(params));
1257
+ }
1258
+ catch (err) {
1259
+ inviteErrors.push({ aid, error: err instanceof Error ? err.message : String(err) });
1260
+ }
1261
+ }
1262
+ }
1263
+ return { ...(isJsonObject(result) ? result : {}), invites, invite_errors: inviteErrors };
1227
1264
  }
1228
1265
  _groupAgentMdContent(groupAid, params, group) {
1229
1266
  const explicit = params.group_agent_md ?? params.groupAgentMd ?? params.content;
@@ -2381,6 +2418,225 @@ export class AUNClient {
2381
2418
  _persistSeq(ns, forceSeq) {
2382
2419
  return this._delivery.persistSeq(ns, forceSeq);
2383
2420
  }
2421
+ static GROUP_INVITE_METADATA_KEY = 'group_invites_pending_v1';
2422
+ _groupInviteArray(value) {
2423
+ if (Array.isArray(value))
2424
+ return value.map((item) => String(item ?? '').trim()).filter(Boolean);
2425
+ const item = String(value ?? '').trim();
2426
+ return item ? [item] : [];
2427
+ }
2428
+ _groupInvitePayload(data) {
2429
+ if (!isJsonObject(data))
2430
+ return null;
2431
+ const source = data;
2432
+ const groupId = normalizeGroupId(source.group_id ?? source.group_aid ?? source.groupId ?? source.groupAid);
2433
+ const inviteeAid = String(source.invitee_aid ?? source.inviteeAid ?? this._aid ?? '').trim();
2434
+ if (!groupId || !inviteeAid)
2435
+ return null;
2436
+ if (this._aid && inviteeAid.toLowerCase() !== this._aid.toLowerCase())
2437
+ return null;
2438
+ const inviteIds = this._groupInviteArray(source.invite_ids ?? source.invite_id ?? source.inviteIds);
2439
+ const inviterAids = this._groupInviteArray(source.inviter_aids ?? source.inviter_aid ?? source.inviterAids);
2440
+ return {
2441
+ ...source,
2442
+ group_id: groupId,
2443
+ group_aid: String(source.group_aid ?? groupId).trim() || groupId,
2444
+ invite_ids: inviteIds,
2445
+ inviter_aids: inviterAids,
2446
+ invitee_aid: inviteeAid,
2447
+ role: String(source.role ?? 'member'),
2448
+ member_type: String(source.member_type ?? 'human'),
2449
+ status: String(source.status ?? 'pending').toLowerCase() || 'pending',
2450
+ };
2451
+ }
2452
+ _groupInviteKey(payload) {
2453
+ return `${String(payload.group_id ?? '').trim().toLowerCase()}\u0000${String(payload.invitee_aid ?? '').trim().toLowerCase()}`;
2454
+ }
2455
+ _loadGroupInviteAggregates() {
2456
+ if (this._groupInviteAggregatesLoaded)
2457
+ return;
2458
+ this._groupInviteAggregatesLoaded = true;
2459
+ const aid = String(this._aid ?? '').trim();
2460
+ const store = this._tokenStore;
2461
+ if (!aid || !store || typeof store.loadMetadata !== 'function')
2462
+ return;
2463
+ try {
2464
+ const raw = store.loadMetadata(aid)?.[AUNClient.GROUP_INVITE_METADATA_KEY];
2465
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
2466
+ if (!isJsonObject(parsed))
2467
+ return;
2468
+ for (const [key, value] of Object.entries(parsed)) {
2469
+ if (isJsonObject(value) && String(value.status ?? 'pending').toLowerCase() === 'pending') {
2470
+ this._groupInviteAggregates.set(key, value);
2471
+ }
2472
+ }
2473
+ }
2474
+ catch (exc) {
2475
+ this._clientLog.debug(`group invite metadata restore failed: ${formatCaughtError(exc)}`);
2476
+ }
2477
+ }
2478
+ _persistGroupInviteAggregates() {
2479
+ const aid = String(this._aid ?? '').trim();
2480
+ const store = this._tokenStore;
2481
+ if (!aid || !store || typeof store.saveMetadata !== 'function')
2482
+ return;
2483
+ try {
2484
+ const values = {};
2485
+ for (const [key, value] of this._groupInviteAggregates)
2486
+ values[key] = value;
2487
+ store.saveMetadata(aid, { [AUNClient.GROUP_INVITE_METADATA_KEY]: JSON.stringify(values) });
2488
+ }
2489
+ catch (exc) {
2490
+ this._clientLog.debug(`group invite metadata persist failed: ${formatCaughtError(exc)}`);
2491
+ }
2492
+ }
2493
+ async _onRawGroupInviteCreated(data) {
2494
+ await this._handleGroupInviteAggregate(data);
2495
+ }
2496
+ async _onRawGroupInviteFinalized(data) {
2497
+ const payload = this._groupInvitePayload(data);
2498
+ if (!payload)
2499
+ return;
2500
+ this._loadGroupInviteAggregates();
2501
+ const key = this._groupInviteKey(payload);
2502
+ const existing = this._groupInviteAggregates.get(key);
2503
+ if (!existing)
2504
+ return;
2505
+ const source = data;
2506
+ const status = String(payload.status ?? '').toLowerCase();
2507
+ const aggregateStatus = String(source.aggregate_status ?? source.aggregateStatus ?? '').toLowerCase();
2508
+ const remainingPending = source.remaining_pending === true
2509
+ || source.remaining_pending === 1
2510
+ || String(source.remaining_pending ?? '').toLowerCase() === 'true';
2511
+ const pending = source.pending === true
2512
+ || source.pending === 1
2513
+ || String(source.pending ?? '').toLowerCase() === 'true';
2514
+ const pendingIds = this._groupInviteArray(source.pending_invite_ids ?? source.pendingInviteIds);
2515
+ const pendingInviters = this._groupInviteArray(source.pending_inviter_aids ?? source.pendingInviterAids);
2516
+ const currentIds = this._groupInviteArray(existing.invite_ids);
2517
+ const inviteId = String(source.invite_id ?? '').trim();
2518
+ const inferredPartialRevoke = status === 'revoked' && inviteId
2519
+ && currentIds.length > 1 && currentIds.includes(inviteId);
2520
+ const leavesPending = remainingPending || pending || status === 'pending'
2521
+ || aggregateStatus === 'pending' || pendingIds.length > 0 || inferredPartialRevoke;
2522
+ if (!leavesPending) {
2523
+ this._groupInviteAggregates.delete(key);
2524
+ }
2525
+ else {
2526
+ const finalizedIds = new Set(remainingPending && inviteId
2527
+ ? [inviteId]
2528
+ : this._groupInviteArray(payload.invite_ids));
2529
+ const nextIds = pendingIds.length > 0
2530
+ ? pendingIds
2531
+ : this._groupInviteArray(existing.invite_ids).filter((id) => !finalizedIds.has(id));
2532
+ if (nextIds.length === 0) {
2533
+ this._groupInviteAggregates.delete(key);
2534
+ }
2535
+ else {
2536
+ existing.invite_ids = nextIds;
2537
+ existing.inviter_aids = pendingInviters.length > 0
2538
+ ? pendingInviters
2539
+ : this._groupInviteArray(existing.inviter_aids);
2540
+ }
2541
+ }
2542
+ this._persistGroupInviteAggregates();
2543
+ }
2544
+ async _handleGroupInviteAggregate(data) {
2545
+ const payload = this._groupInvitePayload(data);
2546
+ if (!payload)
2547
+ return;
2548
+ this._loadGroupInviteAggregates();
2549
+ const status = String(payload.status ?? 'pending').toLowerCase();
2550
+ const key = this._groupInviteKey(payload);
2551
+ if (status !== 'pending') {
2552
+ if (this._groupInviteAggregates.delete(key))
2553
+ this._persistGroupInviteAggregates();
2554
+ return;
2555
+ }
2556
+ const existing = this._groupInviteAggregates.get(key);
2557
+ if (existing) {
2558
+ existing.invite_ids = [...new Set([
2559
+ ...this._groupInviteArray(existing.invite_ids),
2560
+ ...this._groupInviteArray(payload.invite_ids),
2561
+ ])];
2562
+ existing.inviter_aids = [...new Set([
2563
+ ...this._groupInviteArray(existing.inviter_aids),
2564
+ ...this._groupInviteArray(payload.inviter_aids),
2565
+ ])];
2566
+ this._persistGroupInviteAggregates();
2567
+ return;
2568
+ }
2569
+ const snapshot = {
2570
+ ...payload,
2571
+ invite_ids: this._groupInviteArray(payload.invite_ids),
2572
+ inviter_aids: this._groupInviteArray(payload.inviter_aids),
2573
+ };
2574
+ this._groupInviteAggregates.set(key, snapshot);
2575
+ this._persistGroupInviteAggregates();
2576
+ this._dispatcher.enqueue('group.invite_received', { ...snapshot, invite_ids: [...this._groupInviteArray(snapshot.invite_ids)], inviter_aids: [...this._groupInviteArray(snapshot.inviter_aids)] });
2577
+ }
2578
+ /** 连接建立/恢复后从事实列表补发尚未提示的群级邀请。 */
2579
+ async _recoverGroupInvites(result) {
2580
+ let response = result;
2581
+ let cursor = '';
2582
+ const seen = new Set();
2583
+ do {
2584
+ if (response === undefined) {
2585
+ response = await this.call('group.list_my_invites', cursor ? { cursor } : {});
2586
+ }
2587
+ let items = [];
2588
+ let nextCursor = '';
2589
+ if (Array.isArray(response))
2590
+ items = response;
2591
+ else if (isJsonObject(response)) {
2592
+ const responseObject = response;
2593
+ if (Array.isArray(responseObject.items))
2594
+ items = responseObject.items;
2595
+ else if (Array.isArray(responseObject.invites))
2596
+ items = responseObject.invites;
2597
+ else if (isJsonObject(responseObject.data) && Array.isArray(responseObject.data.items))
2598
+ items = responseObject.data.items;
2599
+ nextCursor = String(responseObject.next_cursor ?? responseObject.nextCursor ?? responseObject.cursor ?? '');
2600
+ }
2601
+ for (const item of items) {
2602
+ const payload = this._groupInvitePayload(item);
2603
+ if (payload)
2604
+ seen.add(this._groupInviteKey(payload));
2605
+ await this._handleGroupInviteAggregate(item);
2606
+ }
2607
+ cursor = nextCursor;
2608
+ response = undefined;
2609
+ } while (cursor);
2610
+ this._loadGroupInviteAggregates();
2611
+ let stale = false;
2612
+ for (const key of this._groupInviteAggregates.keys()) {
2613
+ if (!seen.has(key)) {
2614
+ this._groupInviteAggregates.delete(key);
2615
+ stale = true;
2616
+ }
2617
+ }
2618
+ if (stale)
2619
+ this._persistGroupInviteAggregates();
2620
+ }
2621
+ /** 接受/拒绝/撤回成功后清理本地待处理聚合项。 */
2622
+ clearGroupInviteAggregate(params) {
2623
+ this._loadGroupInviteAggregates();
2624
+ const input = params ?? {};
2625
+ const inviteId = String(input.invite_id ?? input.inviteId ?? '').trim();
2626
+ const groupId = normalizeGroupId(input.group_id ?? input.group_aid ?? input.groupId ?? input.groupAid);
2627
+ const inviteeAid = String(input.invitee_aid ?? input.inviteeAid ?? this._aid ?? '').trim();
2628
+ let removed = false;
2629
+ for (const [key, value] of this._groupInviteAggregates) {
2630
+ const matchesKey = groupId && inviteeAid && key === `${groupId.toLowerCase()}\u0000${inviteeAid.toLowerCase()}`;
2631
+ const matchesInvite = inviteId && this._groupInviteArray(value.invite_ids).includes(inviteId);
2632
+ if (matchesKey || matchesInvite) {
2633
+ this._groupInviteAggregates.delete(key);
2634
+ removed = true;
2635
+ }
2636
+ }
2637
+ if (removed)
2638
+ this._persistGroupInviteAggregates();
2639
+ }
2384
2640
  _commitSeqTrackerState(ns) {
2385
2641
  return this._delivery.commitSeqTrackerState(ns);
2386
2642
  }
@@ -2394,19 +2650,10 @@ export class AUNClient {
2394
2650
  return this._delivery.clampAckParams(method, params);
2395
2651
  }
2396
2652
  _repairPushContiguousBound(ns, pushSeq, hasPayload, label) {
2397
- if (!ns || !Number.isFinite(pushSeq) || pushSeq <= 0) {
2398
- return ns ? this._seqTracker.getContiguousSeq(ns) : 0;
2399
- }
2400
- const contig = this._seqTracker.getContiguousSeq(ns);
2401
- const shouldRepair = contig > pushSeq;
2402
- if (!shouldRepair)
2403
- return contig;
2404
- const repairedTo = Math.max(0, pushSeq - 1);
2405
- this._seqTracker.repairContiguousSeq(ns, repairedTo);
2406
- const repaired = this._seqTracker.getContiguousSeq(ns);
2407
- this._persistRepairedSeq(ns);
2408
- this._clientLog.warn(`${label} push repaired contiguous_seq: ns=${ns} payload=${hasPayload} push_seq=${pushSeq} contiguous=${contig}->${repaired}`);
2409
- return repaired;
2653
+ void pushSeq;
2654
+ void hasPayload;
2655
+ void label;
2656
+ return ns ? this._seqTracker.getContiguousSeq(ns) : 0;
2410
2657
  }
2411
2658
  // ── URL 辅助 ──────────────────────────────────────────────
2412
2659
  /** 跨域时将 Gateway URL 替换为 peer 所在域的 Gateway URL */
@@ -2558,6 +2805,12 @@ export class AUNClient {
2558
2805
  const hasExplicitBackgroundSync = Object.prototype.hasOwnProperty.call(params, 'background_sync');
2559
2806
  const backgroundSyncEnabled = this._sessionOptions.background_sync !== false
2560
2807
  && (!isShortConnection || hasExplicitBackgroundSync);
2808
+ if (backgroundSyncEnabled) {
2809
+ // 连接建立/恢复后先从事实列表恢复定向邀请提示;失败不影响连接本身。
2810
+ this._safeAsync(this._recoverGroupInvites().catch((exc) => {
2811
+ this._clientLog.debug(`group invite recovery failed: ${formatCaughtError(exc)}`);
2812
+ }));
2813
+ }
2561
2814
  if (!isShortConnection) {
2562
2815
  await this._v2E2EE.onConnected({ backgroundSync: backgroundSyncEnabled });
2563
2816
  this._assertReconnectOwner(reconnectOwner);