@awiki/dsh-plugin 0.3.7 → 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 +109 -71
- package/README.zh.md +89 -61
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/cordis.patch.yml +6 -5
- package/lib/builtin-tenants.json +22 -0
- package/lib/builtin-tenants.sha256 +1 -0
- package/lib/client.js +7973 -2264
- package/lib/client.js.map +1 -1
- package/lib/index.js +3481 -1145
- package/lib/model-proxy-contract.js +11 -3
- package/lib/provider.js +73 -17
- package/lib/{sdk-adapter--trNlr-T.mjs → sdk-adapter-CYcJH7sM.mjs} +374 -101
- package/lib/typert.host.js +1595 -168
- package/lib/typert.remote-client.d.ts +43 -7
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +1595 -168
- 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 +33 -0
- package/lib/types/client/AwikiDevices.d.ts.map +1 -0
- package/lib/types/client/AwikiDevices.js +141 -0
- package/lib/types/client/AwikiDevices.js.map +1 -0
- 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 +15 -8
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +203 -39
- 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 +1 -1
- package/lib/types/client/AwikiMail.d.ts.map +1 -1
- package/lib/types/client/AwikiMail.js +17 -137
- 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 -28
- 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 +84 -44
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +515 -309
- 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 +63 -19
- 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 +192 -14
- package/lib/types/client/settings-locales.js.map +1 -1
- package/lib/types/client/slots.d.ts +20 -10
- 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/conversation-preferences.d.ts.map +1 -1
- package/lib/types/conversation-preferences.js +0 -17
- package/lib/types/conversation-preferences.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 +164 -41
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +1562 -529
- 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/listener-state.d.ts +5 -3
- package/lib/types/listener-state.d.ts.map +1 -1
- package/lib/types/listener-state.js +28 -13
- package/lib/types/listener-state.js.map +1 -1
- package/lib/types/listener.d.ts +9 -32
- package/lib/types/listener.d.ts.map +1 -1
- package/lib/types/listener.js +11 -127
- package/lib/types/listener.js.map +1 -1
- 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/mail.d.ts +2 -28
- package/lib/types/mail.d.ts.map +1 -1
- package/lib/types/mail.js +2 -79
- package/lib/types/mail.js.map +1 -1
- 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 +2 -13
- package/lib/types/profile-state.d.ts.map +1 -1
- package/lib/types/profile-state.js +3 -20
- package/lib/types/profile-state.js.map +1 -1
- package/lib/types/provider-api.d.ts +134 -43
- package/lib/types/provider-api.d.ts.map +1 -1
- package/lib/types/provider.d.ts +8 -2
- package/lib/types/provider.d.ts.map +1 -1
- package/lib/types/provider.js +76 -16
- package/lib/types/provider.js.map +1 -1
- package/lib/types/realtime-supervisor.d.ts +60 -0
- package/lib/types/realtime-supervisor.d.ts.map +1 -0
- package/lib/types/realtime-supervisor.js +255 -0
- package/lib/types/realtime-supervisor.js.map +1 -0
- package/lib/types/sdk-adapter.d.ts +62 -11
- package/lib/types/sdk-adapter.d.ts.map +1 -1
- package/lib/types/sdk-adapter.js +285 -123
- 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/tools.d.ts +1 -1
- package/lib/types/tools.d.ts.map +1 -1
- package/lib/types/tools.js +4 -20
- package/lib/types/tools.js.map +1 -1
- package/lib/types/types.d.ts +249 -66
- 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 +26 -8
- package/lib/types/base64.d.ts +0 -4
- package/lib/types/base64.d.ts.map +0 -1
- package/lib/types/base64.js +0 -28
- package/lib/types/base64.js.map +0 -1
- package/lib/types/client/mail-attachment.d.ts +0 -40
- package/lib/types/client/mail-attachment.d.ts.map +0 -1
- package/lib/types/client/mail-attachment.js +0 -187
- package/lib/types/client/mail-attachment.js.map +0 -1
- package/lib/types/sent-mail-store.d.ts +0 -27
- package/lib/types/sent-mail-store.d.ts.map +0 -1
- package/lib/types/sent-mail-store.js +0 -348
- 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
|
}
|
|
@@ -106,7 +112,6 @@ const INITIAL_VIEW = Object.freeze({
|
|
|
106
112
|
groupAccess: null,
|
|
107
113
|
groupMembers: Object.freeze([]),
|
|
108
114
|
groupMembersHasMore: false,
|
|
109
|
-
groupRecovery: null,
|
|
110
115
|
messages: Object.freeze([]),
|
|
111
116
|
historyHasMore: false,
|
|
112
117
|
localPending: false,
|
|
@@ -114,9 +119,11 @@ const INITIAL_VIEW = Object.freeze({
|
|
|
114
119
|
pending: null,
|
|
115
120
|
error: null,
|
|
116
121
|
attachmentMaxBytes: 0,
|
|
117
|
-
|
|
118
|
-
legacySharedStateDetected: false,
|
|
122
|
+
handleRecoveryPhoneEnabled: false,
|
|
119
123
|
summaries: Object.freeze({}),
|
|
124
|
+
identityAccess: null,
|
|
125
|
+
accessLoading: true,
|
|
126
|
+
accessError: null,
|
|
120
127
|
recoveryOperationId: null,
|
|
121
128
|
recoveryProgress: null,
|
|
122
129
|
});
|
|
@@ -178,44 +185,9 @@ function groupReadFailureMessage(failure) {
|
|
|
178
185
|
return `${failure.code}:${failure.message}`;
|
|
179
186
|
}
|
|
180
187
|
}
|
|
181
|
-
function groupRecoveryFailureMessage(failure) {
|
|
182
|
-
return failure.code === 'network'
|
|
183
|
-
? '无法连接 AWiki 服务,暂时不能恢复旧群聊身份。'
|
|
184
|
-
: '暂时不能检查旧群聊身份,请稍后重试。';
|
|
185
|
-
}
|
|
186
188
|
function conversationPreferenceFailureMessage(_failure) {
|
|
187
189
|
return '无法保存本机会话设置,请稍后重试。';
|
|
188
190
|
}
|
|
189
|
-
function recoveryFingerprint(parts) {
|
|
190
|
-
let hash = 0x811c9dc5;
|
|
191
|
-
for (const part of parts) {
|
|
192
|
-
for (let index = 0; index < part.length; index += 1) {
|
|
193
|
-
hash ^= part.charCodeAt(index);
|
|
194
|
-
hash = Math.imul(hash, 0x01000193);
|
|
195
|
-
}
|
|
196
|
-
hash ^= 0;
|
|
197
|
-
hash = Math.imul(hash, 0x01000193);
|
|
198
|
-
}
|
|
199
|
-
return (hash >>> 0).toString(16).padStart(8, '0');
|
|
200
|
-
}
|
|
201
|
-
function groupRecoveryProjection(summary, hiddenGroupDids) {
|
|
202
|
-
const hiddenItems = summary.items.filter(item => hiddenGroupDids.has(item.groupDid));
|
|
203
|
-
const hiddenPending = hiddenItems.filter(item => item.status === 'pending').length;
|
|
204
|
-
const hiddenBlocked = hiddenItems.filter(item => item.status === 'blocked').length;
|
|
205
|
-
const pending = Math.max(0, summary.pending - hiddenPending);
|
|
206
|
-
const blocked = Math.max(0, summary.blocked - hiddenBlocked);
|
|
207
|
-
const visibleItems = summary.items
|
|
208
|
-
.filter(item => !hiddenGroupDids.has(item.groupDid))
|
|
209
|
-
.map(item => `${item.status}:${item.groupDid}`)
|
|
210
|
-
.sort();
|
|
211
|
-
const signature = `v1:${pending}:${blocked}:${recoveryFingerprint(visibleItems)}`;
|
|
212
|
-
if (blocked > 0)
|
|
213
|
-
return { view: { status: 'blocked', pending, blocked }, signature };
|
|
214
|
-
return {
|
|
215
|
-
view: pending > 0 ? { status: 'pending', pending, blocked: 0 } : null,
|
|
216
|
-
signature,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
191
|
function groupReadFailureReason(failure) {
|
|
220
192
|
switch (failure.code) {
|
|
221
193
|
case 'group-identity-stale': return 'recovering';
|
|
@@ -262,6 +234,20 @@ async function call(operation, failureMessage = failure => `${failure.code}:${
|
|
|
262
234
|
};
|
|
263
235
|
}
|
|
264
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
|
+
}
|
|
265
251
|
/** Preserve only the stable business code for controller-level state transitions. */
|
|
266
252
|
async function callWithFailureCode(operation) {
|
|
267
253
|
try {
|
|
@@ -386,23 +372,15 @@ function sameIdentity(identity, peer) {
|
|
|
386
372
|
const GROUP_HISTORY_RETRY_DELAYS_MS = [250, 750, 1_500, 2_500];
|
|
387
373
|
/** Runtime-only decoded-byte budget that prevents repeat Host calls while browsing. */
|
|
388
374
|
const BROWSER_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
389
|
-
const
|
|
390
|
-
function recoveryOperationStorageKey(
|
|
391
|
-
return
|
|
375
|
+
const RECOVERY_OPERATION_STORAGE_KEY = 'awiki.handle-recovery.operation.v1';
|
|
376
|
+
function recoveryOperationStorageKey(tenantId) {
|
|
377
|
+
return tenantId === undefined || tenantId === ''
|
|
378
|
+
? RECOVERY_OPERATION_STORAGE_KEY
|
|
379
|
+
: `${RECOVERY_OPERATION_STORAGE_KEY}.${encodeURIComponent(tenantId)}`;
|
|
392
380
|
}
|
|
393
|
-
function
|
|
381
|
+
function storeRecoveryOperation(operationId, tenantId) {
|
|
394
382
|
try {
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
function storeRecoveryOperation(tenantDomain, operationId) {
|
|
402
|
-
if (tenantDomain === undefined)
|
|
403
|
-
return;
|
|
404
|
-
try {
|
|
405
|
-
const key = recoveryOperationStorageKey(tenantDomain);
|
|
383
|
+
const key = recoveryOperationStorageKey(tenantId);
|
|
406
384
|
if (operationId === null)
|
|
407
385
|
globalThis.localStorage?.removeItem(key);
|
|
408
386
|
else
|
|
@@ -458,6 +436,7 @@ function targetOf(conversation) {
|
|
|
458
436
|
export class AwikiController {
|
|
459
437
|
remote;
|
|
460
438
|
persistentImageCache;
|
|
439
|
+
drafts = new AwikiDraftStore();
|
|
461
440
|
view = INITIAL_VIEW;
|
|
462
441
|
listeners = new Set();
|
|
463
442
|
config = null;
|
|
@@ -465,6 +444,10 @@ export class AwikiController {
|
|
|
465
444
|
historyCursor;
|
|
466
445
|
groupMembersCursor;
|
|
467
446
|
timer;
|
|
447
|
+
opening;
|
|
448
|
+
drawerOpen = false;
|
|
449
|
+
accessRequest;
|
|
450
|
+
tenantTransition;
|
|
468
451
|
generation = 0;
|
|
469
452
|
selectionRevision = 0;
|
|
470
453
|
disposed = false;
|
|
@@ -476,18 +459,12 @@ export class AwikiController {
|
|
|
476
459
|
directProfiles = new Map();
|
|
477
460
|
/** Last trustworthy group title for the active identity, keyed by canonical Group DID. */
|
|
478
461
|
groupTitles = new Map();
|
|
479
|
-
/** Current secret-free Core journal state, keyed by canonical Group DID. */
|
|
480
|
-
groupRecoveryItems = new Map();
|
|
481
462
|
/** Identity-scoped product overlays. Core conversations and history remain untouched. */
|
|
482
463
|
hiddenConversationPreferences = new Map();
|
|
483
|
-
dismissedGroupRecoverySignature;
|
|
484
|
-
lastGroupRecoverySignature;
|
|
485
|
-
lastGroupRecoverySummary;
|
|
486
464
|
/** Verified image payloads retained outside observable state for instant remounts. */
|
|
487
465
|
imageAttachments = new Map();
|
|
488
466
|
imageAttachmentCacheBytes = 0;
|
|
489
467
|
presentationCacheOwnerDid = null;
|
|
490
|
-
groupRecoveryOperation;
|
|
491
468
|
/**
|
|
492
469
|
* @param remote - generated Host Remote namespace.
|
|
493
470
|
* @param persistentImageCache - browser-origin verified preview cache.
|
|
@@ -503,51 +480,63 @@ export class AwikiController {
|
|
|
503
480
|
this.listeners.add(listener);
|
|
504
481
|
return () => { this.listeners.delete(listener); };
|
|
505
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
|
+
}
|
|
506
489
|
/** Load Host policy and the shared identity state without starting drawer polling. */
|
|
507
|
-
async loadSession() {
|
|
490
|
+
async loadSession(preserveView = false) {
|
|
508
491
|
if (this.disposed)
|
|
509
492
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
510
|
-
this.
|
|
511
|
-
|
|
493
|
+
this.stopPollingLifecycle();
|
|
494
|
+
if (!preserveView)
|
|
495
|
+
this.summaryBaselines.clear();
|
|
496
|
+
const previous = this.view;
|
|
497
|
+
const previousTenant = this.config?.tenantId;
|
|
512
498
|
const generation = this.generation;
|
|
513
|
-
|
|
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
|
+
};
|
|
514
508
|
const config = await call(() => this.remote.getConfig());
|
|
515
509
|
if (!this.current(generation))
|
|
516
510
|
return { ok: true, value: undefined };
|
|
517
511
|
if (!config.ok)
|
|
518
|
-
return
|
|
512
|
+
return failed(config.error);
|
|
519
513
|
this.config = config.value;
|
|
520
514
|
const session = await call(() => this.remote.getSession());
|
|
521
515
|
if (!this.current(generation))
|
|
522
516
|
return { ok: true, value: undefined };
|
|
523
517
|
if (!session.ok)
|
|
524
|
-
return
|
|
518
|
+
return failed(session.error);
|
|
525
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);
|
|
526
523
|
this.activatePresentationCache(identity);
|
|
527
524
|
this.publish({
|
|
528
|
-
...this.view,
|
|
525
|
+
...(sameOwner ? this.view : INITIAL_VIEW),
|
|
529
526
|
status: 'ready',
|
|
530
527
|
sessionStatus: session.value.status,
|
|
531
528
|
identity: session.value.status === 'active' ? session.value.identity : null,
|
|
532
529
|
error: null,
|
|
533
530
|
attachmentMaxBytes: config.value.attachmentMaxBytes,
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
recoveryOperationId: storedRecoveryOperation(config.value.tenantDomain),
|
|
531
|
+
handleRecoveryPhoneEnabled: config.value.handleRecoveryPhoneEnabled,
|
|
532
|
+
recoveryOperationId: previousTenant === config.value.tenantId ? previous.recoveryOperationId : null,
|
|
537
533
|
});
|
|
538
|
-
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
storeRecoveryOperation(config.value.tenantDomain, null);
|
|
545
|
-
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: recovery.value });
|
|
546
|
-
if (identity === null)
|
|
547
|
-
return this.loadSession();
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
}
|
|
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();
|
|
551
540
|
if (identity !== null) {
|
|
552
541
|
await this.loadConversationPreferences(generation);
|
|
553
542
|
const profile = await call(() => this.remote.getProfile());
|
|
@@ -561,19 +550,42 @@ export class AwikiController {
|
|
|
561
550
|
* @returns successful readiness or one display-safe Host failure.
|
|
562
551
|
*/
|
|
563
552
|
async open() {
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
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');
|
|
567
571
|
const generation = this.generation;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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);
|
|
572
582
|
}
|
|
573
|
-
if (this.current(generation)) {
|
|
583
|
+
if (this.current(generation) && this.drawerOpen) {
|
|
584
|
+
if (this.timer !== undefined)
|
|
585
|
+
clearInterval(this.timer);
|
|
574
586
|
this.timer = setInterval(() => { void this.poll(generation); }, this.config?.pollIntervalMs ?? 3_000);
|
|
575
587
|
}
|
|
576
|
-
return
|
|
588
|
+
return listed;
|
|
577
589
|
}
|
|
578
590
|
/** Sign out locally while retaining the SDK-owned identity and database. */
|
|
579
591
|
async logout(request) {
|
|
@@ -582,6 +594,7 @@ export class AwikiController {
|
|
|
582
594
|
const result = await call(() => this.remote.logout(request));
|
|
583
595
|
if (!result.ok)
|
|
584
596
|
return result;
|
|
597
|
+
this.drafts.clearScope();
|
|
585
598
|
this.close();
|
|
586
599
|
this.conversationsCursor = undefined;
|
|
587
600
|
this.historyCursor = undefined;
|
|
@@ -592,7 +605,9 @@ export class AwikiController {
|
|
|
592
605
|
status: 'ready',
|
|
593
606
|
sessionStatus: 'signed-out',
|
|
594
607
|
attachmentMaxBytes: this.config?.attachmentMaxBytes ?? 0,
|
|
608
|
+
handleRecoveryPhoneEnabled: this.config?.handleRecoveryPhoneEnabled ?? false,
|
|
595
609
|
});
|
|
610
|
+
await this.loadSession();
|
|
596
611
|
return result;
|
|
597
612
|
}
|
|
598
613
|
/** Resume the preserved local identity and reload its conversations. */
|
|
@@ -609,7 +624,52 @@ export class AwikiController {
|
|
|
609
624
|
}
|
|
610
625
|
/** Stop polling and invalidate all in-flight drawer work. */
|
|
611
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() {
|
|
612
671
|
this.generation += 1;
|
|
672
|
+
this.accessRequest = undefined;
|
|
613
673
|
this.selectionRevision += 1;
|
|
614
674
|
if (this.timer !== undefined)
|
|
615
675
|
clearInterval(this.timer);
|
|
@@ -626,6 +686,101 @@ export class AwikiController {
|
|
|
626
686
|
async sendRegistrationOtp(request) {
|
|
627
687
|
return this.withPending('发送验证码', () => call(() => this.remote.sendRegistrationOtp(request), registrationOtpFailureMessage));
|
|
628
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
|
+
}
|
|
629
784
|
/** Classify one Handle before sending exactly one registration or recovery OTP. */
|
|
630
785
|
async inspectIdentityAccess(request) {
|
|
631
786
|
return this.withPending('检查身份', () => call(() => this.remote.inspectIdentityAccess(request), identityAccessInspectionFailureMessage));
|
|
@@ -638,15 +793,82 @@ export class AwikiController {
|
|
|
638
793
|
async registerIdentity(request) {
|
|
639
794
|
const generation = this.generation;
|
|
640
795
|
const result = await this.withPending('注册身份', () => call(() => this.remote.registerIdentity(request), registrationFailureMessage));
|
|
641
|
-
if (!result.ok)
|
|
796
|
+
if (!result.ok) {
|
|
797
|
+
if (this.current(generation))
|
|
798
|
+
await this.refreshIdentityAccess();
|
|
642
799
|
return result;
|
|
800
|
+
}
|
|
643
801
|
if (!this.current(generation))
|
|
644
802
|
return result;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
803
|
+
if (result.value.status === 'join-required') {
|
|
804
|
+
this.publish({ ...this.view, identityAccess: { choice: result.value, joining: false, recoveries: [] } });
|
|
805
|
+
}
|
|
806
|
+
if (result.value.status === 'registered') {
|
|
807
|
+
this.drafts.clearScope();
|
|
808
|
+
this.drafts.setScope(this.config?.tenantId ?? 'default', result.value.identity.did);
|
|
809
|
+
this.activatePresentationCache(result.value.identity);
|
|
810
|
+
this.publish({ ...this.view, sessionStatus: 'active', identity: result.value.identity, error: null });
|
|
811
|
+
await this.refreshConversations(generation);
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
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;
|
|
821
|
+
}
|
|
822
|
+
async getDeviceJoinStatus() {
|
|
823
|
+
const generation = this.generation;
|
|
824
|
+
const result = await call(() => this.remote.getDeviceJoinStatus());
|
|
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
|
+
}
|
|
648
831
|
return result;
|
|
649
832
|
}
|
|
833
|
+
cancelDeviceJoin() {
|
|
834
|
+
return this.withPending('取消加入设备', async () => {
|
|
835
|
+
const generation = this.generation;
|
|
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: [] } });
|
|
839
|
+
return result.ok ? { ok: true, value: undefined } : result;
|
|
840
|
+
});
|
|
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
|
+
}
|
|
851
|
+
refreshDeviceManagement() {
|
|
852
|
+
return call(() => this.remote.refreshDeviceManagement());
|
|
853
|
+
}
|
|
854
|
+
startDeviceJoinVerification(request) {
|
|
855
|
+
return this.withPending('开始设备验证', () => call(() => this.remote.startDeviceJoinVerification(request)));
|
|
856
|
+
}
|
|
857
|
+
approveDeviceJoin(request) {
|
|
858
|
+
return this.withPending('批准设备', () => call(() => this.remote.approveDeviceJoin(request)));
|
|
859
|
+
}
|
|
860
|
+
rejectDeviceJoin(request) {
|
|
861
|
+
return this.withPending('拒绝设备', () => call(() => this.remote.rejectDeviceJoin(request)));
|
|
862
|
+
}
|
|
863
|
+
revokeDevice(request) {
|
|
864
|
+
return this.withPending('撤销设备', () => call(() => this.remote.revokeDevice(request)));
|
|
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
|
+
}
|
|
650
872
|
/**
|
|
651
873
|
* Update the deployment identity's public display name.
|
|
652
874
|
* @param displayName - replacement display name selected by the user.
|
|
@@ -686,105 +908,101 @@ export class AwikiController {
|
|
|
686
908
|
this.publish({ ...this.view, identity, profile: result.value, error: null });
|
|
687
909
|
return result;
|
|
688
910
|
}
|
|
689
|
-
/** Request a
|
|
911
|
+
/** Request a dedicated OTP only within the selected account flow. */
|
|
690
912
|
async sendRecoveryOtp(request) {
|
|
691
|
-
const
|
|
692
|
-
|
|
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);
|
|
693
920
|
return result;
|
|
694
|
-
|
|
921
|
+
}
|
|
695
922
|
this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null });
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
/** Verify the recovery OTP without attempting the remote identity mutation yet. */
|
|
699
|
-
async prepareRecovery(request) {
|
|
700
|
-
const operationId = this.view.recoveryOperationId;
|
|
701
|
-
if (operationId === null)
|
|
702
|
-
return this.fail('请先获取恢复验证码');
|
|
703
|
-
const result = await this.withPending('验证恢复信息', () => call(() => this.remote.prepareRecovery({ ...request, operationId }), recoveryPreparationFailureMessage, recoveryCarrierFailureMessage), { publishFailure: false });
|
|
704
|
-
if (result.ok)
|
|
705
|
-
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
706
|
-
return result;
|
|
923
|
+
await this.refreshRecoveryStatus();
|
|
924
|
+
return this.recoveryCurrent(generation, revision) ? result : this.recoveryLeft();
|
|
707
925
|
}
|
|
708
|
-
/**
|
|
709
|
-
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;
|
|
710
930
|
const operationId = this.view.recoveryOperationId;
|
|
711
931
|
if (operationId === null)
|
|
712
|
-
return
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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 });
|
|
718
943
|
return result;
|
|
719
|
-
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
720
|
-
if (result.value.phase === 'applied') {
|
|
721
|
-
storeRecoveryOperation(this.config?.tenantDomain, null);
|
|
722
|
-
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
|
|
723
|
-
await this.open();
|
|
724
944
|
}
|
|
945
|
+
this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
|
|
946
|
+
if (enterWhenApplied && result.value.phase === 'applied')
|
|
947
|
+
return this.enterRecoveredSession();
|
|
725
948
|
return result;
|
|
726
949
|
}
|
|
727
|
-
|
|
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. */
|
|
728
962
|
async refreshRecoveryStatus() {
|
|
729
|
-
|
|
730
|
-
if (operationId === null)
|
|
731
|
-
return this.fail('没有可查询的身份恢复操作');
|
|
732
|
-
const result = await this.withPending('刷新恢复状态', () => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage), { publishFailure: false });
|
|
733
|
-
if (!result.ok)
|
|
734
|
-
return result;
|
|
735
|
-
this.publish({ ...this.view, recoveryProgress: result.value });
|
|
736
|
-
if (result.value.phase === 'applied') {
|
|
737
|
-
storeRecoveryOperation(this.config?.tenantDomain, null);
|
|
738
|
-
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
|
|
739
|
-
await this.open();
|
|
740
|
-
}
|
|
741
|
-
return result;
|
|
963
|
+
return this.runRecovery('刷新恢复状态', operationId => call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage));
|
|
742
964
|
}
|
|
743
|
-
/** Resume only a Core-declared retryable or uncertain phase. */
|
|
744
965
|
async resumeRecovery() {
|
|
745
|
-
const operationId = this.view.recoveryOperationId;
|
|
746
966
|
const progress = this.view.recoveryProgress;
|
|
747
|
-
if (
|
|
748
|
-
return
|
|
749
|
-
|
|
750
|
-
|
|
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: '当前恢复身份尚不可进入,请重新检查。' };
|
|
751
977
|
}
|
|
752
|
-
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();
|
|
753
981
|
if (!result.ok)
|
|
754
982
|
return result;
|
|
755
|
-
this.
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: result.value });
|
|
759
|
-
await this.open();
|
|
760
|
-
}
|
|
761
|
-
return result;
|
|
983
|
+
this.leaveRecovery();
|
|
984
|
+
const opened = await this.open();
|
|
985
|
+
return opened.ok ? result : opened;
|
|
762
986
|
}
|
|
763
|
-
/**
|
|
987
|
+
/** Only Core-authorized pre-attempt cancellation destroys the pending operation. */
|
|
764
988
|
async discardRecovery() {
|
|
989
|
+
const generation = this.generation;
|
|
990
|
+
const revision = this.recoveryRevision;
|
|
765
991
|
const operationId = this.view.recoveryOperationId;
|
|
766
992
|
if (operationId === null)
|
|
767
993
|
return { ok: true, value: undefined };
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
return this.fail('当前恢复状态不能取消');
|
|
994
|
+
if (!this.view.recoveryProgress?.allowedActions?.includes('discard_pre_attempt')) {
|
|
995
|
+
return { ok: false, error: '当前恢复状态不能取消;可以返回入口,稍后继续。' };
|
|
771
996
|
}
|
|
772
|
-
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();
|
|
773
1000
|
if (!result.ok)
|
|
774
1001
|
return result;
|
|
775
|
-
|
|
776
|
-
this.
|
|
1002
|
+
this.leaveRecovery();
|
|
1003
|
+
void this.refreshIdentityAccess();
|
|
777
1004
|
return { ok: true, value: undefined };
|
|
778
1005
|
}
|
|
779
|
-
/** Return cached browser-safe runtime policy, or load it once on demand. */
|
|
780
|
-
async getConfig() {
|
|
781
|
-
if (this.config !== null)
|
|
782
|
-
return { ok: true, value: this.config };
|
|
783
|
-
const result = await call(() => this.remote.getConfig(), mailFailureMessage);
|
|
784
|
-
if (result.ok)
|
|
785
|
-
this.config = result.value;
|
|
786
|
-
return result;
|
|
787
|
-
}
|
|
788
1006
|
/** Read the active deployment identity's public mailbox state. */
|
|
789
1007
|
getMailAccount() {
|
|
790
1008
|
return call(() => this.remote.getMailAccount(), mailFailureMessage);
|
|
@@ -805,16 +1023,6 @@ export class AwikiController {
|
|
|
805
1023
|
sendMail(request) {
|
|
806
1024
|
return call(() => this.remote.sendMail(request), mailFailureMessage);
|
|
807
1025
|
}
|
|
808
|
-
/** Download one explicitly selected mail attachment without retaining its bytes. */
|
|
809
|
-
async downloadMailAttachment(request) {
|
|
810
|
-
if (this.disposed)
|
|
811
|
-
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
812
|
-
const generation = this.generation;
|
|
813
|
-
const result = await call(() => this.remote.downloadMailAttachment(request), mailFailureMessage);
|
|
814
|
-
if (!this.current(generation))
|
|
815
|
-
return { ok: false, error: 'AWiki 已关闭' };
|
|
816
|
-
return result;
|
|
817
|
-
}
|
|
818
1026
|
/**
|
|
819
1027
|
* Load another page of the conversation roster.
|
|
820
1028
|
* @returns successful pagination or one display-safe failure.
|
|
@@ -834,33 +1042,10 @@ export class AwikiController {
|
|
|
834
1042
|
...this.view,
|
|
835
1043
|
conversations: appendUnique(this.view.conversations, conversations.visible, value => value.id),
|
|
836
1044
|
hiddenConversations: conversations.hidden,
|
|
837
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
838
1045
|
conversationsHasMore: result.value.hasMore && result.value.nextCursor !== undefined,
|
|
839
1046
|
});
|
|
840
1047
|
return { ok: true, value: undefined };
|
|
841
1048
|
}
|
|
842
|
-
/** Synchronize account projections, then retry Core's durable group-rebind journal. */
|
|
843
|
-
async retryGroupRebindRecovery() {
|
|
844
|
-
const generation = this.generation;
|
|
845
|
-
return this.withPending('恢复群聊身份', async () => {
|
|
846
|
-
const listed = await this.refreshConversations(generation, true);
|
|
847
|
-
if (!listed.ok)
|
|
848
|
-
return { ok: false, error: listed.error };
|
|
849
|
-
return this.resumeGroupRebindRecovery(generation);
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
/** Dismiss only the current recovery-summary revision; Core recovery remains untouched. */
|
|
853
|
-
async dismissGroupRecoveryNotice() {
|
|
854
|
-
const signature = this.lastGroupRecoverySignature;
|
|
855
|
-
if (signature === undefined || this.view.groupRecovery === null)
|
|
856
|
-
return { ok: true, value: undefined };
|
|
857
|
-
const result = await call(() => this.remote.updateConversationPreference({ action: 'dismiss-group-recovery', signature }), conversationPreferenceFailureMessage);
|
|
858
|
-
if (!result.ok)
|
|
859
|
-
return this.fail(result.error);
|
|
860
|
-
this.applyConversationPreferences(result.value);
|
|
861
|
-
this.publish({ ...this.view, groupRecovery: null, error: null });
|
|
862
|
-
return { ok: true, value: undefined };
|
|
863
|
-
}
|
|
864
1049
|
/** Hide one recent row locally without leaving a group or deleting history. */
|
|
865
1050
|
async hideConversation(conversationId) {
|
|
866
1051
|
const conversation = this.view.conversations.find(item => item.id === conversationId);
|
|
@@ -896,7 +1081,6 @@ export class AwikiController {
|
|
|
896
1081
|
refreshing: false,
|
|
897
1082
|
}
|
|
898
1083
|
: {}),
|
|
899
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
900
1084
|
error: null,
|
|
901
1085
|
});
|
|
902
1086
|
return { ok: true, value: undefined };
|
|
@@ -918,7 +1102,6 @@ export class AwikiController {
|
|
|
918
1102
|
...this.view,
|
|
919
1103
|
conversations: appendUnique([conversation], this.view.conversations, item => item.id),
|
|
920
1104
|
hiddenConversations: this.hiddenConversationsView(),
|
|
921
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
922
1105
|
error: null,
|
|
923
1106
|
});
|
|
924
1107
|
return { ok: true, value: undefined };
|
|
@@ -1074,9 +1257,6 @@ export class AwikiController {
|
|
|
1074
1257
|
const generation = this.generation;
|
|
1075
1258
|
this.publish({ ...this.view, pending: '刷新群成员', error: null });
|
|
1076
1259
|
const result = await (async () => {
|
|
1077
|
-
if (this.view.groupAccess?.status !== 'available' && this.view.groupAccess?.status !== 'loading') {
|
|
1078
|
-
await this.resumeGroupRebindRecovery(this.generation);
|
|
1079
|
-
}
|
|
1080
1260
|
const refreshed = await this.loadGroupState(conversation, true, true);
|
|
1081
1261
|
return refreshed.ok ? { ok: true, value: undefined } : refreshed;
|
|
1082
1262
|
})();
|
|
@@ -1613,28 +1793,90 @@ export class AwikiController {
|
|
|
1613
1793
|
}
|
|
1614
1794
|
return result;
|
|
1615
1795
|
}
|
|
1616
|
-
/** Clear
|
|
1796
|
+
/** Clear current-tenant local data and only its owned browser projections. */
|
|
1617
1797
|
async clearLocalData(request) {
|
|
1618
1798
|
if (this.disposed)
|
|
1619
1799
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
1800
|
+
const generation = this.generation;
|
|
1801
|
+
const ownerDid = this.presentationCacheOwnerDid;
|
|
1802
|
+
const tenantId = this.config?.tenantId;
|
|
1620
1803
|
const result = await call(() => this.remote.clearLocalData(request));
|
|
1621
1804
|
if (!result.ok)
|
|
1622
1805
|
return result;
|
|
1623
|
-
|
|
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();
|
|
1624
1818
|
this.close();
|
|
1625
|
-
const tenantDomain = this.config?.tenantDomain;
|
|
1626
1819
|
this.config = null;
|
|
1627
1820
|
this.conversationsCursor = undefined;
|
|
1628
1821
|
this.historyCursor = undefined;
|
|
1629
1822
|
this.unreadAtOpen.clear();
|
|
1630
1823
|
this.summaryBaselines.clear();
|
|
1631
1824
|
this.clearPresentationCache();
|
|
1632
|
-
|
|
1633
|
-
this.publish({ ...INITIAL_VIEW, status: 'ready' });
|
|
1825
|
+
this.publish({ ...INITIAL_VIEW, status: 'ready', accessLoading: false, identityAccess: { choice: null, joining: false, recoveries: [] } });
|
|
1634
1826
|
return result;
|
|
1635
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
|
+
}
|
|
1636
1877
|
/** Stop timers, invalidate work, and drop subscribers during HMR unload. */
|
|
1637
1878
|
dispose() {
|
|
1879
|
+
this.drafts.clear();
|
|
1638
1880
|
this.disposed = true;
|
|
1639
1881
|
this.close();
|
|
1640
1882
|
this.listeners.clear();
|
|
@@ -1654,25 +1896,12 @@ export class AwikiController {
|
|
|
1654
1896
|
hiddenAt: hidden.hiddenAt,
|
|
1655
1897
|
});
|
|
1656
1898
|
}
|
|
1657
|
-
this.dismissedGroupRecoverySignature = preferences.dismissedGroupRecoverySignature;
|
|
1658
1899
|
}
|
|
1659
1900
|
hiddenConversationsView() {
|
|
1660
1901
|
return [...this.hiddenConversationPreferences.values()]
|
|
1661
1902
|
.sort((left, right) => right.hiddenAt - left.hiddenAt)
|
|
1662
1903
|
.map(item => item.conversation);
|
|
1663
1904
|
}
|
|
1664
|
-
currentGroupRecoveryView() {
|
|
1665
|
-
const summary = this.lastGroupRecoverySummary;
|
|
1666
|
-
if (summary === undefined)
|
|
1667
|
-
return this.view.groupRecovery;
|
|
1668
|
-
const hiddenGroupDids = new Set([...this.hiddenConversationPreferences.values()]
|
|
1669
|
-
.map(item => item.conversation)
|
|
1670
|
-
.filter((conversation) => conversation.kind === 'group')
|
|
1671
|
-
.map(conversation => conversation.groupDid));
|
|
1672
|
-
const projection = groupRecoveryProjection(summary, hiddenGroupDids);
|
|
1673
|
-
this.lastGroupRecoverySignature = projection.signature;
|
|
1674
|
-
return projection.signature === this.dismissedGroupRecoverySignature ? null : projection.view;
|
|
1675
|
-
}
|
|
1676
1905
|
async reconcileConversationPage(incoming, generation) {
|
|
1677
1906
|
const current = [...this.view.conversations, ...this.view.hiddenConversations];
|
|
1678
1907
|
const conversations = incoming.map(item => this.cacheConversation(item, current.find(candidate => candidate.id === item.id)));
|
|
@@ -1723,7 +1952,6 @@ export class AwikiController {
|
|
|
1723
1952
|
? refreshed.visible
|
|
1724
1953
|
: appendUnique(refreshed.visible, this.view.conversations, value => value.id),
|
|
1725
1954
|
hiddenConversations: refreshed.hidden,
|
|
1726
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
1727
1955
|
conversationsHasMore: firstPage
|
|
1728
1956
|
? result.value.hasMore && result.value.nextCursor !== undefined
|
|
1729
1957
|
: this.view.conversationsHasMore,
|
|
@@ -1734,14 +1962,18 @@ export class AwikiController {
|
|
|
1734
1962
|
/** List the active identity's own conversations and detect a revoked local credential. */
|
|
1735
1963
|
async listConversationPage(request) {
|
|
1736
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
|
+
}
|
|
1737
1969
|
if (!result.ok && (result.failureCode === 'identity-recovery-required' || result.failureCode === 'forbidden')) {
|
|
1738
|
-
this.
|
|
1970
|
+
this.enterBlockedIdentityState('recovery-required');
|
|
1739
1971
|
return { ok: false, error: '当前设备的 AWiki 身份凭证已失效,请重新恢复身份。' };
|
|
1740
1972
|
}
|
|
1741
1973
|
return result.ok ? result : { ok: false, error: result.error };
|
|
1742
1974
|
}
|
|
1743
1975
|
/** Replace only visible browser projections; Core identity and SQLite state remain untouched. */
|
|
1744
|
-
|
|
1976
|
+
enterBlockedIdentityState(status) {
|
|
1745
1977
|
if (this.view.identity === null)
|
|
1746
1978
|
return;
|
|
1747
1979
|
this.close();
|
|
@@ -1750,14 +1982,11 @@ export class AwikiController {
|
|
|
1750
1982
|
this.groupMembersCursor = undefined;
|
|
1751
1983
|
this.unreadAtOpen.clear();
|
|
1752
1984
|
this.summaryBaselines.clear();
|
|
1753
|
-
this.groupRecoveryItems.clear();
|
|
1754
|
-
this.lastGroupRecoverySignature = undefined;
|
|
1755
|
-
this.lastGroupRecoverySummary = undefined;
|
|
1756
1985
|
this.clearImageAttachments();
|
|
1757
1986
|
this.publish({
|
|
1758
1987
|
...this.view,
|
|
1759
1988
|
status: 'ready',
|
|
1760
|
-
sessionStatus:
|
|
1989
|
+
sessionStatus: status,
|
|
1761
1990
|
conversations: Object.freeze([]),
|
|
1762
1991
|
hiddenConversations: Object.freeze([]),
|
|
1763
1992
|
conversationsHasMore: false,
|
|
@@ -1766,7 +1995,6 @@ export class AwikiController {
|
|
|
1766
1995
|
groupAccess: null,
|
|
1767
1996
|
groupMembers: Object.freeze([]),
|
|
1768
1997
|
groupMembersHasMore: false,
|
|
1769
|
-
groupRecovery: null,
|
|
1770
1998
|
messages: Object.freeze([]),
|
|
1771
1999
|
historyHasMore: false,
|
|
1772
2000
|
localPending: false,
|
|
@@ -1776,49 +2004,6 @@ export class AwikiController {
|
|
|
1776
2004
|
summaries: Object.freeze({}),
|
|
1777
2005
|
});
|
|
1778
2006
|
}
|
|
1779
|
-
/** Hydrate account-projected groups before asking Core to resume their durable rebind jobs. */
|
|
1780
|
-
async refreshConversationsAndRecoverGroups(generation, background = false) {
|
|
1781
|
-
const listed = await this.refreshConversations(generation, background);
|
|
1782
|
-
if (!listed.ok || !this.current(generation))
|
|
1783
|
-
return listed;
|
|
1784
|
-
await this.resumeGroupRebindRecovery(generation);
|
|
1785
|
-
return listed;
|
|
1786
|
-
}
|
|
1787
|
-
resumeGroupRebindRecovery(generation) {
|
|
1788
|
-
const active = this.groupRecoveryOperation;
|
|
1789
|
-
if (active !== undefined && active.generation === generation)
|
|
1790
|
-
return active.operation;
|
|
1791
|
-
let operation;
|
|
1792
|
-
operation = call(() => this.remote.resumeGroupRebindRecovery(), groupRecoveryFailureMessage).then((result) => {
|
|
1793
|
-
if (!this.current(generation))
|
|
1794
|
-
return result;
|
|
1795
|
-
if (result.ok) {
|
|
1796
|
-
this.lastGroupRecoverySummary = result.value;
|
|
1797
|
-
this.groupRecoveryItems.clear();
|
|
1798
|
-
for (const item of result.value.items) {
|
|
1799
|
-
this.groupRecoveryItems.set(item.groupDid, item.status);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
else {
|
|
1803
|
-
this.lastGroupRecoverySummary = undefined;
|
|
1804
|
-
this.lastGroupRecoverySignature = 'unavailable';
|
|
1805
|
-
}
|
|
1806
|
-
this.publish({
|
|
1807
|
-
...this.view,
|
|
1808
|
-
groupRecovery: result.ok
|
|
1809
|
-
? this.currentGroupRecoveryView()
|
|
1810
|
-
: this.dismissedGroupRecoverySignature === 'unavailable'
|
|
1811
|
-
? null
|
|
1812
|
-
: { status: 'unavailable', pending: 0, blocked: 0 },
|
|
1813
|
-
});
|
|
1814
|
-
return result;
|
|
1815
|
-
}).finally(() => {
|
|
1816
|
-
if (this.groupRecoveryOperation?.operation === operation)
|
|
1817
|
-
this.groupRecoveryOperation = undefined;
|
|
1818
|
-
});
|
|
1819
|
-
this.groupRecoveryOperation = { generation, operation };
|
|
1820
|
-
return operation;
|
|
1821
|
-
}
|
|
1822
2007
|
async loadHistory(older) {
|
|
1823
2008
|
const conversationId = this.view.selectedConversationId;
|
|
1824
2009
|
if (conversationId === null)
|
|
@@ -1862,13 +2047,7 @@ export class AwikiController {
|
|
|
1862
2047
|
return;
|
|
1863
2048
|
this.polling = true;
|
|
1864
2049
|
try {
|
|
1865
|
-
|
|
1866
|
-
.filter(conversation => conversation.kind === 'group')
|
|
1867
|
-
.map(conversation => conversation.groupDid));
|
|
1868
|
-
const listed = await this.refreshConversations(generation, true);
|
|
1869
|
-
if (listed.ok && this.view.conversations.some(conversation => conversation.kind === 'group' && !knownGroups.has(conversation.groupDid))) {
|
|
1870
|
-
await this.resumeGroupRebindRecovery(generation);
|
|
1871
|
-
}
|
|
2050
|
+
await this.refreshConversations(generation, true);
|
|
1872
2051
|
const selected = this.view.selectedConversationId;
|
|
1873
2052
|
if (selected === null || !this.current(generation))
|
|
1874
2053
|
return;
|
|
@@ -1882,6 +2061,19 @@ export class AwikiController {
|
|
|
1882
2061
|
this.publish({ ...this.view, error: 'AWiki 远端消息归属不一致,请重新打开会话。' });
|
|
1883
2062
|
return;
|
|
1884
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]));
|
|
1885
2077
|
const existingIds = new Set(this.view.messages.map(message => message.id));
|
|
1886
2078
|
const incoming = result.value.items.filter(message => !existingIds.has(message.id));
|
|
1887
2079
|
const messages = mergeLatestMessages(this.view.messages, result.value.items);
|
|
@@ -1891,7 +2083,22 @@ export class AwikiController {
|
|
|
1891
2083
|
}
|
|
1892
2084
|
this.publish({
|
|
1893
2085
|
...this.view,
|
|
1894
|
-
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
|
+
}),
|
|
1895
2102
|
summaries: this.staleSummaries(selected, incoming),
|
|
1896
2103
|
});
|
|
1897
2104
|
}
|
|
@@ -1899,20 +2106,34 @@ export class AwikiController {
|
|
|
1899
2106
|
this.polling = false;
|
|
1900
2107
|
}
|
|
1901
2108
|
}
|
|
2109
|
+
pendingRequest;
|
|
1902
2110
|
async withPending(label, operation, options = {}) {
|
|
1903
2111
|
if (this.disposed)
|
|
1904
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;
|
|
1905
2118
|
const generation = this.generation;
|
|
1906
2119
|
this.publish({ ...this.view, pending: label, error: null });
|
|
1907
|
-
|
|
1908
|
-
|
|
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
|
+
});
|
|
1909
2126
|
return result;
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
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
|
+
}
|
|
1916
2137
|
}
|
|
1917
2138
|
appendMessage(message) {
|
|
1918
2139
|
if (this.view.selectedConversationId !== message.conversationId)
|
|
@@ -1963,14 +2184,7 @@ export class AwikiController {
|
|
|
1963
2184
|
return selected === null ? undefined : this.view.conversations.find(value => value.id === selected);
|
|
1964
2185
|
}
|
|
1965
2186
|
publishGroupAccessFailure(conversation, reason) {
|
|
1966
|
-
const
|
|
1967
|
-
const status = reason === 'not-member'
|
|
1968
|
-
? journal === 'blocked'
|
|
1969
|
-
? 'blocked'
|
|
1970
|
-
: journal === 'pending'
|
|
1971
|
-
? 'recovering'
|
|
1972
|
-
: 'not-member'
|
|
1973
|
-
: reason;
|
|
2187
|
+
const status = reason;
|
|
1974
2188
|
this.publish({
|
|
1975
2189
|
...this.view,
|
|
1976
2190
|
groupAccess: { groupDid: conversation.groupDid, status },
|
|
@@ -1984,11 +2198,7 @@ export class AwikiController {
|
|
|
1984
2198
|
return;
|
|
1985
2199
|
this.directProfiles.clear();
|
|
1986
2200
|
this.groupTitles.clear();
|
|
1987
|
-
this.groupRecoveryItems.clear();
|
|
1988
2201
|
this.hiddenConversationPreferences.clear();
|
|
1989
|
-
this.dismissedGroupRecoverySignature = undefined;
|
|
1990
|
-
this.lastGroupRecoverySignature = undefined;
|
|
1991
|
-
this.lastGroupRecoverySummary = undefined;
|
|
1992
2202
|
this.clearImageAttachments();
|
|
1993
2203
|
this.presentationCacheOwnerDid = ownerDid;
|
|
1994
2204
|
}
|
|
@@ -1996,11 +2206,7 @@ export class AwikiController {
|
|
|
1996
2206
|
clearPresentationCache() {
|
|
1997
2207
|
this.directProfiles.clear();
|
|
1998
2208
|
this.groupTitles.clear();
|
|
1999
|
-
this.groupRecoveryItems.clear();
|
|
2000
2209
|
this.hiddenConversationPreferences.clear();
|
|
2001
|
-
this.dismissedGroupRecoverySignature = undefined;
|
|
2002
|
-
this.lastGroupRecoverySignature = undefined;
|
|
2003
|
-
this.lastGroupRecoverySummary = undefined;
|
|
2004
2210
|
this.clearImageAttachments();
|
|
2005
2211
|
this.presentationCacheOwnerDid = null;
|
|
2006
2212
|
}
|