@awiki/dsh-plugin 0.3.7-dsh-test.20260831.2 → 0.3.10

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.
Files changed (183) hide show
  1. package/README.md +66 -26
  2. package/README.zh.md +57 -23
  3. package/cordis.patch.yml +7 -5
  4. package/lib/builtin-tenants.json +22 -0
  5. package/lib/builtin-tenants.sha256 +1 -0
  6. package/lib/client.js +7123 -1635
  7. package/lib/client.js.map +1 -1
  8. package/lib/index.js +2534 -474
  9. package/lib/model-proxy-contract.js +11 -3
  10. package/lib/provider.js +6 -3
  11. package/lib/{sdk-adapter-FGaOi8It.mjs → sdk-adapter-CYcJH7sM.mjs} +210 -21
  12. package/lib/typert.host.js +1305 -106
  13. package/lib/typert.remote-client.d.ts +25 -1
  14. package/lib/typert.remote-client.d.ts.map +1 -1
  15. package/lib/typert.remote-client.js +1305 -106
  16. package/lib/types/builtin-tenant-config.d.ts +19 -0
  17. package/lib/types/builtin-tenant-config.d.ts.map +1 -0
  18. package/lib/types/builtin-tenant-config.js +57 -0
  19. package/lib/types/builtin-tenant-config.js.map +1 -0
  20. package/lib/types/client/AwikiDeviceJoinReminder.d.ts +15 -0
  21. package/lib/types/client/AwikiDeviceJoinReminder.d.ts.map +1 -0
  22. package/lib/types/client/AwikiDeviceJoinReminder.js +74 -0
  23. package/lib/types/client/AwikiDeviceJoinReminder.js.map +1 -0
  24. package/lib/types/client/AwikiDevices.d.ts +8 -3
  25. package/lib/types/client/AwikiDevices.d.ts.map +1 -1
  26. package/lib/types/client/AwikiDevices.js +64 -8
  27. package/lib/types/client/AwikiDevices.js.map +1 -1
  28. package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -1
  29. package/lib/types/client/AwikiGroupDetails.js +16 -13
  30. package/lib/types/client/AwikiGroupDetails.js.map +1 -1
  31. package/lib/types/client/AwikiIdentityAccess.d.ts +10 -4
  32. package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
  33. package/lib/types/client/AwikiIdentityAccess.js +139 -36
  34. package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
  35. package/lib/types/client/AwikiIntegrationSettings.d.ts +21 -0
  36. package/lib/types/client/AwikiIntegrationSettings.d.ts.map +1 -0
  37. package/lib/types/client/AwikiIntegrationSettings.js +105 -0
  38. package/lib/types/client/AwikiIntegrationSettings.js.map +1 -0
  39. package/lib/types/client/AwikiMail.d.ts.map +1 -1
  40. package/lib/types/client/AwikiMail.js +12 -8
  41. package/lib/types/client/AwikiMail.js.map +1 -1
  42. package/lib/types/client/AwikiOverlay.d.ts +0 -1
  43. package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
  44. package/lib/types/client/AwikiOverlay.js +26 -21
  45. package/lib/types/client/AwikiOverlay.js.map +1 -1
  46. package/lib/types/client/AwikiProfileCard.d.ts.map +1 -1
  47. package/lib/types/client/AwikiProfileCard.js +17 -18
  48. package/lib/types/client/AwikiProfileCard.js.map +1 -1
  49. package/lib/types/client/AwikiRecoveryForm.d.ts +5 -1
  50. package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
  51. package/lib/types/client/AwikiRecoveryForm.js +73 -30
  52. package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
  53. package/lib/types/client/AwikiSettingsSection.d.ts +28 -13
  54. package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
  55. package/lib/types/client/AwikiSettingsSection.js +93 -61
  56. package/lib/types/client/AwikiSettingsSection.js.map +1 -1
  57. package/lib/types/client/AwikiUpdates.d.ts +10 -0
  58. package/lib/types/client/AwikiUpdates.d.ts.map +1 -0
  59. package/lib/types/client/AwikiUpdates.js +57 -0
  60. package/lib/types/client/AwikiUpdates.js.map +1 -0
  61. package/lib/types/client/awiki-client-bridge.d.ts +1 -1
  62. package/lib/types/client/awiki-client-bridge.d.ts.map +1 -1
  63. package/lib/types/client/awiki-client-bridge.js +4 -1
  64. package/lib/types/client/awiki-client-bridge.js.map +1 -1
  65. package/lib/types/client/controller.d.ts +65 -11
  66. package/lib/types/client/controller.d.ts.map +1 -1
  67. package/lib/types/client/controller.js +480 -127
  68. package/lib/types/client/controller.js.map +1 -1
  69. package/lib/types/client/drafts.d.ts +31 -0
  70. package/lib/types/client/drafts.d.ts.map +1 -0
  71. package/lib/types/client/drafts.js +94 -0
  72. package/lib/types/client/drafts.js.map +1 -0
  73. package/lib/types/client/image-cache.d.ts +2 -2
  74. package/lib/types/client/image-cache.d.ts.map +1 -1
  75. package/lib/types/client/image-cache.js +15 -2
  76. package/lib/types/client/image-cache.js.map +1 -1
  77. package/lib/types/client/index.d.ts.map +1 -1
  78. package/lib/types/client/index.js +55 -15
  79. package/lib/types/client/index.js.map +1 -1
  80. package/lib/types/client/integration-operation.d.ts +9 -0
  81. package/lib/types/client/integration-operation.d.ts.map +1 -0
  82. package/lib/types/client/integration-operation.js +54 -0
  83. package/lib/types/client/integration-operation.js.map +1 -0
  84. package/lib/types/client/mail-list-cache.d.ts +2 -0
  85. package/lib/types/client/mail-list-cache.d.ts.map +1 -1
  86. package/lib/types/client/mail-list-cache.js +18 -0
  87. package/lib/types/client/mail-list-cache.js.map +1 -1
  88. package/lib/types/client/otp-cooldown.d.ts +6 -0
  89. package/lib/types/client/otp-cooldown.d.ts.map +1 -0
  90. package/lib/types/client/otp-cooldown.js +18 -0
  91. package/lib/types/client/otp-cooldown.js.map +1 -0
  92. package/lib/types/client/settings-controller.d.ts +34 -0
  93. package/lib/types/client/settings-controller.d.ts.map +1 -1
  94. package/lib/types/client/settings-controller.js +182 -2
  95. package/lib/types/client/settings-controller.js.map +1 -1
  96. package/lib/types/client/settings-locales.d.ts +1 -1
  97. package/lib/types/client/settings-locales.d.ts.map +1 -1
  98. package/lib/types/client/settings-locales.js +184 -6
  99. package/lib/types/client/settings-locales.js.map +1 -1
  100. package/lib/types/client/slots.d.ts +11 -1
  101. package/lib/types/client/slots.d.ts.map +1 -1
  102. package/lib/types/client/slots.js +0 -1
  103. package/lib/types/client/slots.js.map +1 -1
  104. package/lib/types/desktop-distribution.d.ts +25 -0
  105. package/lib/types/desktop-distribution.d.ts.map +1 -0
  106. package/lib/types/desktop-distribution.js +57 -0
  107. package/lib/types/desktop-distribution.js.map +1 -0
  108. package/lib/types/domain.d.ts +1 -1
  109. package/lib/types/domain.d.ts.map +1 -1
  110. package/lib/types/domain.js +2 -1
  111. package/lib/types/domain.js.map +1 -1
  112. package/lib/types/index.d.ts +112 -18
  113. package/lib/types/index.d.ts.map +1 -1
  114. package/lib/types/index.js +897 -205
  115. package/lib/types/index.js.map +1 -1
  116. package/lib/types/integration-client.d.ts +17 -0
  117. package/lib/types/integration-client.d.ts.map +1 -0
  118. package/lib/types/integration-client.js +233 -0
  119. package/lib/types/integration-client.js.map +1 -0
  120. package/lib/types/legacy-sent-mail-cache.d.ts +4 -0
  121. package/lib/types/legacy-sent-mail-cache.d.ts.map +1 -0
  122. package/lib/types/legacy-sent-mail-cache.js +29 -0
  123. package/lib/types/legacy-sent-mail-cache.js.map +1 -0
  124. package/lib/types/mail-list-client.d.ts +12 -0
  125. package/lib/types/mail-list-client.d.ts.map +1 -0
  126. package/lib/types/mail-list-client.js +254 -0
  127. package/lib/types/mail-list-client.js.map +1 -0
  128. package/lib/types/mail-recovery-observability.d.ts +8 -0
  129. package/lib/types/mail-recovery-observability.d.ts.map +1 -0
  130. package/lib/types/mail-recovery-observability.js +119 -0
  131. package/lib/types/mail-recovery-observability.js.map +1 -0
  132. package/lib/types/model-proxy-contract.d.ts.map +1 -1
  133. package/lib/types/model-proxy-contract.js +7 -0
  134. package/lib/types/model-proxy-contract.js.map +1 -1
  135. package/lib/types/package-version.generated.d.ts +4 -0
  136. package/lib/types/package-version.generated.d.ts.map +1 -0
  137. package/lib/types/package-version.generated.js +4 -0
  138. package/lib/types/package-version.generated.js.map +1 -0
  139. package/lib/types/profile-state.d.ts +6 -0
  140. package/lib/types/profile-state.d.ts.map +1 -0
  141. package/lib/types/profile-state.js +41 -0
  142. package/lib/types/profile-state.js.map +1 -0
  143. package/lib/types/provider-api.d.ts +36 -9
  144. package/lib/types/provider-api.d.ts.map +1 -1
  145. package/lib/types/provider.d.ts.map +1 -1
  146. package/lib/types/provider.js +5 -2
  147. package/lib/types/provider.js.map +1 -1
  148. package/lib/types/realtime-supervisor.d.ts +6 -0
  149. package/lib/types/realtime-supervisor.d.ts.map +1 -1
  150. package/lib/types/realtime-supervisor.js +14 -1
  151. package/lib/types/realtime-supervisor.js.map +1 -1
  152. package/lib/types/sdk-adapter.d.ts +32 -6
  153. package/lib/types/sdk-adapter.d.ts.map +1 -1
  154. package/lib/types/sdk-adapter.js +127 -24
  155. package/lib/types/sdk-adapter.js.map +1 -1
  156. package/lib/types/settings-rpc-contract.d.ts +58 -0
  157. package/lib/types/settings-rpc-contract.d.ts.map +1 -1
  158. package/lib/types/settings-rpc-contract.js +103 -0
  159. package/lib/types/settings-rpc-contract.js.map +1 -1
  160. package/lib/types/settings-rpc.d.ts +14 -1
  161. package/lib/types/settings-rpc.d.ts.map +1 -1
  162. package/lib/types/settings-rpc.js +130 -1
  163. package/lib/types/settings-rpc.js.map +1 -1
  164. package/lib/types/tenant-registry.d.ts +74 -0
  165. package/lib/types/tenant-registry.d.ts.map +1 -0
  166. package/lib/types/tenant-registry.js +440 -0
  167. package/lib/types/tenant-registry.js.map +1 -0
  168. package/lib/types/types.d.ts +173 -1
  169. package/lib/types/types.d.ts.map +1 -1
  170. package/lib/types/types.js.map +1 -1
  171. package/lib/types/update-policy.d.ts +51 -0
  172. package/lib/types/update-policy.d.ts.map +1 -0
  173. package/lib/types/update-policy.js +379 -0
  174. package/lib/types/update-policy.js.map +1 -0
  175. package/lib/types/version.d.ts +4 -0
  176. package/lib/types/version.d.ts.map +1 -0
  177. package/lib/types/version.js +44 -0
  178. package/lib/types/version.js.map +1 -0
  179. package/package.json +34 -21
  180. package/lib/types/sent-mail-store.d.ts +0 -22
  181. package/lib/types/sent-mail-store.d.ts.map +0 -1
  182. package/lib/types/sent-mail-store.js +0 -246
  183. package/lib/types/sent-mail-store.js.map +0 -1
@@ -1,5 +1,6 @@
1
- /** React-free browser controller for the deployment's one AWiki identity. */
1
+ import { AwikiDraftStore } from "./drafts.js";
2
2
  import { IndexedDbAwikiBrowserImageCache, } from "./image-cache.js";
3
+ import { clearMailBrowserCache } from "./mail-list-cache.js";
3
4
  /** Turn a registration rejection into an actionable message without exposing remote response text. */
4
5
  function registrationFailureMessage(failure) {
5
6
  switch (failure.code) {
@@ -19,6 +20,8 @@ function registrationFailureMessage(failure) {
19
20
  return '注册请求过于频繁,请稍后重试。';
20
21
  case 'network':
21
22
  return '无法连接 AWiki 服务,请检查网络后重试。';
23
+ case 'identity-recovery-required':
24
+ return '这台设备保留了原 AWiki 身份,但本地登录状态已被清除。请先恢复该身份或完整清除这台设备上的 AWiki 身份数据,再重新注册。';
22
25
  case 'forbidden':
23
26
  return '当前 AWiki 服务未开放公开注册,或该手机号不在注册白名单。请使用已获准的手机号,或联系管理员开通注册权限。';
24
27
  case 'remote':
@@ -65,7 +68,7 @@ function recoveryPreparationFailureMessage(failure) {
65
68
  function recoveryContinuationFailureMessage(failure, phase) {
66
69
  const remoteCommitted = phase === 'remote_committed' || phase === 'identity_transition_pending';
67
70
  if (remoteCommitted && ['invalid-request', 'conflict', 'forbidden', 'remote'].includes(failure.code)) {
68
- return '身份已在服务端恢复,但本机切换尚未完成。请保留当前恢复操作,并继续完成本机切换;不要重新获取验证码或创建新身份。';
71
+ return '身份已在服务端恢复,本机切换尚未完成。可以重试本机切换,也可以返回入口稍后继续;请勿为同一账号重复发起恢复。';
69
72
  }
70
73
  switch (failure.code) {
71
74
  case 'network':
@@ -74,9 +77,12 @@ function recoveryContinuationFailureMessage(failure, phase) {
74
77
  : '恢复请求结果尚未确认。请保留当前恢复操作并重新检查状态,不要重复提交。';
75
78
  case 'invalid-request':
76
79
  case 'conflict':
77
- return '身份恢复暂未完成。请保留当前恢复操作并重新检查状态,不要重新获取验证码或创建新身份。';
80
+ return '身份恢复暂未完成。请重新检查状态,或返回入口稍后继续;请勿为同一账号重复发起恢复。';
78
81
  case 'rate-limited':
79
82
  return '恢复状态检查过于频繁,请稍后继续。';
83
+ case 'remote':
84
+ case 'forbidden':
85
+ return 'AWiki 服务暂时未能完成恢复。可以重新检查结果,或返回入口稍后继续。';
80
86
  default:
81
87
  return `${failure.code}:${failure.message}`;
82
88
  }
@@ -115,6 +121,9 @@ const INITIAL_VIEW = Object.freeze({
115
121
  attachmentMaxBytes: 0,
116
122
  handleRecoveryPhoneEnabled: false,
117
123
  summaries: Object.freeze({}),
124
+ identityAccess: null,
125
+ accessLoading: true,
126
+ accessError: null,
118
127
  recoveryOperationId: null,
119
128
  recoveryProgress: null,
120
129
  });
@@ -225,6 +234,20 @@ async function call(operation, failureMessage = failure => `${failure.code}:${
225
234
  };
226
235
  }
227
236
  }
237
+ /** Flatten the carrier and the isolated Guest Integration business result. */
238
+ async function callIntegration(operation) {
239
+ try {
240
+ const carried = await operation();
241
+ if (!carried.ok)
242
+ return { ok: false, error: `连接 AWiki Host 失败:${carried.error.message}` };
243
+ if (!carried.value.ok)
244
+ return { ok: false, error: carried.value.error.message };
245
+ return { ok: true, value: carried.value.value };
246
+ }
247
+ catch {
248
+ return { ok: false, error: '临时消息服务暂时不可用,请稍后重试。' };
249
+ }
250
+ }
228
251
  /** Preserve only the stable business code for controller-level state transitions. */
229
252
  async function callWithFailureCode(operation) {
230
253
  try {
@@ -350,20 +373,18 @@ const GROUP_HISTORY_RETRY_DELAYS_MS = [250, 750, 1_500, 2_500];
350
373
  /** Runtime-only decoded-byte budget that prevents repeat Host calls while browsing. */
351
374
  const BROWSER_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 32 * 1024 * 1024;
352
375
  const RECOVERY_OPERATION_STORAGE_KEY = 'awiki.handle-recovery.operation.v1';
353
- function storedRecoveryOperation() {
354
- try {
355
- return globalThis.localStorage?.getItem(RECOVERY_OPERATION_STORAGE_KEY) ?? null;
356
- }
357
- catch {
358
- return null;
359
- }
376
+ function recoveryOperationStorageKey(tenantId) {
377
+ return tenantId === undefined || tenantId === ''
378
+ ? RECOVERY_OPERATION_STORAGE_KEY
379
+ : `${RECOVERY_OPERATION_STORAGE_KEY}.${encodeURIComponent(tenantId)}`;
360
380
  }
361
- function storeRecoveryOperation(operationId) {
381
+ function storeRecoveryOperation(operationId, tenantId) {
362
382
  try {
383
+ const key = recoveryOperationStorageKey(tenantId);
363
384
  if (operationId === null)
364
- globalThis.localStorage?.removeItem(RECOVERY_OPERATION_STORAGE_KEY);
385
+ globalThis.localStorage?.removeItem(key);
365
386
  else
366
- globalThis.localStorage?.setItem(RECOVERY_OPERATION_STORAGE_KEY, operationId);
387
+ globalThis.localStorage?.setItem(key, operationId);
367
388
  }
368
389
  catch {
369
390
  // Recovery remains durable in Core even when browser storage is unavailable.
@@ -415,6 +436,7 @@ function targetOf(conversation) {
415
436
  export class AwikiController {
416
437
  remote;
417
438
  persistentImageCache;
439
+ drafts = new AwikiDraftStore();
418
440
  view = INITIAL_VIEW;
419
441
  listeners = new Set();
420
442
  config = null;
@@ -422,6 +444,10 @@ export class AwikiController {
422
444
  historyCursor;
423
445
  groupMembersCursor;
424
446
  timer;
447
+ opening;
448
+ drawerOpen = false;
449
+ accessRequest;
450
+ tenantTransition;
425
451
  generation = 0;
426
452
  selectionRevision = 0;
427
453
  disposed = false;
@@ -454,50 +480,63 @@ export class AwikiController {
454
480
  this.listeners.add(listener);
455
481
  return () => { this.listeners.delete(listener); };
456
482
  };
483
+ /** Open the deployment-matched dynamic Integration guide. */
484
+ openIntegrationGuide() {
485
+ const url = this.config?.integrationGuideUrl;
486
+ if (url !== undefined)
487
+ window.open(url, '_blank', 'noopener,noreferrer');
488
+ }
457
489
  /** Load Host policy and the shared identity state without starting drawer polling. */
458
- async loadSession() {
490
+ async loadSession(preserveView = false) {
459
491
  if (this.disposed)
460
492
  return { ok: false, error: 'AWiki 插件已卸载' };
461
- this.close();
462
- this.summaryBaselines.clear();
493
+ this.stopPollingLifecycle();
494
+ if (!preserveView)
495
+ this.summaryBaselines.clear();
496
+ const previous = this.view;
497
+ const previousTenant = this.config?.tenantId;
463
498
  const generation = this.generation;
464
- this.publish({ ...INITIAL_VIEW, status: 'loading' });
499
+ if (!preserveView)
500
+ this.publish({ ...INITIAL_VIEW, status: 'loading' });
501
+ const failed = (error) => {
502
+ if (preserveView)
503
+ this.publish({ ...this.view, error });
504
+ else
505
+ this.fail(error);
506
+ return { ok: false, error };
507
+ };
465
508
  const config = await call(() => this.remote.getConfig());
466
509
  if (!this.current(generation))
467
510
  return { ok: true, value: undefined };
468
511
  if (!config.ok)
469
- return this.fail(config.error);
512
+ return failed(config.error);
470
513
  this.config = config.value;
471
514
  const session = await call(() => this.remote.getSession());
472
515
  if (!this.current(generation))
473
516
  return { ok: true, value: undefined };
474
517
  if (!session.ok)
475
- return this.fail(session.error);
518
+ return failed(session.error);
476
519
  const identity = session.value.status === 'active' ? session.value.identity : null;
520
+ const sameOwner = preserveView && previousTenant === config.value.tenantId
521
+ && previous.sessionStatus === session.value.status && previous.identity?.did === identity?.did;
522
+ this.drafts.setScope(config.value.tenantId ?? 'default', identity?.did ?? session.value.status);
477
523
  this.activatePresentationCache(identity);
478
524
  this.publish({
479
- ...this.view,
525
+ ...(sameOwner ? this.view : INITIAL_VIEW),
480
526
  status: 'ready',
481
527
  sessionStatus: session.value.status,
482
528
  identity: session.value.status === 'active' ? session.value.identity : null,
483
529
  error: null,
484
530
  attachmentMaxBytes: config.value.attachmentMaxBytes,
485
531
  handleRecoveryPhoneEnabled: config.value.handleRecoveryPhoneEnabled,
486
- recoveryOperationId: storedRecoveryOperation(),
532
+ recoveryOperationId: previousTenant === config.value.tenantId ? previous.recoveryOperationId : null,
487
533
  });
488
- const operationId = storedRecoveryOperation();
489
- if (operationId !== null) {
490
- const recovery = await call(() => this.remote.getRecoveryStatus({ operationId }));
491
- if (this.current(generation) && recovery.ok) {
492
- this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: recovery.value });
493
- if (recovery.value.phase === 'applied') {
494
- storeRecoveryOperation(null);
495
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: recovery.value });
496
- if (identity === null)
497
- return this.loadSession();
498
- }
499
- }
500
- }
534
+ await this.refreshIdentityAccess();
535
+ if (!this.current(generation))
536
+ return { ok: true, value: undefined };
537
+ if (identity !== null && previous.identity?.did !== identity.did
538
+ && this.view.recoveryOperationId === null && this.view.identityAccess?.recoveries.length === 0)
539
+ this.drafts.clearFlow();
501
540
  if (identity !== null) {
502
541
  await this.loadConversationPreferences(generation);
503
542
  const profile = await call(() => this.remote.getProfile());
@@ -511,19 +550,42 @@ export class AwikiController {
511
550
  * @returns successful readiness or one display-safe Host failure.
512
551
  */
513
552
  async open() {
514
- const loaded = await this.loadSession();
515
- if (!loaded.ok)
516
- return loaded;
553
+ this.drawerOpen = true;
554
+ if (this.tenantTransition !== undefined) {
555
+ return this.tenantTransition.then(() => this.view.error === null ? { ok: true, value: undefined } : { ok: false, error: this.view.error }, () => ({ ok: false, error: this.view.error ?? '租户切换未完成,请重试' }));
556
+ }
557
+ if (this.opening !== undefined)
558
+ return this.opening;
559
+ const opening = this.openOnce();
560
+ this.opening = opening;
561
+ try {
562
+ return await opening;
563
+ }
564
+ finally {
565
+ if (this.opening === opening)
566
+ this.opening = undefined;
567
+ }
568
+ }
569
+ async openOnce() {
570
+ const loading = this.loadSession(this.view.status === 'ready');
517
571
  const generation = this.generation;
518
- if (this.view.identity !== null) {
519
- const listed = await this.refreshConversations(generation);
520
- if (!listed.ok)
521
- return listed;
572
+ const loaded = await loading;
573
+ if (!loaded.ok && this.view.status !== 'ready')
574
+ return loaded;
575
+ if (!this.current(generation) || !this.drawerOpen)
576
+ return loaded;
577
+ let listed = loaded;
578
+ if (loaded.ok && this.view.identity !== null) {
579
+ listed = await this.refreshConversations(generation, this.view.conversations.length > 0);
580
+ if (this.view.selectedConversationId !== null)
581
+ await this.poll(generation);
522
582
  }
523
- if (this.current(generation)) {
583
+ if (this.current(generation) && this.drawerOpen) {
584
+ if (this.timer !== undefined)
585
+ clearInterval(this.timer);
524
586
  this.timer = setInterval(() => { void this.poll(generation); }, this.config?.pollIntervalMs ?? 3_000);
525
587
  }
526
- return { ok: true, value: undefined };
588
+ return listed;
527
589
  }
528
590
  /** Sign out locally while retaining the SDK-owned identity and database. */
529
591
  async logout(request) {
@@ -532,6 +594,7 @@ export class AwikiController {
532
594
  const result = await call(() => this.remote.logout(request));
533
595
  if (!result.ok)
534
596
  return result;
597
+ this.drafts.clearScope();
535
598
  this.close();
536
599
  this.conversationsCursor = undefined;
537
600
  this.historyCursor = undefined;
@@ -544,6 +607,7 @@ export class AwikiController {
544
607
  attachmentMaxBytes: this.config?.attachmentMaxBytes ?? 0,
545
608
  handleRecoveryPhoneEnabled: this.config?.handleRecoveryPhoneEnabled ?? false,
546
609
  });
610
+ await this.loadSession();
547
611
  return result;
548
612
  }
549
613
  /** Resume the preserved local identity and reload its conversations. */
@@ -560,7 +624,52 @@ export class AwikiController {
560
624
  }
561
625
  /** Stop polling and invalidate all in-flight drawer work. */
562
626
  close() {
627
+ this.drawerOpen = false;
628
+ this.opening = undefined;
629
+ this.stopPollingLifecycle();
630
+ }
631
+ /** Fence old requests before switching, then restore the currently visible tenant view. */
632
+ async switchTenant(change) {
633
+ if (this.disposed)
634
+ throw new Error('AWiki 插件已卸载');
635
+ if (this.tenantTransition !== undefined)
636
+ throw new Error('租户正在切换,请稍候');
637
+ this.opening = undefined;
638
+ this.stopPollingLifecycle();
639
+ this.conversationsCursor = undefined;
640
+ this.historyCursor = undefined;
641
+ this.unreadAtOpen.clear();
642
+ this.clearPresentationCache();
643
+ this.publish({ ...INITIAL_VIEW, status: 'loading' });
644
+ const transition = (async () => {
645
+ let switchError;
646
+ try {
647
+ await change();
648
+ }
649
+ catch (error) {
650
+ switchError = error;
651
+ }
652
+ // On failure the Host restores its previous tenant. Reload that exact committed state too.
653
+ if (!this.disposed) {
654
+ const loaded = this.drawerOpen ? await this.openOnce() : await this.loadSession();
655
+ if (!loaded.ok)
656
+ throw new Error(loaded.error);
657
+ }
658
+ if (switchError !== undefined)
659
+ throw switchError;
660
+ })();
661
+ this.tenantTransition = transition;
662
+ try {
663
+ await transition;
664
+ }
665
+ finally {
666
+ if (this.tenantTransition === transition)
667
+ this.tenantTransition = undefined;
668
+ }
669
+ }
670
+ stopPollingLifecycle() {
563
671
  this.generation += 1;
672
+ this.accessRequest = undefined;
564
673
  this.selectionRevision += 1;
565
674
  if (this.timer !== undefined)
566
675
  clearInterval(this.timer);
@@ -577,6 +686,101 @@ export class AwikiController {
577
686
  async sendRegistrationOtp(request) {
578
687
  return this.withPending('发送验证码', () => call(() => this.remote.sendRegistrationOtp(request), registrationOtpFailureMessage));
579
688
  }
689
+ /** Discover Core-owned work without selecting an account or activating a session. */
690
+ refreshIdentityAccess() {
691
+ if (this.accessRequest !== undefined)
692
+ return this.accessRequest;
693
+ const request = this.loadIdentityAccess();
694
+ this.accessRequest = request;
695
+ const settled = () => { if (this.accessRequest === request)
696
+ this.accessRequest = undefined; };
697
+ void request.then(settled, settled);
698
+ return request;
699
+ }
700
+ async loadIdentityAccess() {
701
+ const generation = this.generation;
702
+ const revision = this.recoveryRevision;
703
+ this.publish({ ...this.view, accessLoading: true, accessError: null });
704
+ const result = await call(() => this.remote.getIdentityAccessState());
705
+ if (!this.recoveryCurrent(generation, revision))
706
+ return this.recoveryLeft();
707
+ if (!result.ok) {
708
+ this.publish({ ...this.view, accessLoading: false, accessError: result.error });
709
+ return result;
710
+ }
711
+ const candidates = result.value.recoveries;
712
+ const selected = this.view.recoveryOperationId;
713
+ this.publish({ ...this.view, identityAccess: result.value });
714
+ if (selected !== null) {
715
+ const status = await callWithFailureCode(() => this.remote.getRecoveryStatus({ operationId: selected }));
716
+ if (!this.recoveryCurrent(generation, revision))
717
+ return this.recoveryLeft();
718
+ if (!status.ok) {
719
+ if (status.failureCode === 'not-found' && candidates.length === 0) {
720
+ storeRecoveryOperation(null, this.config?.tenantId);
721
+ this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null });
722
+ }
723
+ else {
724
+ this.publish({ ...this.view, accessLoading: false, accessError: status.error });
725
+ return { ok: false, error: status.error };
726
+ }
727
+ }
728
+ else {
729
+ this.publish({ ...this.view, recoveryProgress: status.value });
730
+ }
731
+ }
732
+ this.publish({ ...this.view, accessLoading: false, accessError: null });
733
+ return { ok: true, value: undefined };
734
+ }
735
+ async selectRecovery(operationId) {
736
+ if (!this.view.identityAccess?.recoveries.some(value => value.operationId === operationId)) {
737
+ return { ok: false, error: '恢复操作已变化,请刷新后重试。' };
738
+ }
739
+ if (this.view.recoveryOperationId !== null)
740
+ this.leaveRecovery();
741
+ else
742
+ this.recoveryRevision++;
743
+ this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: null });
744
+ const result = await this.refreshRecoveryStatus();
745
+ return result.ok ? { ok: true, value: undefined } : result;
746
+ }
747
+ recoveryRevision = 0;
748
+ pendingRecovery = false;
749
+ recoveryCurrent(generation, revision) {
750
+ return this.current(generation) && revision === this.recoveryRevision;
751
+ }
752
+ recoveryLeft() {
753
+ return { ok: false, error: '已离开此恢复流程;进度保留在本机。' };
754
+ }
755
+ /** Navigation only. Never cancel or delete the Core-owned operation. */
756
+ leaveRecovery() {
757
+ this.recoveryRevision++;
758
+ this.accessRequest = undefined;
759
+ const wasPending = this.pendingRecovery;
760
+ if (wasPending)
761
+ this.pendingRequest = undefined;
762
+ this.pendingRecovery = false;
763
+ this.drafts.clearFlow();
764
+ storeRecoveryOperation(null, this.config?.tenantId);
765
+ this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, accessError: null,
766
+ accessLoading: false, pending: wasPending ? null : this.view.pending, error: null });
767
+ }
768
+ /** Inspect this tenant's durable operations before requesting any new OTP for a Handle. */
769
+ async continueRecoveryForHandle(handle) {
770
+ const target = handle.trim().toLowerCase();
771
+ const loaded = await this.refreshIdentityAccess();
772
+ if (!loaded.ok)
773
+ return loaded;
774
+ const matches = this.view.identityAccess?.recoveries.filter(value => target.includes('.')
775
+ ? value.fullHandle.toLowerCase() === target
776
+ : value.fullHandle.slice(0, value.fullHandle.indexOf('.')).toLowerCase() === target) ?? [];
777
+ if (matches.length > 1)
778
+ return { ok: false, error: '此账号的恢复状态存在冲突,请重新检查。' };
779
+ if (matches.length === 0)
780
+ return { ok: true, value: false };
781
+ const selected = await this.selectRecovery(matches[0].operationId);
782
+ return selected.ok ? { ok: true, value: true } : selected;
783
+ }
580
784
  /** Classify one Handle before sending exactly one registration or recovery OTP. */
581
785
  async inspectIdentityAccess(request) {
582
786
  return this.withPending('检查身份', () => call(() => this.remote.inspectIdentityAccess(request), identityAccessInspectionFailureMessage));
@@ -589,32 +793,61 @@ export class AwikiController {
589
793
  async registerIdentity(request) {
590
794
  const generation = this.generation;
591
795
  const result = await this.withPending('注册身份', () => call(() => this.remote.registerIdentity(request), registrationFailureMessage));
592
- if (!result.ok)
796
+ if (!result.ok) {
797
+ if (this.current(generation))
798
+ await this.refreshIdentityAccess();
593
799
  return result;
800
+ }
594
801
  if (!this.current(generation))
595
802
  return result;
803
+ if (result.value.status === 'join-required') {
804
+ this.publish({ ...this.view, identityAccess: { choice: result.value, joining: false, recoveries: [] } });
805
+ }
596
806
  if (result.value.status === 'registered') {
807
+ this.drafts.clearScope();
808
+ this.drafts.setScope(this.config?.tenantId ?? 'default', result.value.identity.did);
597
809
  this.activatePresentationCache(result.value.identity);
598
810
  this.publish({ ...this.view, sessionStatus: 'active', identity: result.value.identity, error: null });
599
811
  await this.refreshConversations(generation);
600
812
  }
601
813
  return result;
602
814
  }
603
- beginDeviceJoin() {
604
- return this.withPending('开始加入设备', () => call(() => this.remote.beginDeviceJoin()));
815
+ async beginDeviceJoin() {
816
+ const generation = this.generation;
817
+ const result = await this.withPending('开始加入设备', () => call(() => this.remote.beginDeviceJoin()));
818
+ if (result.ok && this.current(generation))
819
+ this.publish({ ...this.view, identityAccess: { choice: null, joining: true, recoveries: [] } });
820
+ return result;
605
821
  }
606
822
  async getDeviceJoinStatus() {
823
+ const generation = this.generation;
607
824
  const result = await call(() => this.remote.getDeviceJoinStatus());
608
- if (result.ok && result.value?.completed)
609
- await this.loadSession();
825
+ if (!this.current(generation))
826
+ return { ok: false, error: '身份环境已改变,请重新确认当前流程。' };
827
+ if (result.ok && result.value?.completed) {
828
+ this.drafts.clearFlow();
829
+ await this.open();
830
+ }
610
831
  return result;
611
832
  }
612
833
  cancelDeviceJoin() {
613
834
  return this.withPending('取消加入设备', async () => {
835
+ const generation = this.generation;
614
836
  const result = await call(() => this.remote.cancelDeviceJoin());
837
+ if (result.ok && this.current(generation))
838
+ this.publish({ ...this.view, identityAccess: { choice: null, joining: false, recoveries: [] } });
615
839
  return result.ok ? { ok: true, value: undefined } : result;
616
840
  });
617
841
  }
842
+ retireDeviceIdentityForRejoin() {
843
+ return this.withPending('准备重新加入设备', async () => {
844
+ const result = await call(() => this.remote.retireDeviceIdentityForRejoin());
845
+ if (!result.ok)
846
+ return result;
847
+ this.publish({ ...this.view, sessionStatus: 'unregistered', identity: null, error: null });
848
+ return { ok: true, value: undefined };
849
+ });
850
+ }
618
851
  refreshDeviceManagement() {
619
852
  return call(() => this.remote.refreshDeviceManagement());
620
853
  }
@@ -630,6 +863,12 @@ export class AwikiController {
630
863
  revokeDevice(request) {
631
864
  return this.withPending('撤销设备', () => call(() => this.remote.revokeDevice(request)));
632
865
  }
866
+ prepareRootTransfer(request) {
867
+ return this.withPending('准备授予管理权', () => call(() => this.remote.prepareRootTransfer(request)));
868
+ }
869
+ confirmRootTransfer(request) {
870
+ return this.withPending('验证并授予管理权', () => call(() => this.remote.confirmRootTransfer(request)));
871
+ }
633
872
  /**
634
873
  * Update the deployment identity's public display name.
635
874
  * @param displayName - replacement display name selected by the user.
@@ -669,94 +908,99 @@ export class AwikiController {
669
908
  this.publish({ ...this.view, identity, profile: result.value, error: null });
670
909
  return result;
671
910
  }
672
- /** Request a recovery OTP and persist only its secret-free operation id in the browser. */
911
+ /** Request a dedicated OTP only within the selected account flow. */
673
912
  async sendRecoveryOtp(request) {
674
- const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage), { publishFailure: false });
675
- if (!result.ok)
913
+ const generation = this.generation;
914
+ const revision = this.recoveryRevision;
915
+ const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage), { publishFailure: false, recovery: true });
916
+ if (!this.recoveryCurrent(generation, revision))
917
+ return this.recoveryLeft();
918
+ if (!result.ok) {
919
+ await this.continueRecoveryForHandle(request.fullHandle);
676
920
  return result;
677
- storeRecoveryOperation(result.value.operationId);
921
+ }
678
922
  this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null });
679
- return result;
680
- }
681
- /** Verify the recovery OTP without attempting the remote identity mutation yet. */
682
- async prepareRecovery(request) {
683
- const operationId = this.view.recoveryOperationId;
684
- if (operationId === null)
685
- return this.fail('请先获取恢复验证码');
686
- const result = await this.withPending('验证恢复信息', () => call(() => this.remote.prepareRecovery({ ...request, operationId }), recoveryPreparationFailureMessage, recoveryCarrierFailureMessage), { publishFailure: false });
687
- if (result.ok)
688
- this.publish({ ...this.view, recoveryProgress: result.value });
689
- return result;
923
+ await this.refreshRecoveryStatus();
924
+ return this.recoveryCurrent(generation, revision) ? result : this.recoveryLeft();
690
925
  }
691
- /** Commit the prepared operation once. Unknown outcomes remain available through status refresh. */
692
- async activateRecovery() {
926
+ /** Publish results only into the exact selected flow; Core work survives navigation. */
927
+ async runRecovery(label, operation, enterWhenApplied = false) {
928
+ const generation = this.generation;
929
+ const revision = this.recoveryRevision;
693
930
  const operationId = this.view.recoveryOperationId;
694
931
  if (operationId === null)
695
- return this.fail('没有可继续的身份恢复操作');
696
- if (this.view.recoveryProgress?.phase !== 'ready_to_commit')
697
- return this.fail('请先完成恢复信息验证');
698
- const phase = this.view.recoveryProgress.phase;
699
- const result = await this.withPending('恢复身份', () => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, phase), recoveryCarrierFailureMessage), { publishFailure: false });
700
- if (!result.ok)
932
+ return { ok: false, error: '没有可继续的身份恢复操作' };
933
+ const result = await this.withPending(label, () => operation(operationId), { publishFailure: false, recovery: true });
934
+ if (!this.recoveryCurrent(generation, revision))
935
+ return this.recoveryLeft();
936
+ if (!result.ok) {
937
+ // Reconcile authority after an ambiguous failure, never infer whether Commit occurred.
938
+ const status = await call(() => this.remote.getRecoveryStatus({ operationId }));
939
+ if (!this.recoveryCurrent(generation, revision))
940
+ return this.recoveryLeft();
941
+ this.publish({ ...this.view, recoveryProgress: status.ok ? status.value : this.view.recoveryProgress,
942
+ accessError: status.ok ? null : status.error });
701
943
  return result;
702
- this.publish({ ...this.view, recoveryProgress: result.value });
703
- if (result.value.phase === 'applied') {
704
- storeRecoveryOperation(null);
705
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
706
- await this.open();
707
944
  }
945
+ this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
946
+ if (enterWhenApplied && result.value.phase === 'applied')
947
+ return this.enterRecoveredSession();
708
948
  return result;
709
949
  }
710
- /** Refresh Core status without repeating activation. */
950
+ async prepareRecovery(request) {
951
+ if (!this.view.recoveryProgress?.allowedActions?.includes('prepare'))
952
+ return { ok: false, error: '请先检查恢复状态' };
953
+ return this.runRecovery('验证恢复信息', operationId => call(() => this.remote.prepareRecovery({ ...request, operationId }), recoveryPreparationFailureMessage, recoveryCarrierFailureMessage));
954
+ }
955
+ async activateRecovery() {
956
+ const progress = this.view.recoveryProgress;
957
+ if (!progress?.allowedActions?.includes('activate'))
958
+ return { ok: false, error: '请先完成恢复信息验证' };
959
+ return this.runRecovery('恢复身份', operationId => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), true);
960
+ }
961
+ /** Read-only even for applied operations. Entering the identity is a separate action. */
711
962
  async refreshRecoveryStatus() {
712
- const operationId = this.view.recoveryOperationId;
713
- if (operationId === null)
714
- return this.fail('没有可查询的身份恢复操作');
715
- const result = await this.withPending('刷新恢复状态', () => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage), { publishFailure: false });
716
- if (!result.ok)
717
- return result;
718
- this.publish({ ...this.view, recoveryProgress: result.value });
719
- if (result.value.phase === 'applied') {
720
- storeRecoveryOperation(null);
721
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
722
- await this.open();
723
- }
724
- return result;
963
+ return this.runRecovery('刷新恢复状态', operationId => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage));
725
964
  }
726
- /** Resume only a Core-declared retryable or uncertain phase. */
727
965
  async resumeRecovery() {
728
- const operationId = this.view.recoveryOperationId;
729
966
  const progress = this.view.recoveryProgress;
730
- if (operationId === null || progress === null)
731
- return this.fail('请先刷新恢复状态');
732
- if (!progress.retryable && !['remote_outcome_unknown', 'remote_committed', 'identity_transition_pending'].includes(progress.phase)) {
733
- return this.fail('当前恢复状态不能重试');
967
+ if (!progress?.allowedActions?.includes('resume'))
968
+ return { ok: false, error: '当前恢复状态不能重试' };
969
+ return this.runRecovery('继续恢复身份', operationId => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), true);
970
+ }
971
+ async enterRecoveredSession() {
972
+ const generation = this.generation;
973
+ const revision = this.recoveryRevision;
974
+ const progress = this.view.recoveryProgress;
975
+ if (progress?.phase !== 'applied' || !progress.allowedActions?.includes('activate_identity')) {
976
+ return { ok: false, error: '当前恢复身份尚不可进入,请重新检查。' };
734
977
  }
735
- const result = await this.withPending('继续恢复身份', () => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), { publishFailure: false });
978
+ const result = await this.withPending('进入恢复后的身份', () => call(() => this.remote.enterRecoveredSession({ operationId: progress.operationId })), { publishFailure: false, recovery: true });
979
+ if (!this.recoveryCurrent(generation, revision))
980
+ return this.recoveryLeft();
736
981
  if (!result.ok)
737
982
  return result;
738
- this.publish({ ...this.view, recoveryProgress: result.value });
739
- if (result.value.phase === 'applied') {
740
- storeRecoveryOperation(null);
741
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
742
- await this.open();
743
- }
744
- return result;
983
+ this.leaveRecovery();
984
+ const opened = await this.open();
985
+ return opened.ok ? result : opened;
745
986
  }
746
- /** Discard only a pre-attempt operation. */
987
+ /** Only Core-authorized pre-attempt cancellation destroys the pending operation. */
747
988
  async discardRecovery() {
989
+ const generation = this.generation;
990
+ const revision = this.recoveryRevision;
748
991
  const operationId = this.view.recoveryOperationId;
749
992
  if (operationId === null)
750
993
  return { ok: true, value: undefined };
751
- const phase = this.view.recoveryProgress?.phase;
752
- if (phase !== undefined && phase !== 'awaiting_factor' && phase !== 'ready_to_commit') {
753
- return this.fail('当前恢复状态不能取消');
994
+ if (!this.view.recoveryProgress?.allowedActions?.includes('discard_pre_attempt')) {
995
+ return { ok: false, error: '当前恢复状态不能取消;可以返回入口,稍后继续。' };
754
996
  }
755
- const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })), { publishFailure: false });
997
+ const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })), { publishFailure: false, recovery: true });
998
+ if (!this.recoveryCurrent(generation, revision))
999
+ return this.recoveryLeft();
756
1000
  if (!result.ok)
757
1001
  return result;
758
- storeRecoveryOperation(null);
759
- this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null });
1002
+ this.leaveRecovery();
1003
+ void this.refreshIdentityAccess();
760
1004
  return { ok: true, value: undefined };
761
1005
  }
762
1006
  /** Read the active deployment identity's public mailbox state. */
@@ -1549,14 +1793,28 @@ export class AwikiController {
1549
1793
  }
1550
1794
  return result;
1551
1795
  }
1552
- /** Clear Host-owned local data and immediately remove every cached browser projection. */
1796
+ /** Clear current-tenant local data and only its owned browser projections. */
1553
1797
  async clearLocalData(request) {
1554
1798
  if (this.disposed)
1555
1799
  return { ok: false, error: 'AWiki 插件已卸载' };
1800
+ const generation = this.generation;
1801
+ const ownerDid = this.presentationCacheOwnerDid;
1802
+ const tenantId = this.config?.tenantId;
1556
1803
  const result = await call(() => this.remote.clearLocalData(request));
1557
1804
  if (!result.ok)
1558
1805
  return result;
1559
- await this.persistentImageCache.clear().catch(() => undefined);
1806
+ const owners = [...(result.value.clearedIdentityDids ?? []), ...(ownerDid === null ? [] : [ownerDid])];
1807
+ for (const owner of new Set(owners)) {
1808
+ await this.persistentImageCache.clear(owner).catch(() => undefined);
1809
+ try {
1810
+ clearMailBrowserCache(globalThis.localStorage, owner);
1811
+ }
1812
+ catch { }
1813
+ }
1814
+ storeRecoveryOperation(null, tenantId);
1815
+ if (!this.current(generation))
1816
+ return result;
1817
+ this.drafts.clearScope();
1560
1818
  this.close();
1561
1819
  this.config = null;
1562
1820
  this.conversationsCursor = undefined;
@@ -1564,12 +1822,61 @@ export class AwikiController {
1564
1822
  this.unreadAtOpen.clear();
1565
1823
  this.summaryBaselines.clear();
1566
1824
  this.clearPresentationCache();
1567
- storeRecoveryOperation(null);
1568
- this.publish({ ...INITIAL_VIEW, status: 'ready' });
1825
+ this.publish({ ...INITIAL_VIEW, status: 'ready', accessLoading: false, identityAccess: { choice: null, joining: false, recoveries: [] } });
1569
1826
  return result;
1570
1827
  }
1828
+ /** Read the Integration without coupling Guest Gateway health to the main AWiki view. */
1829
+ getIntegration() {
1830
+ return callIntegration(() => this.remote.getIntegration());
1831
+ }
1832
+ createIntegration(request) {
1833
+ return callIntegration(() => this.remote.createIntegration(request));
1834
+ }
1835
+ updateIntegration(request) {
1836
+ return callIntegration(() => this.remote.updateIntegration(request));
1837
+ }
1838
+ rotateIntegrationId(request) {
1839
+ return callIntegration(() => this.remote.rotateIntegrationId(request));
1840
+ }
1841
+ closeIntegration(request) {
1842
+ return callIntegration(() => this.remote.closeIntegration(request));
1843
+ }
1844
+ reopenIntegration(request) {
1845
+ return callIntegration(() => this.remote.reopenIntegration(request));
1846
+ }
1847
+ /** Return only locally known groups for which the active identity is authoritative owner. */
1848
+ async listOwnedGroups() {
1849
+ const groups = [];
1850
+ const visited = new Set();
1851
+ let cursor;
1852
+ for (let pageIndex = 0; pageIndex < 20 && groups.length < 20; pageIndex += 1) {
1853
+ const conversations = await call(() => this.remote.listConversations({
1854
+ limit: 100,
1855
+ ...(cursor === undefined ? {} : { cursor }),
1856
+ }));
1857
+ if (!conversations.ok)
1858
+ return conversations;
1859
+ for (const conversation of conversations.value.items) {
1860
+ if (conversation.kind !== 'group' || visited.has(conversation.groupDid))
1861
+ continue;
1862
+ visited.add(conversation.groupDid);
1863
+ const snapshot = await call(() => this.remote.getGroup({ groupDid: conversation.groupDid }));
1864
+ if (!snapshot.ok)
1865
+ continue;
1866
+ if (snapshot.value.myRole === 'owner')
1867
+ groups.push(snapshot.value);
1868
+ if (groups.length >= 20)
1869
+ break;
1870
+ }
1871
+ cursor = conversations.value.nextCursor;
1872
+ if (cursor === undefined)
1873
+ break;
1874
+ }
1875
+ return { ok: true, value: groups };
1876
+ }
1571
1877
  /** Stop timers, invalidate work, and drop subscribers during HMR unload. */
1572
1878
  dispose() {
1879
+ this.drafts.clear();
1573
1880
  this.disposed = true;
1574
1881
  this.close();
1575
1882
  this.listeners.clear();
@@ -1655,14 +1962,18 @@ export class AwikiController {
1655
1962
  /** List the active identity's own conversations and detect a revoked local credential. */
1656
1963
  async listConversationPage(request) {
1657
1964
  const result = await callWithFailureCode(() => this.remote.listConversations(request));
1965
+ if (!result.ok && result.failureCode === 'device-rejoin-required') {
1966
+ this.enterBlockedIdentityState('device-rejoin-required');
1967
+ return { ok: false, error: '当前设备已被撤销,请重新申请加入。' };
1968
+ }
1658
1969
  if (!result.ok && (result.failureCode === 'identity-recovery-required' || result.failureCode === 'forbidden')) {
1659
- this.enterIdentityRecoveryRequired();
1970
+ this.enterBlockedIdentityState('recovery-required');
1660
1971
  return { ok: false, error: '当前设备的 AWiki 身份凭证已失效,请重新恢复身份。' };
1661
1972
  }
1662
1973
  return result.ok ? result : { ok: false, error: result.error };
1663
1974
  }
1664
1975
  /** Replace only visible browser projections; Core identity and SQLite state remain untouched. */
1665
- enterIdentityRecoveryRequired() {
1976
+ enterBlockedIdentityState(status) {
1666
1977
  if (this.view.identity === null)
1667
1978
  return;
1668
1979
  this.close();
@@ -1675,7 +1986,7 @@ export class AwikiController {
1675
1986
  this.publish({
1676
1987
  ...this.view,
1677
1988
  status: 'ready',
1678
- sessionStatus: 'recovery-required',
1989
+ sessionStatus: status,
1679
1990
  conversations: Object.freeze([]),
1680
1991
  hiddenConversations: Object.freeze([]),
1681
1992
  conversationsHasMore: false,
@@ -1750,6 +2061,19 @@ export class AwikiController {
1750
2061
  this.publish({ ...this.view, error: 'AWiki 远端消息归属不一致,请重新打开会话。' });
1751
2062
  return;
1752
2063
  }
2064
+ const displayPeers = [...new Set([
2065
+ ...this.view.messages.map(message => message.senderDid),
2066
+ ...this.view.groupMembers.flatMap(member => member.did === undefined ? [] : [member.did]),
2067
+ ])];
2068
+ const displayProfiles = [];
2069
+ for (let start = 0; start < displayPeers.length; start += 100) {
2070
+ const profileResult = await call(() => this.remote.getDisplayProfiles(displayPeers.slice(start, start + 100)));
2071
+ if (!this.current(generation) || this.view.selectedConversationId !== selected)
2072
+ return;
2073
+ if (profileResult.ok)
2074
+ displayProfiles.push(...profileResult.value);
2075
+ }
2076
+ const byDid = new Map(displayProfiles.filter(profile => profile.cacheHit).map(profile => [profile.did, profile]));
1753
2077
  const existingIds = new Set(this.view.messages.map(message => message.id));
1754
2078
  const incoming = result.value.items.filter(message => !existingIds.has(message.id));
1755
2079
  const messages = mergeLatestMessages(this.view.messages, result.value.items);
@@ -1759,7 +2083,22 @@ export class AwikiController {
1759
2083
  }
1760
2084
  this.publish({
1761
2085
  ...this.view,
1762
- messages,
2086
+ messages: messages.map(message => {
2087
+ const profile = byDid.get(message.senderDid);
2088
+ if (profile === undefined)
2089
+ return message;
2090
+ const { senderDisplayName: _snapshot, ...rest } = message;
2091
+ const handle = profile.handle ?? message.senderHandle;
2092
+ return { ...rest, ...profile.displayName === undefined ? {} : { senderDisplayName: profile.displayName }, ...handle === undefined ? {} : { senderHandle: handle } };
2093
+ }),
2094
+ groupMembers: this.view.groupMembers.map(member => {
2095
+ const profile = member.did === undefined ? undefined : byDid.get(member.did);
2096
+ if (profile === undefined)
2097
+ return member;
2098
+ const { displayName: _snapshot, ...rest } = member;
2099
+ const handle = profile.handle ?? member.handle;
2100
+ return { ...rest, ...profile.displayName === undefined ? {} : { displayName: profile.displayName }, ...handle === undefined ? {} : { handle } };
2101
+ }),
1763
2102
  summaries: this.staleSummaries(selected, incoming),
1764
2103
  });
1765
2104
  }
@@ -1767,20 +2106,34 @@ export class AwikiController {
1767
2106
  this.polling = false;
1768
2107
  }
1769
2108
  }
2109
+ pendingRequest;
1770
2110
  async withPending(label, operation, options = {}) {
1771
2111
  if (this.disposed)
1772
2112
  return { ok: false, error: 'AWiki 插件已卸载' };
2113
+ if (this.pendingRequest !== undefined)
2114
+ return { ok: false, error: '上一项操作仍在处理,请稍候再试。' };
2115
+ const token = Symbol(label);
2116
+ this.pendingRequest = token;
2117
+ this.pendingRecovery = options.recovery === true;
1773
2118
  const generation = this.generation;
1774
2119
  this.publish({ ...this.view, pending: label, error: null });
1775
- const result = await operation();
1776
- if (!this.current(generation))
2120
+ try {
2121
+ const result = await operation();
2122
+ if (this.current(generation) && this.pendingRequest === token)
2123
+ this.publish({
2124
+ ...this.view, error: result.ok || options.publishFailure === false ? null : result.error,
2125
+ });
1777
2126
  return result;
1778
- this.publish({
1779
- ...this.view,
1780
- pending: null,
1781
- error: result.ok || options.publishFailure === false ? null : result.error,
1782
- });
1783
- return result;
2127
+ }
2128
+ finally {
2129
+ if (this.pendingRequest === token) {
2130
+ this.pendingRequest = undefined;
2131
+ this.pendingRecovery = false;
2132
+ // Closing/reopening invalidates reads, but the original mutation still settles.
2133
+ if (!this.disposed)
2134
+ this.publish({ ...this.view, pending: null });
2135
+ }
2136
+ }
1784
2137
  }
1785
2138
  appendMessage(message) {
1786
2139
  if (this.view.selectedConversationId !== message.conversationId)