@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.
- package/README.md +66 -26
- package/README.zh.md +57 -23
- package/cordis.patch.yml +7 -5
- package/lib/builtin-tenants.json +22 -0
- package/lib/builtin-tenants.sha256 +1 -0
- package/lib/client.js +7123 -1635
- package/lib/client.js.map +1 -1
- package/lib/index.js +2534 -474
- package/lib/model-proxy-contract.js +11 -3
- package/lib/provider.js +6 -3
- package/lib/{sdk-adapter-FGaOi8It.mjs → sdk-adapter-CYcJH7sM.mjs} +210 -21
- package/lib/typert.host.js +1305 -106
- package/lib/typert.remote-client.d.ts +25 -1
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +1305 -106
- package/lib/types/builtin-tenant-config.d.ts +19 -0
- package/lib/types/builtin-tenant-config.d.ts.map +1 -0
- package/lib/types/builtin-tenant-config.js +57 -0
- package/lib/types/builtin-tenant-config.js.map +1 -0
- package/lib/types/client/AwikiDeviceJoinReminder.d.ts +15 -0
- package/lib/types/client/AwikiDeviceJoinReminder.d.ts.map +1 -0
- package/lib/types/client/AwikiDeviceJoinReminder.js +74 -0
- package/lib/types/client/AwikiDeviceJoinReminder.js.map +1 -0
- package/lib/types/client/AwikiDevices.d.ts +8 -3
- package/lib/types/client/AwikiDevices.d.ts.map +1 -1
- package/lib/types/client/AwikiDevices.js +64 -8
- package/lib/types/client/AwikiDevices.js.map +1 -1
- package/lib/types/client/AwikiGroupDetails.d.ts.map +1 -1
- package/lib/types/client/AwikiGroupDetails.js +16 -13
- package/lib/types/client/AwikiGroupDetails.js.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.d.ts +10 -4
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +139 -36
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
- package/lib/types/client/AwikiIntegrationSettings.d.ts +21 -0
- package/lib/types/client/AwikiIntegrationSettings.d.ts.map +1 -0
- package/lib/types/client/AwikiIntegrationSettings.js +105 -0
- package/lib/types/client/AwikiIntegrationSettings.js.map +1 -0
- package/lib/types/client/AwikiMail.d.ts.map +1 -1
- package/lib/types/client/AwikiMail.js +12 -8
- package/lib/types/client/AwikiMail.js.map +1 -1
- package/lib/types/client/AwikiOverlay.d.ts +0 -1
- package/lib/types/client/AwikiOverlay.d.ts.map +1 -1
- package/lib/types/client/AwikiOverlay.js +26 -21
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/AwikiProfileCard.d.ts.map +1 -1
- package/lib/types/client/AwikiProfileCard.js +17 -18
- package/lib/types/client/AwikiProfileCard.js.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.d.ts +5 -1
- package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.js +73 -30
- package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
- package/lib/types/client/AwikiSettingsSection.d.ts +28 -13
- package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
- package/lib/types/client/AwikiSettingsSection.js +93 -61
- package/lib/types/client/AwikiSettingsSection.js.map +1 -1
- package/lib/types/client/AwikiUpdates.d.ts +10 -0
- package/lib/types/client/AwikiUpdates.d.ts.map +1 -0
- package/lib/types/client/AwikiUpdates.js +57 -0
- package/lib/types/client/AwikiUpdates.js.map +1 -0
- package/lib/types/client/awiki-client-bridge.d.ts +1 -1
- package/lib/types/client/awiki-client-bridge.d.ts.map +1 -1
- package/lib/types/client/awiki-client-bridge.js +4 -1
- package/lib/types/client/awiki-client-bridge.js.map +1 -1
- package/lib/types/client/controller.d.ts +65 -11
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +480 -127
- package/lib/types/client/controller.js.map +1 -1
- package/lib/types/client/drafts.d.ts +31 -0
- package/lib/types/client/drafts.d.ts.map +1 -0
- package/lib/types/client/drafts.js +94 -0
- package/lib/types/client/drafts.js.map +1 -0
- package/lib/types/client/image-cache.d.ts +2 -2
- package/lib/types/client/image-cache.d.ts.map +1 -1
- package/lib/types/client/image-cache.js +15 -2
- package/lib/types/client/image-cache.js.map +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +55 -15
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/integration-operation.d.ts +9 -0
- package/lib/types/client/integration-operation.d.ts.map +1 -0
- package/lib/types/client/integration-operation.js +54 -0
- package/lib/types/client/integration-operation.js.map +1 -0
- package/lib/types/client/mail-list-cache.d.ts +2 -0
- package/lib/types/client/mail-list-cache.d.ts.map +1 -1
- package/lib/types/client/mail-list-cache.js +18 -0
- package/lib/types/client/mail-list-cache.js.map +1 -1
- package/lib/types/client/otp-cooldown.d.ts +6 -0
- package/lib/types/client/otp-cooldown.d.ts.map +1 -0
- package/lib/types/client/otp-cooldown.js +18 -0
- package/lib/types/client/otp-cooldown.js.map +1 -0
- package/lib/types/client/settings-controller.d.ts +34 -0
- package/lib/types/client/settings-controller.d.ts.map +1 -1
- package/lib/types/client/settings-controller.js +182 -2
- package/lib/types/client/settings-controller.js.map +1 -1
- package/lib/types/client/settings-locales.d.ts +1 -1
- package/lib/types/client/settings-locales.d.ts.map +1 -1
- package/lib/types/client/settings-locales.js +184 -6
- package/lib/types/client/settings-locales.js.map +1 -1
- package/lib/types/client/slots.d.ts +11 -1
- package/lib/types/client/slots.d.ts.map +1 -1
- package/lib/types/client/slots.js +0 -1
- package/lib/types/client/slots.js.map +1 -1
- package/lib/types/desktop-distribution.d.ts +25 -0
- package/lib/types/desktop-distribution.d.ts.map +1 -0
- package/lib/types/desktop-distribution.js +57 -0
- package/lib/types/desktop-distribution.js.map +1 -0
- package/lib/types/domain.d.ts +1 -1
- package/lib/types/domain.d.ts.map +1 -1
- package/lib/types/domain.js +2 -1
- package/lib/types/domain.js.map +1 -1
- package/lib/types/index.d.ts +112 -18
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +897 -205
- package/lib/types/index.js.map +1 -1
- package/lib/types/integration-client.d.ts +17 -0
- package/lib/types/integration-client.d.ts.map +1 -0
- package/lib/types/integration-client.js +233 -0
- package/lib/types/integration-client.js.map +1 -0
- package/lib/types/legacy-sent-mail-cache.d.ts +4 -0
- package/lib/types/legacy-sent-mail-cache.d.ts.map +1 -0
- package/lib/types/legacy-sent-mail-cache.js +29 -0
- package/lib/types/legacy-sent-mail-cache.js.map +1 -0
- package/lib/types/mail-list-client.d.ts +12 -0
- package/lib/types/mail-list-client.d.ts.map +1 -0
- package/lib/types/mail-list-client.js +254 -0
- package/lib/types/mail-list-client.js.map +1 -0
- package/lib/types/mail-recovery-observability.d.ts +8 -0
- package/lib/types/mail-recovery-observability.d.ts.map +1 -0
- package/lib/types/mail-recovery-observability.js +119 -0
- package/lib/types/mail-recovery-observability.js.map +1 -0
- package/lib/types/model-proxy-contract.d.ts.map +1 -1
- package/lib/types/model-proxy-contract.js +7 -0
- package/lib/types/model-proxy-contract.js.map +1 -1
- package/lib/types/package-version.generated.d.ts +4 -0
- package/lib/types/package-version.generated.d.ts.map +1 -0
- package/lib/types/package-version.generated.js +4 -0
- package/lib/types/package-version.generated.js.map +1 -0
- package/lib/types/profile-state.d.ts +6 -0
- package/lib/types/profile-state.d.ts.map +1 -0
- package/lib/types/profile-state.js +41 -0
- package/lib/types/profile-state.js.map +1 -0
- package/lib/types/provider-api.d.ts +36 -9
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +5 -2
- package/lib/types/provider.js.map +1 -1
- package/lib/types/realtime-supervisor.d.ts +6 -0
- package/lib/types/realtime-supervisor.d.ts.map +1 -1
- package/lib/types/realtime-supervisor.js +14 -1
- package/lib/types/realtime-supervisor.js.map +1 -1
- package/lib/types/sdk-adapter.d.ts +32 -6
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +127 -24
- package/lib/types/sdk-adapter.js.map +1 -1
- package/lib/types/settings-rpc-contract.d.ts +58 -0
- package/lib/types/settings-rpc-contract.d.ts.map +1 -1
- package/lib/types/settings-rpc-contract.js +103 -0
- package/lib/types/settings-rpc-contract.js.map +1 -1
- package/lib/types/settings-rpc.d.ts +14 -1
- package/lib/types/settings-rpc.d.ts.map +1 -1
- package/lib/types/settings-rpc.js +130 -1
- package/lib/types/settings-rpc.js.map +1 -1
- package/lib/types/tenant-registry.d.ts +74 -0
- package/lib/types/tenant-registry.d.ts.map +1 -0
- package/lib/types/tenant-registry.js +440 -0
- package/lib/types/tenant-registry.js.map +1 -0
- package/lib/types/types.d.ts +173 -1
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/types.js.map +1 -1
- package/lib/types/update-policy.d.ts +51 -0
- package/lib/types/update-policy.d.ts.map +1 -0
- package/lib/types/update-policy.js +379 -0
- package/lib/types/update-policy.js.map +1 -0
- package/lib/types/version.d.ts +4 -0
- package/lib/types/version.d.ts.map +1 -0
- package/lib/types/version.js +44 -0
- package/lib/types/version.js.map +1 -0
- package/package.json +34 -21
- package/lib/types/sent-mail-store.d.ts +0 -22
- package/lib/types/sent-mail-store.d.ts.map +0 -1
- package/lib/types/sent-mail-store.js +0 -246
- package/lib/types/sent-mail-store.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
|
|
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
|
|
354
|
-
|
|
355
|
-
|
|
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(
|
|
385
|
+
globalThis.localStorage?.removeItem(key);
|
|
365
386
|
else
|
|
366
|
-
globalThis.localStorage?.setItem(
|
|
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.
|
|
462
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
532
|
+
recoveryOperationId: previousTenant === config.value.tenantId ? previous.recoveryOperationId : null,
|
|
487
533
|
});
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
515
|
-
if (
|
|
516
|
-
return
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
609
|
-
|
|
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
|
|
911
|
+
/** Request a dedicated OTP only within the selected account flow. */
|
|
673
912
|
async sendRecoveryOtp(request) {
|
|
674
|
-
const
|
|
675
|
-
|
|
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
|
-
|
|
921
|
+
}
|
|
678
922
|
this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null });
|
|
679
|
-
|
|
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
|
-
/**
|
|
692
|
-
async
|
|
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
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
731
|
-
return
|
|
732
|
-
|
|
733
|
-
|
|
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('
|
|
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.
|
|
739
|
-
|
|
740
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
752
|
-
|
|
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
|
-
|
|
759
|
-
this.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1776
|
-
|
|
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
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
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)
|