@awiki/dsh-plugin 0.3.7 → 0.3.11-rc.1
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 +112 -73
- package/README.zh.md +91 -62
- 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 +9281 -2379
- package/lib/client.js.map +1 -1
- package/lib/index.js +3553 -1138
- package/lib/model-proxy-contract.js +19 -11
- 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 +17 -8
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +269 -37
- 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 +6 -1
- package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.js +74 -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 +2 -2
- 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 +92 -44
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +761 -334
- 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 +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +64 -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 +7 -0
- package/lib/types/client/otp-cooldown.d.ts.map +1 -0
- package/lib/types/client/otp-cooldown.js +29 -0
- package/lib/types/client/otp-cooldown.js.map +1 -0
- package/lib/types/client/settings-controller.d.ts +38 -2
- package/lib/types/client/settings-controller.d.ts.map +1 -1
- package/lib/types/client/settings-controller.js +206 -10
- 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 +21 -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/client/store.d.ts +1 -1
- package/lib/types/client/store.d.ts.map +1 -1
- package/lib/types/client/store.js +1 -1
- package/lib/types/client/store.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 +165 -41
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +1594 -535
- 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 +13 -128
- 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 +8 -8
- package/lib/types/model-proxy-contract.d.ts.map +1 -1
- package/lib/types/model-proxy-contract.js +15 -8
- 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/registration-policy.d.ts +7 -0
- package/lib/types/registration-policy.d.ts.map +1 -0
- package/lib/types/registration-policy.js +21 -0
- package/lib/types/registration-policy.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 +63 -5
- package/lib/types/settings-rpc-contract.d.ts.map +1 -1
- package/lib/types/settings-rpc-contract.js +108 -5
- 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 +132 -3
- package/lib/types/settings-rpc.js.map +1 -1
- package/lib/types/settings-transport.d.ts +7 -0
- package/lib/types/settings-transport.d.ts.map +1 -0
- package/lib/types/settings-transport.js +52 -0
- package/lib/types/settings-transport.js.map +1 -0
- 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 +88 -61
- 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,33 @@
|
|
|
1
|
-
|
|
1
|
+
import { AwikiDraftStore } from "./drafts.js";
|
|
2
|
+
import { SHORT_HANDLE_INVITE_MESSAGE } from "../registration-policy.js";
|
|
2
3
|
import { IndexedDbAwikiBrowserImageCache, } from "./image-cache.js";
|
|
4
|
+
import { clearMailBrowserCache } from "./mail-list-cache.js";
|
|
5
|
+
const GENERIC_BUSINESS_FAILURE_MESSAGE = 'AWiki 暂时无法完成此操作,请稍后重试。';
|
|
6
|
+
const GENERIC_CARRIER_FAILURE_MESSAGE = '暂时无法连接 AWiki Host,请稍后重试。';
|
|
7
|
+
const GENERIC_THROW_FAILURE_MESSAGE = 'AWiki 调用暂时失败,请稍后重试。';
|
|
8
|
+
/** Project only a stable business code into Browser copy; provider messages remain Host-only. */
|
|
9
|
+
function genericFailureMessage(failure) {
|
|
10
|
+
switch (failure.code) {
|
|
11
|
+
case 'network':
|
|
12
|
+
return '无法连接 AWiki 服务,请检查网络后重试。';
|
|
13
|
+
case 'rate-limited':
|
|
14
|
+
return 'AWiki 请求过于频繁,请稍后重试。';
|
|
15
|
+
case 'not-registered':
|
|
16
|
+
return '请先注册 AWiki 身份后重试。';
|
|
17
|
+
case 'signed-out':
|
|
18
|
+
return '当前 AWiki 身份已退出,请重新进入后重试。';
|
|
19
|
+
case 'identity-recovery-required':
|
|
20
|
+
return '当前设备的 AWiki 身份凭证已失效,请重新恢复身份。';
|
|
21
|
+
case 'device-rejoin-required':
|
|
22
|
+
return '当前设备已被撤销,请重新申请加入。';
|
|
23
|
+
default:
|
|
24
|
+
return GENERIC_BUSINESS_FAILURE_MESSAGE;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Carrier diagnostics are not a Browser contract, including for unknown carrier codes. */
|
|
28
|
+
function genericCarrierFailureMessage(_failure) {
|
|
29
|
+
return GENERIC_CARRIER_FAILURE_MESSAGE;
|
|
30
|
+
}
|
|
3
31
|
/** Turn a registration rejection into an actionable message without exposing remote response text. */
|
|
4
32
|
function registrationFailureMessage(failure) {
|
|
5
33
|
switch (failure.code) {
|
|
@@ -13,23 +41,29 @@ function registrationFailureMessage(failure) {
|
|
|
13
41
|
return '验证码状态已失效,请重新获取验证码后再注册。';
|
|
14
42
|
case 'handle-unavailable':
|
|
15
43
|
return '该 Handle 刚刚已被注册。请返回身份入口,再按恢复流程重新获取验证码。';
|
|
44
|
+
case 'short-handle-invite-required':
|
|
45
|
+
return SHORT_HANDLE_INVITE_MESSAGE;
|
|
16
46
|
case 'conflict':
|
|
17
47
|
return '注册冲突:服务端可能已收到上次注册请求,或该手机号 / Handle 已绑定其他身份。请保留当前页面并再次提交;若仍失败,请勿清除本机身份数据,联系管理员并提供失败时间。';
|
|
18
48
|
case 'rate-limited':
|
|
19
49
|
return '注册请求过于频繁,请稍后重试。';
|
|
20
50
|
case 'network':
|
|
21
51
|
return '无法连接 AWiki 服务,请检查网络后重试。';
|
|
52
|
+
case 'identity-recovery-required':
|
|
53
|
+
return '这台设备保留了原 AWiki 身份,但本地登录状态已被清除。请先恢复该身份或完整清除这台设备上的 AWiki 身份数据,再重新注册。';
|
|
22
54
|
case 'forbidden':
|
|
23
55
|
return '当前 AWiki 服务未开放公开注册,或该手机号不在注册白名单。请使用已获准的手机号,或联系管理员开通注册权限。';
|
|
24
56
|
case 'remote':
|
|
25
57
|
return 'AWiki 服务暂时无法完成注册,请稍后重试;若持续失败,请联系管理员并提供失败时间。';
|
|
26
58
|
default:
|
|
27
|
-
return
|
|
59
|
+
return 'AWiki 服务暂时无法完成注册,请稍后重试;若持续失败,请联系管理员并提供失败时间。';
|
|
28
60
|
}
|
|
29
61
|
}
|
|
30
62
|
/** Turn a verification-code request failure into a safe next action. */
|
|
31
63
|
function registrationOtpFailureMessage(failure) {
|
|
32
64
|
switch (failure.code) {
|
|
65
|
+
case 'short-handle-invite-required':
|
|
66
|
+
return SHORT_HANDLE_INVITE_MESSAGE;
|
|
33
67
|
case 'rate-limited':
|
|
34
68
|
return '验证码发送过于频繁,请等待限流解除后再重新获取。';
|
|
35
69
|
case 'invalid-request':
|
|
@@ -59,13 +93,13 @@ function recoveryPreparationFailureMessage(failure) {
|
|
|
59
93
|
case 'remote':
|
|
60
94
|
return 'AWiki 服务暂时无法验证恢复信息,请稍后重试。';
|
|
61
95
|
default:
|
|
62
|
-
return
|
|
96
|
+
return 'AWiki 服务暂时无法验证恢复信息,请稍后重试。';
|
|
63
97
|
}
|
|
64
98
|
}
|
|
65
99
|
function recoveryContinuationFailureMessage(failure, phase) {
|
|
66
100
|
const remoteCommitted = phase === 'remote_committed' || phase === 'identity_transition_pending';
|
|
67
101
|
if (remoteCommitted && ['invalid-request', 'conflict', 'forbidden', 'remote'].includes(failure.code)) {
|
|
68
|
-
return '
|
|
102
|
+
return '身份已在服务端恢复,本机切换尚未完成。可以重试本机切换,也可以返回入口稍后继续;请勿为同一账号重复发起恢复。';
|
|
69
103
|
}
|
|
70
104
|
switch (failure.code) {
|
|
71
105
|
case 'network':
|
|
@@ -74,11 +108,14 @@ function recoveryContinuationFailureMessage(failure, phase) {
|
|
|
74
108
|
: '恢复请求结果尚未确认。请保留当前恢复操作并重新检查状态,不要重复提交。';
|
|
75
109
|
case 'invalid-request':
|
|
76
110
|
case 'conflict':
|
|
77
|
-
return '
|
|
111
|
+
return '身份恢复暂未完成。请重新检查状态,或返回入口稍后继续;请勿为同一账号重复发起恢复。';
|
|
78
112
|
case 'rate-limited':
|
|
79
113
|
return '恢复状态检查过于频繁,请稍后继续。';
|
|
114
|
+
case 'remote':
|
|
115
|
+
case 'forbidden':
|
|
116
|
+
return 'AWiki 服务暂时未能完成恢复。可以重新检查结果,或返回入口稍后继续。';
|
|
80
117
|
default:
|
|
81
|
-
return
|
|
118
|
+
return '身份恢复暂未完成。请保留当前恢复操作并重新检查状态,不要重新获取验证码或创建新身份。';
|
|
82
119
|
}
|
|
83
120
|
}
|
|
84
121
|
function identityAccessInspectionFailureMessage(failure) {
|
|
@@ -90,9 +127,10 @@ function identityAccessInspectionFailureMessage(failure) {
|
|
|
90
127
|
case 'remote':
|
|
91
128
|
return 'AWiki 服务暂时无法确认该 Handle 的状态,请稍后重试。';
|
|
92
129
|
default:
|
|
93
|
-
return
|
|
130
|
+
return 'AWiki 服务暂时无法确认该 Handle 的状态,请稍后重试。';
|
|
94
131
|
}
|
|
95
132
|
}
|
|
133
|
+
const IDENTITY_ACCESS_FAILURE_MESSAGE = '暂时无法读取本机身份状态,请稍后重新检查。';
|
|
96
134
|
const INITIAL_VIEW = Object.freeze({
|
|
97
135
|
status: 'cold',
|
|
98
136
|
sessionStatus: 'unregistered',
|
|
@@ -106,7 +144,6 @@ const INITIAL_VIEW = Object.freeze({
|
|
|
106
144
|
groupAccess: null,
|
|
107
145
|
groupMembers: Object.freeze([]),
|
|
108
146
|
groupMembersHasMore: false,
|
|
109
|
-
groupRecovery: null,
|
|
110
147
|
messages: Object.freeze([]),
|
|
111
148
|
historyHasMore: false,
|
|
112
149
|
localPending: false,
|
|
@@ -114,11 +151,14 @@ const INITIAL_VIEW = Object.freeze({
|
|
|
114
151
|
pending: null,
|
|
115
152
|
error: null,
|
|
116
153
|
attachmentMaxBytes: 0,
|
|
117
|
-
|
|
118
|
-
legacySharedStateDetected: false,
|
|
154
|
+
handleRecoveryPhoneEnabled: false,
|
|
119
155
|
summaries: Object.freeze({}),
|
|
156
|
+
identityAccess: null,
|
|
157
|
+
accessLoading: true,
|
|
158
|
+
accessError: null,
|
|
120
159
|
recoveryOperationId: null,
|
|
121
160
|
recoveryProgress: null,
|
|
161
|
+
recoveryOtpRetryAt: null,
|
|
122
162
|
});
|
|
123
163
|
/** Turn a closed Host summary failure into one actionable Chinese message. */
|
|
124
164
|
function summaryFailureMessage(failure) {
|
|
@@ -128,7 +168,7 @@ function summaryFailureMessage(failure) {
|
|
|
128
168
|
case 'summary-cancelled': return 'AI 总结已取消,请重新生成。';
|
|
129
169
|
case 'summary-invalid-output': return '模型没有返回有效的结构化摘要,请重新生成。';
|
|
130
170
|
case 'summary-failed': return '暂时无法生成 AI 总结,请检查模型连接后重试。';
|
|
131
|
-
default: return
|
|
171
|
+
default: return '暂时无法生成 AI 总结,请检查模型连接后重试。';
|
|
132
172
|
}
|
|
133
173
|
}
|
|
134
174
|
/** Turn closed mail failures into safe, actionable browser messages. */
|
|
@@ -156,7 +196,7 @@ function groupCreateFailureMessage(failure) {
|
|
|
156
196
|
case 'network': return '无法连接 AWiki 服务,请检查网络后重试。';
|
|
157
197
|
case 'conflict': return '群聊创建状态发生冲突,请刷新群聊列表后确认是否已经创建。';
|
|
158
198
|
case 'remote': return 'AWiki 服务暂时无法创建群聊,请稍后重试。';
|
|
159
|
-
default: return
|
|
199
|
+
default: return 'AWiki 服务暂时无法创建群聊,请稍后重试。';
|
|
160
200
|
}
|
|
161
201
|
}
|
|
162
202
|
/** Keep group authorization failures actionable without exposing raw service text. */
|
|
@@ -175,47 +215,12 @@ function groupReadFailureMessage(failure) {
|
|
|
175
215
|
case 'remote':
|
|
176
216
|
return 'AWiki 暂时无法读取这个群聊,请稍后重试。';
|
|
177
217
|
default:
|
|
178
|
-
return
|
|
218
|
+
return 'AWiki 暂时无法读取这个群聊,请稍后重试。';
|
|
179
219
|
}
|
|
180
220
|
}
|
|
181
|
-
function groupRecoveryFailureMessage(failure) {
|
|
182
|
-
return failure.code === 'network'
|
|
183
|
-
? '无法连接 AWiki 服务,暂时不能恢复旧群聊身份。'
|
|
184
|
-
: '暂时不能检查旧群聊身份,请稍后重试。';
|
|
185
|
-
}
|
|
186
221
|
function conversationPreferenceFailureMessage(_failure) {
|
|
187
222
|
return '无法保存本机会话设置,请稍后重试。';
|
|
188
223
|
}
|
|
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
224
|
function groupReadFailureReason(failure) {
|
|
220
225
|
switch (failure.code) {
|
|
221
226
|
case 'group-identity-stale': return 'recovering';
|
|
@@ -245,49 +250,70 @@ async function callGroupRead(operation) {
|
|
|
245
250
|
}
|
|
246
251
|
}
|
|
247
252
|
/** Flatten the carrier and business result once for every controller caller. */
|
|
248
|
-
async function call(operation, failureMessage =
|
|
253
|
+
async function call(operation, failureMessage = genericFailureMessage, carrierFailureMessage = genericCarrierFailureMessage) {
|
|
249
254
|
try {
|
|
250
255
|
const carried = await operation();
|
|
251
256
|
if (!carried.ok)
|
|
252
|
-
return { ok: false, error: carrierFailureMessage(carried.error
|
|
257
|
+
return { ok: false, error: carrierFailureMessage(carried.error) };
|
|
253
258
|
if (!carried.value.ok) {
|
|
254
259
|
return { ok: false, error: failureMessage(carried.value.error) };
|
|
255
260
|
}
|
|
256
261
|
return { ok: true, value: carried.value.value };
|
|
257
262
|
}
|
|
258
|
-
catch
|
|
259
|
-
return {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
catch {
|
|
264
|
+
return { ok: false, error: GENERIC_THROW_FAILURE_MESSAGE };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Flatten the carrier and the isolated Guest Integration business result. */
|
|
268
|
+
async function callIntegration(operation) {
|
|
269
|
+
try {
|
|
270
|
+
const carried = await operation();
|
|
271
|
+
if (!carried.ok)
|
|
272
|
+
return { ok: false, error: genericCarrierFailureMessage(carried.error) };
|
|
273
|
+
if (!carried.value.ok) {
|
|
274
|
+
const failure = carried.value.error;
|
|
275
|
+
const error = failure.code === 'not-found'
|
|
276
|
+
? '尚未创建 Integration。'
|
|
277
|
+
: failure.code === 'conflict'
|
|
278
|
+
? 'Integration 已在其他位置更新,请重新加载后再试。'
|
|
279
|
+
: failure.code === 'invalid-request'
|
|
280
|
+
? 'Integration 信息不完整或格式不正确。'
|
|
281
|
+
: failure.code === 'unauthorized' || failure.code === 'forbidden'
|
|
282
|
+
? '当前 AWiki 身份无权管理此 Integration。'
|
|
283
|
+
: failure.code === 'rate-limited'
|
|
284
|
+
? 'Integration 操作过于频繁,请稍后重试。'
|
|
285
|
+
: '临时消息服务暂时不可用,请稍后重试。';
|
|
286
|
+
return { ok: false, error, failureCode: failure.code };
|
|
287
|
+
}
|
|
288
|
+
return { ok: true, value: carried.value.value };
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return { ok: false, error: '临时消息服务暂时不可用,请稍后重试。' };
|
|
263
292
|
}
|
|
264
293
|
}
|
|
265
294
|
/** Preserve only the stable business code for controller-level state transitions. */
|
|
266
|
-
async function callWithFailureCode(operation) {
|
|
295
|
+
async function callWithFailureCode(operation, failureMessage = genericFailureMessage) {
|
|
267
296
|
try {
|
|
268
297
|
const carried = await operation();
|
|
269
298
|
if (!carried.ok)
|
|
270
|
-
return { ok: false, error:
|
|
299
|
+
return { ok: false, error: genericCarrierFailureMessage(carried.error) };
|
|
271
300
|
if (!carried.value.ok) {
|
|
272
301
|
return {
|
|
273
302
|
ok: false,
|
|
274
|
-
error:
|
|
303
|
+
error: failureMessage(carried.value.error),
|
|
275
304
|
failureCode: carried.value.error.code,
|
|
276
305
|
};
|
|
277
306
|
}
|
|
278
307
|
return { ok: true, value: carried.value.value };
|
|
279
308
|
}
|
|
280
|
-
catch
|
|
281
|
-
return {
|
|
282
|
-
ok: false,
|
|
283
|
-
error: error instanceof Error ? `AWiki 调用失败:${error.message}` : 'AWiki 调用失败',
|
|
284
|
-
};
|
|
309
|
+
catch {
|
|
310
|
+
return { ok: false, error: GENERIC_THROW_FAILURE_MESSAGE };
|
|
285
311
|
}
|
|
286
312
|
}
|
|
287
|
-
function recoveryCarrierFailureMessage(
|
|
288
|
-
return
|
|
313
|
+
function recoveryCarrierFailureMessage(failure) {
|
|
314
|
+
return failure.code === 'invalid-response'
|
|
289
315
|
? '恢复信息已验证,但暂时无法读取恢复状态。请稍后重试。'
|
|
290
|
-
:
|
|
316
|
+
: '暂时无法连接 AWiki Host,无法完成身份恢复。请稍后重试。';
|
|
291
317
|
}
|
|
292
318
|
/** Append unique values while retaining existing references. */
|
|
293
319
|
function appendUnique(current, incoming, id) {
|
|
@@ -386,23 +412,19 @@ function sameIdentity(identity, peer) {
|
|
|
386
412
|
const GROUP_HISTORY_RETRY_DELAYS_MS = [250, 750, 1_500, 2_500];
|
|
387
413
|
/** Runtime-only decoded-byte budget that prevents repeat Host calls while browsing. */
|
|
388
414
|
const BROWSER_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
415
|
+
const RECOVERY_OPERATION_STORAGE_KEY = 'awiki.handle-recovery.operation.v1';
|
|
416
|
+
const RECOVERY_OTP_RETRY_STORAGE_KEY = 'awiki.handle-recovery.retry-at.v1';
|
|
417
|
+
function recoveryOperationStorageKey(tenantId) {
|
|
418
|
+
return tenantId === undefined || tenantId === ''
|
|
419
|
+
? RECOVERY_OPERATION_STORAGE_KEY
|
|
420
|
+
: `${RECOVERY_OPERATION_STORAGE_KEY}.${encodeURIComponent(tenantId)}`;
|
|
392
421
|
}
|
|
393
|
-
function
|
|
394
|
-
|
|
395
|
-
return globalThis.localStorage?.getItem(recoveryOperationStorageKey(tenantDomain)) ?? null;
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
return null;
|
|
399
|
-
}
|
|
422
|
+
function recoveryTenantKey(tenantId) {
|
|
423
|
+
return tenantId === undefined || tenantId === '' ? 'default' : tenantId;
|
|
400
424
|
}
|
|
401
|
-
function storeRecoveryOperation(
|
|
402
|
-
if (tenantDomain === undefined)
|
|
403
|
-
return;
|
|
425
|
+
function storeRecoveryOperation(operationId, tenantId) {
|
|
404
426
|
try {
|
|
405
|
-
const key = recoveryOperationStorageKey(
|
|
427
|
+
const key = recoveryOperationStorageKey(tenantId);
|
|
406
428
|
if (operationId === null)
|
|
407
429
|
globalThis.localStorage?.removeItem(key);
|
|
408
430
|
else
|
|
@@ -412,6 +434,49 @@ function storeRecoveryOperation(tenantDomain, operationId) {
|
|
|
412
434
|
// Recovery remains durable in Core even when browser storage is unavailable.
|
|
413
435
|
}
|
|
414
436
|
}
|
|
437
|
+
function recoveryOtpRetryStorageKey(operationId, tenantId) {
|
|
438
|
+
return `${RECOVERY_OTP_RETRY_STORAGE_KEY}.${encodeURIComponent(recoveryTenantKey(tenantId))}.${encodeURIComponent(operationId)}`;
|
|
439
|
+
}
|
|
440
|
+
function recoveryOtpRetryAt(result) {
|
|
441
|
+
const advertised = Date.parse(result.retryAt);
|
|
442
|
+
const deadline = Number.isFinite(advertised)
|
|
443
|
+
? advertised
|
|
444
|
+
: Date.now() + Math.max(0, result.retryAfterSeconds) * 1_000;
|
|
445
|
+
return new Date(deadline).toISOString();
|
|
446
|
+
}
|
|
447
|
+
function storedRecoveryOtpRetryAt(operationId, tenantId) {
|
|
448
|
+
if (operationId === null)
|
|
449
|
+
return null;
|
|
450
|
+
try {
|
|
451
|
+
const key = recoveryOtpRetryStorageKey(operationId, tenantId);
|
|
452
|
+
const value = globalThis.localStorage?.getItem(key);
|
|
453
|
+
if (value === null || value === undefined)
|
|
454
|
+
return null;
|
|
455
|
+
const deadline = Date.parse(value);
|
|
456
|
+
if (!Number.isFinite(deadline) || deadline <= Date.now()) {
|
|
457
|
+
globalThis.localStorage?.removeItem(key);
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
return new Date(deadline).toISOString();
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
function storeRecoveryOtpRetryAt(operationId, retryAt, tenantId) {
|
|
467
|
+
try {
|
|
468
|
+
globalThis.localStorage?.setItem(recoveryOtpRetryStorageKey(operationId, tenantId), retryAt);
|
|
469
|
+
}
|
|
470
|
+
catch { }
|
|
471
|
+
}
|
|
472
|
+
function clearRecoveryOtpRetryAt(operationId, tenantId) {
|
|
473
|
+
if (operationId === null)
|
|
474
|
+
return;
|
|
475
|
+
try {
|
|
476
|
+
globalThis.localStorage?.removeItem(recoveryOtpRetryStorageKey(operationId, tenantId));
|
|
477
|
+
}
|
|
478
|
+
catch { }
|
|
479
|
+
}
|
|
415
480
|
/** Wait between group-history readiness probes without retaining controller state. */
|
|
416
481
|
function delay(milliseconds) {
|
|
417
482
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
@@ -458,13 +523,20 @@ function targetOf(conversation) {
|
|
|
458
523
|
export class AwikiController {
|
|
459
524
|
remote;
|
|
460
525
|
persistentImageCache;
|
|
526
|
+
drafts = new AwikiDraftStore();
|
|
461
527
|
view = INITIAL_VIEW;
|
|
462
528
|
listeners = new Set();
|
|
529
|
+
/** Runtime-only selected operations survive tenant round trips without reopening stale browser hints after reload. */
|
|
530
|
+
recoveryOperationsByTenant = new Map();
|
|
463
531
|
config = null;
|
|
464
532
|
conversationsCursor;
|
|
465
533
|
historyCursor;
|
|
466
534
|
groupMembersCursor;
|
|
467
535
|
timer;
|
|
536
|
+
opening;
|
|
537
|
+
drawerOpen = false;
|
|
538
|
+
accessRequest;
|
|
539
|
+
tenantTransition;
|
|
468
540
|
generation = 0;
|
|
469
541
|
selectionRevision = 0;
|
|
470
542
|
disposed = false;
|
|
@@ -476,18 +548,12 @@ export class AwikiController {
|
|
|
476
548
|
directProfiles = new Map();
|
|
477
549
|
/** Last trustworthy group title for the active identity, keyed by canonical Group DID. */
|
|
478
550
|
groupTitles = new Map();
|
|
479
|
-
/** Current secret-free Core journal state, keyed by canonical Group DID. */
|
|
480
|
-
groupRecoveryItems = new Map();
|
|
481
551
|
/** Identity-scoped product overlays. Core conversations and history remain untouched. */
|
|
482
552
|
hiddenConversationPreferences = new Map();
|
|
483
|
-
dismissedGroupRecoverySignature;
|
|
484
|
-
lastGroupRecoverySignature;
|
|
485
|
-
lastGroupRecoverySummary;
|
|
486
553
|
/** Verified image payloads retained outside observable state for instant remounts. */
|
|
487
554
|
imageAttachments = new Map();
|
|
488
555
|
imageAttachmentCacheBytes = 0;
|
|
489
556
|
presentationCacheOwnerDid = null;
|
|
490
|
-
groupRecoveryOperation;
|
|
491
557
|
/**
|
|
492
558
|
* @param remote - generated Host Remote namespace.
|
|
493
559
|
* @param persistentImageCache - browser-origin verified preview cache.
|
|
@@ -503,51 +569,67 @@ export class AwikiController {
|
|
|
503
569
|
this.listeners.add(listener);
|
|
504
570
|
return () => { this.listeners.delete(listener); };
|
|
505
571
|
};
|
|
572
|
+
/** Open the deployment-matched dynamic Integration guide. */
|
|
573
|
+
openIntegrationGuide() {
|
|
574
|
+
const url = this.config?.integrationGuideUrl;
|
|
575
|
+
if (url !== undefined)
|
|
576
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
577
|
+
}
|
|
506
578
|
/** Load Host policy and the shared identity state without starting drawer polling. */
|
|
507
|
-
async loadSession() {
|
|
579
|
+
async loadSession(preserveView = false) {
|
|
508
580
|
if (this.disposed)
|
|
509
581
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
510
|
-
this.
|
|
511
|
-
|
|
582
|
+
this.stopPollingLifecycle();
|
|
583
|
+
if (!preserveView)
|
|
584
|
+
this.summaryBaselines.clear();
|
|
585
|
+
const previous = this.view;
|
|
586
|
+
const previousTenant = this.config?.tenantId;
|
|
512
587
|
const generation = this.generation;
|
|
513
|
-
|
|
588
|
+
if (!preserveView)
|
|
589
|
+
this.publish({ ...INITIAL_VIEW, status: 'loading' });
|
|
590
|
+
const failed = (error) => {
|
|
591
|
+
if (preserveView)
|
|
592
|
+
this.publish({ ...this.view, error });
|
|
593
|
+
else
|
|
594
|
+
this.fail(error);
|
|
595
|
+
return { ok: false, error };
|
|
596
|
+
};
|
|
514
597
|
const config = await call(() => this.remote.getConfig());
|
|
515
598
|
if (!this.current(generation))
|
|
516
599
|
return { ok: true, value: undefined };
|
|
517
600
|
if (!config.ok)
|
|
518
|
-
return
|
|
601
|
+
return failed(config.error);
|
|
519
602
|
this.config = config.value;
|
|
520
603
|
const session = await call(() => this.remote.getSession());
|
|
521
604
|
if (!this.current(generation))
|
|
522
605
|
return { ok: true, value: undefined };
|
|
523
606
|
if (!session.ok)
|
|
524
|
-
return
|
|
607
|
+
return failed(session.error);
|
|
525
608
|
const identity = session.value.status === 'active' ? session.value.identity : null;
|
|
609
|
+
const sameOwner = preserveView && previousTenant === config.value.tenantId
|
|
610
|
+
&& previous.sessionStatus === session.value.status && previous.identity?.did === identity?.did;
|
|
611
|
+
this.drafts.setScope(config.value.tenantId ?? 'default', identity?.did ?? session.value.status);
|
|
526
612
|
this.activatePresentationCache(identity);
|
|
613
|
+
const selectedRecovery = previousTenant === config.value.tenantId
|
|
614
|
+
? previous.recoveryOperationId
|
|
615
|
+
: this.recoveryOperationsByTenant.get(recoveryTenantKey(config.value.tenantId)) ?? null;
|
|
527
616
|
this.publish({
|
|
528
|
-
...this.view,
|
|
617
|
+
...(sameOwner ? this.view : INITIAL_VIEW),
|
|
529
618
|
status: 'ready',
|
|
530
619
|
sessionStatus: session.value.status,
|
|
531
620
|
identity: session.value.status === 'active' ? session.value.identity : null,
|
|
532
621
|
error: null,
|
|
533
622
|
attachmentMaxBytes: config.value.attachmentMaxBytes,
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
623
|
+
handleRecoveryPhoneEnabled: config.value.handleRecoveryPhoneEnabled,
|
|
624
|
+
recoveryOperationId: selectedRecovery,
|
|
625
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(selectedRecovery, config.value.tenantId),
|
|
537
626
|
});
|
|
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
|
-
}
|
|
627
|
+
await this.refreshIdentityAccess();
|
|
628
|
+
if (!this.current(generation))
|
|
629
|
+
return { ok: true, value: undefined };
|
|
630
|
+
if (identity !== null && previous.identity?.did !== identity.did
|
|
631
|
+
&& this.view.recoveryOperationId === null && this.view.identityAccess?.recoveries.length === 0)
|
|
632
|
+
this.drafts.clearFlow();
|
|
551
633
|
if (identity !== null) {
|
|
552
634
|
await this.loadConversationPreferences(generation);
|
|
553
635
|
const profile = await call(() => this.remote.getProfile());
|
|
@@ -561,19 +643,42 @@ export class AwikiController {
|
|
|
561
643
|
* @returns successful readiness or one display-safe Host failure.
|
|
562
644
|
*/
|
|
563
645
|
async open() {
|
|
564
|
-
|
|
565
|
-
if (
|
|
566
|
-
return
|
|
646
|
+
this.drawerOpen = true;
|
|
647
|
+
if (this.tenantTransition !== undefined) {
|
|
648
|
+
return this.tenantTransition.then(() => this.view.error === null ? { ok: true, value: undefined } : { ok: false, error: this.view.error }, () => ({ ok: false, error: this.view.error ?? '租户切换未完成,请重试' }));
|
|
649
|
+
}
|
|
650
|
+
if (this.opening !== undefined)
|
|
651
|
+
return this.opening;
|
|
652
|
+
const opening = this.openOnce();
|
|
653
|
+
this.opening = opening;
|
|
654
|
+
try {
|
|
655
|
+
return await opening;
|
|
656
|
+
}
|
|
657
|
+
finally {
|
|
658
|
+
if (this.opening === opening)
|
|
659
|
+
this.opening = undefined;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async openOnce() {
|
|
663
|
+
const loading = this.loadSession(this.view.status === 'ready');
|
|
567
664
|
const generation = this.generation;
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
665
|
+
const loaded = await loading;
|
|
666
|
+
if (!loaded.ok && this.view.status !== 'ready')
|
|
667
|
+
return loaded;
|
|
668
|
+
if (!this.current(generation) || !this.drawerOpen)
|
|
669
|
+
return loaded;
|
|
670
|
+
let listed = loaded;
|
|
671
|
+
if (loaded.ok && this.view.identity !== null) {
|
|
672
|
+
listed = await this.refreshConversations(generation, this.view.conversations.length > 0);
|
|
673
|
+
if (this.view.selectedConversationId !== null)
|
|
674
|
+
await this.poll(generation);
|
|
572
675
|
}
|
|
573
|
-
if (this.current(generation)) {
|
|
676
|
+
if (this.current(generation) && this.drawerOpen) {
|
|
677
|
+
if (this.timer !== undefined)
|
|
678
|
+
clearInterval(this.timer);
|
|
574
679
|
this.timer = setInterval(() => { void this.poll(generation); }, this.config?.pollIntervalMs ?? 3_000);
|
|
575
680
|
}
|
|
576
|
-
return
|
|
681
|
+
return listed;
|
|
577
682
|
}
|
|
578
683
|
/** Sign out locally while retaining the SDK-owned identity and database. */
|
|
579
684
|
async logout(request) {
|
|
@@ -582,6 +687,7 @@ export class AwikiController {
|
|
|
582
687
|
const result = await call(() => this.remote.logout(request));
|
|
583
688
|
if (!result.ok)
|
|
584
689
|
return result;
|
|
690
|
+
this.drafts.clearScope();
|
|
585
691
|
this.close();
|
|
586
692
|
this.conversationsCursor = undefined;
|
|
587
693
|
this.historyCursor = undefined;
|
|
@@ -592,7 +698,9 @@ export class AwikiController {
|
|
|
592
698
|
status: 'ready',
|
|
593
699
|
sessionStatus: 'signed-out',
|
|
594
700
|
attachmentMaxBytes: this.config?.attachmentMaxBytes ?? 0,
|
|
701
|
+
handleRecoveryPhoneEnabled: this.config?.handleRecoveryPhoneEnabled ?? false,
|
|
595
702
|
});
|
|
703
|
+
await this.loadSession();
|
|
596
704
|
return result;
|
|
597
705
|
}
|
|
598
706
|
/** Resume the preserved local identity and reload its conversations. */
|
|
@@ -609,7 +717,52 @@ export class AwikiController {
|
|
|
609
717
|
}
|
|
610
718
|
/** Stop polling and invalidate all in-flight drawer work. */
|
|
611
719
|
close() {
|
|
720
|
+
this.drawerOpen = false;
|
|
721
|
+
this.opening = undefined;
|
|
722
|
+
this.stopPollingLifecycle();
|
|
723
|
+
}
|
|
724
|
+
/** Fence old requests before switching, then restore the currently visible tenant view. */
|
|
725
|
+
async switchTenant(change) {
|
|
726
|
+
if (this.disposed)
|
|
727
|
+
throw new Error('AWiki 插件已卸载');
|
|
728
|
+
if (this.tenantTransition !== undefined)
|
|
729
|
+
throw new Error('租户正在切换,请稍候');
|
|
730
|
+
this.opening = undefined;
|
|
731
|
+
this.stopPollingLifecycle();
|
|
732
|
+
this.conversationsCursor = undefined;
|
|
733
|
+
this.historyCursor = undefined;
|
|
734
|
+
this.unreadAtOpen.clear();
|
|
735
|
+
this.clearPresentationCache();
|
|
736
|
+
this.publish({ ...INITIAL_VIEW, status: 'loading' });
|
|
737
|
+
const transition = (async () => {
|
|
738
|
+
let switchError;
|
|
739
|
+
try {
|
|
740
|
+
await change();
|
|
741
|
+
}
|
|
742
|
+
catch (error) {
|
|
743
|
+
switchError = error;
|
|
744
|
+
}
|
|
745
|
+
// On failure the Host restores its previous tenant. Reload that exact committed state too.
|
|
746
|
+
if (!this.disposed) {
|
|
747
|
+
const loaded = this.drawerOpen ? await this.openOnce() : await this.loadSession();
|
|
748
|
+
if (!loaded.ok)
|
|
749
|
+
throw new Error(loaded.error);
|
|
750
|
+
}
|
|
751
|
+
if (switchError !== undefined)
|
|
752
|
+
throw switchError;
|
|
753
|
+
})();
|
|
754
|
+
this.tenantTransition = transition;
|
|
755
|
+
try {
|
|
756
|
+
await transition;
|
|
757
|
+
}
|
|
758
|
+
finally {
|
|
759
|
+
if (this.tenantTransition === transition)
|
|
760
|
+
this.tenantTransition = undefined;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
stopPollingLifecycle() {
|
|
612
764
|
this.generation += 1;
|
|
765
|
+
this.accessRequest = undefined;
|
|
613
766
|
this.selectionRevision += 1;
|
|
614
767
|
if (this.timer !== undefined)
|
|
615
768
|
clearInterval(this.timer);
|
|
@@ -624,7 +777,125 @@ export class AwikiController {
|
|
|
624
777
|
* @returns challenge retry metadata or one display-safe failure.
|
|
625
778
|
*/
|
|
626
779
|
async sendRegistrationOtp(request) {
|
|
627
|
-
|
|
780
|
+
const result = await this.withPending('发送验证码', () => callWithFailureCode(() => this.remote.sendRegistrationOtp(request), registrationOtpFailureMessage));
|
|
781
|
+
return !result.ok && result.failureCode !== 'short-handle-invite-required'
|
|
782
|
+
? { ok: false, error: result.error }
|
|
783
|
+
: result;
|
|
784
|
+
}
|
|
785
|
+
/** Discover Core-owned work without selecting an account or activating a session. */
|
|
786
|
+
refreshIdentityAccess() {
|
|
787
|
+
if (this.accessRequest !== undefined)
|
|
788
|
+
return this.accessRequest;
|
|
789
|
+
const request = this.loadIdentityAccess();
|
|
790
|
+
this.accessRequest = request;
|
|
791
|
+
const settled = () => { if (this.accessRequest === request)
|
|
792
|
+
this.accessRequest = undefined; };
|
|
793
|
+
void request.then(settled, settled);
|
|
794
|
+
return request;
|
|
795
|
+
}
|
|
796
|
+
async loadIdentityAccess() {
|
|
797
|
+
const generation = this.generation;
|
|
798
|
+
const revision = this.recoveryRevision;
|
|
799
|
+
this.publish({ ...this.view, accessLoading: true, accessError: null });
|
|
800
|
+
const result = await call(() => this.remote.getIdentityAccessState());
|
|
801
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
802
|
+
return this.recoveryLeft();
|
|
803
|
+
if (!result.ok) {
|
|
804
|
+
this.publish({ ...this.view, accessLoading: false, accessError: IDENTITY_ACCESS_FAILURE_MESSAGE });
|
|
805
|
+
return { ok: false, error: IDENTITY_ACCESS_FAILURE_MESSAGE };
|
|
806
|
+
}
|
|
807
|
+
const candidates = result.value.recoveries;
|
|
808
|
+
const selected = this.view.recoveryOperationId;
|
|
809
|
+
this.publish({ ...this.view, identityAccess: result.value, recoveryOperationId: selected,
|
|
810
|
+
recoveryProgress: selected === this.view.recoveryOperationId ? this.view.recoveryProgress : null,
|
|
811
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(selected, this.config?.tenantId) });
|
|
812
|
+
if (selected !== null) {
|
|
813
|
+
const status = await callWithFailureCode(() => this.remote.getRecoveryStatus({ operationId: selected }));
|
|
814
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
815
|
+
return this.recoveryLeft();
|
|
816
|
+
if (!status.ok) {
|
|
817
|
+
if (status.failureCode === 'not-found' && candidates.length === 0) {
|
|
818
|
+
clearRecoveryOtpRetryAt(selected, this.config?.tenantId);
|
|
819
|
+
this.rememberRecoveryOperation(null, this.config?.tenantId);
|
|
820
|
+
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, recoveryOtpRetryAt: null });
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
this.publish({ ...this.view, accessLoading: false, accessError: IDENTITY_ACCESS_FAILURE_MESSAGE });
|
|
824
|
+
return { ok: false, error: IDENTITY_ACCESS_FAILURE_MESSAGE };
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else {
|
|
828
|
+
const complete = status.value.phase === 'applied';
|
|
829
|
+
if (complete)
|
|
830
|
+
clearRecoveryOtpRetryAt(selected, this.config?.tenantId);
|
|
831
|
+
this.publish({ ...this.view, recoveryProgress: status.value,
|
|
832
|
+
recoveryOtpRetryAt: complete ? null : storedRecoveryOtpRetryAt(selected, this.config?.tenantId),
|
|
833
|
+
identityAccess: result.value });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
this.publish({ ...this.view, accessLoading: false, accessError: null });
|
|
837
|
+
return { ok: true, value: undefined };
|
|
838
|
+
}
|
|
839
|
+
async selectRecovery(operationId) {
|
|
840
|
+
if (!this.view.identityAccess?.recoveries.some(value => value.operationId === operationId)) {
|
|
841
|
+
return { ok: false, error: '恢复操作已变化,请刷新后重试。' };
|
|
842
|
+
}
|
|
843
|
+
if (this.view.recoveryOperationId !== null)
|
|
844
|
+
this.leaveRecovery();
|
|
845
|
+
else
|
|
846
|
+
this.recoveryRevision++;
|
|
847
|
+
this.rememberRecoveryOperation(operationId, this.config?.tenantId);
|
|
848
|
+
this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: null,
|
|
849
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(operationId, this.config?.tenantId) });
|
|
850
|
+
const result = await this.refreshRecoveryStatus();
|
|
851
|
+
return result.ok ? { ok: true, value: undefined } : result;
|
|
852
|
+
}
|
|
853
|
+
recoveryRevision = 0;
|
|
854
|
+
pendingRecovery = false;
|
|
855
|
+
recoveryCurrent(generation, revision) {
|
|
856
|
+
return this.current(generation) && revision === this.recoveryRevision;
|
|
857
|
+
}
|
|
858
|
+
recoveryLeft() {
|
|
859
|
+
return { ok: false, error: '已离开此恢复流程;进度保留在本机。' };
|
|
860
|
+
}
|
|
861
|
+
rememberRecoveryOperation(operationId, tenantId) {
|
|
862
|
+
const key = recoveryTenantKey(tenantId);
|
|
863
|
+
if (operationId === null) {
|
|
864
|
+
this.recoveryOperationsByTenant.delete(key);
|
|
865
|
+
storeRecoveryOperation(null, tenantId);
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
this.recoveryOperationsByTenant.set(key, operationId);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
/** Navigation only. Never cancel or delete the Core-owned operation. */
|
|
872
|
+
leaveRecovery() {
|
|
873
|
+
this.recoveryRevision++;
|
|
874
|
+
this.accessRequest = undefined;
|
|
875
|
+
const wasPending = this.pendingRecovery;
|
|
876
|
+
if (wasPending)
|
|
877
|
+
this.pendingRequest = undefined;
|
|
878
|
+
this.pendingRecovery = false;
|
|
879
|
+
this.drafts.clearFlow();
|
|
880
|
+
this.rememberRecoveryOperation(null, this.config?.tenantId);
|
|
881
|
+
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, accessError: null,
|
|
882
|
+
accessLoading: false, pending: wasPending ? null : this.view.pending, error: null });
|
|
883
|
+
}
|
|
884
|
+
/** Inspect this tenant's durable operations before requesting any new OTP for a Handle. */
|
|
885
|
+
async continueRecoveryForHandle(handle) {
|
|
886
|
+
const target = handle.trim().toLowerCase();
|
|
887
|
+
const loaded = await this.refreshIdentityAccess();
|
|
888
|
+
if (!loaded.ok)
|
|
889
|
+
return loaded;
|
|
890
|
+
const matches = this.view.identityAccess?.recoveries.filter(value => target.includes('.')
|
|
891
|
+
? value.fullHandle.toLowerCase() === target
|
|
892
|
+
: value.fullHandle.slice(0, value.fullHandle.indexOf('.')).toLowerCase() === target) ?? [];
|
|
893
|
+
if (matches.length > 1)
|
|
894
|
+
return { ok: false, error: '此账号的恢复状态存在冲突,请重新检查。' };
|
|
895
|
+
if (matches.length === 0)
|
|
896
|
+
return { ok: true, value: false };
|
|
897
|
+
const selected = await this.selectRecovery(matches[0].operationId);
|
|
898
|
+
return selected.ok ? { ok: true, value: true } : selected;
|
|
628
899
|
}
|
|
629
900
|
/** Classify one Handle before sending exactly one registration or recovery OTP. */
|
|
630
901
|
async inspectIdentityAccess(request) {
|
|
@@ -638,15 +909,144 @@ export class AwikiController {
|
|
|
638
909
|
async registerIdentity(request) {
|
|
639
910
|
const generation = this.generation;
|
|
640
911
|
const result = await this.withPending('注册身份', () => call(() => this.remote.registerIdentity(request), registrationFailureMessage));
|
|
641
|
-
if (!result.ok)
|
|
912
|
+
if (!result.ok) {
|
|
913
|
+
if (this.current(generation))
|
|
914
|
+
await this.refreshIdentityAccess();
|
|
642
915
|
return result;
|
|
916
|
+
}
|
|
643
917
|
if (!this.current(generation))
|
|
644
918
|
return result;
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
919
|
+
if (result.value.status === 'join-required') {
|
|
920
|
+
this.publish({ ...this.view, identityAccess: { choice: result.value, joining: false, recoveries: [] } });
|
|
921
|
+
}
|
|
922
|
+
if (result.value.status === 'registered') {
|
|
923
|
+
this.drafts.clearScope();
|
|
924
|
+
this.drafts.setScope(this.config?.tenantId ?? 'default', result.value.identity.did);
|
|
925
|
+
this.activatePresentationCache(result.value.identity);
|
|
926
|
+
this.publish({ ...this.view, sessionStatus: 'active', identity: result.value.identity, error: null });
|
|
927
|
+
await this.refreshConversations(generation);
|
|
928
|
+
}
|
|
648
929
|
return result;
|
|
649
930
|
}
|
|
931
|
+
async beginDeviceJoin() {
|
|
932
|
+
const generation = this.generation;
|
|
933
|
+
const result = await this.withPending('开始加入设备', () => call(() => this.remote.beginDeviceJoin()));
|
|
934
|
+
if (result.ok && this.current(generation))
|
|
935
|
+
this.publish({ ...this.view, identityAccess: { choice: null, joining: true, recoveries: [] } });
|
|
936
|
+
return result;
|
|
937
|
+
}
|
|
938
|
+
async getDeviceJoinStatus() {
|
|
939
|
+
const generation = this.generation;
|
|
940
|
+
const result = await call(() => this.remote.getDeviceJoinStatus());
|
|
941
|
+
if (!this.current(generation))
|
|
942
|
+
return { ok: false, error: '身份环境已改变,请重新确认当前流程。' };
|
|
943
|
+
if (result.ok && result.value?.completed) {
|
|
944
|
+
this.drafts.clearFlow();
|
|
945
|
+
await this.open();
|
|
946
|
+
}
|
|
947
|
+
return result;
|
|
948
|
+
}
|
|
949
|
+
cancelDeviceJoin() {
|
|
950
|
+
return this.withPending('取消加入设备', async () => {
|
|
951
|
+
const generation = this.generation;
|
|
952
|
+
const result = await call(() => this.remote.cancelDeviceJoin());
|
|
953
|
+
if (result.ok && this.current(generation))
|
|
954
|
+
this.publish({ ...this.view, identityAccess: { choice: null, joining: false, recoveries: [] } });
|
|
955
|
+
return result.ok ? { ok: true, value: undefined } : result;
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
/** Cancel one exact Join choice and send Recovery OTP only while its original browser scope remains current. */
|
|
959
|
+
async beginRecoveryFromDeviceJoin(request) {
|
|
960
|
+
const generation = this.generation;
|
|
961
|
+
const tenantId = this.config?.tenantId;
|
|
962
|
+
const flowScope = this.drafts.getScope('recovery:retryDeadline');
|
|
963
|
+
const flowEpoch = this.drafts.epoch(flowScope);
|
|
964
|
+
const changed = () => ({
|
|
965
|
+
ok: false,
|
|
966
|
+
error: '恢复能力或身份环境已改变,请重新检查后再选择。',
|
|
967
|
+
});
|
|
968
|
+
const scopeCurrent = () => this.current(generation)
|
|
969
|
+
&& this.config?.tenantId === tenantId
|
|
970
|
+
&& this.drafts.getScope('recovery:retryDeadline') === flowScope
|
|
971
|
+
&& this.drafts.epoch(flowScope) === flowEpoch;
|
|
972
|
+
if (!scopeCurrent())
|
|
973
|
+
return changed();
|
|
974
|
+
const latest = await call(() => this.remote.getConfig());
|
|
975
|
+
if (!scopeCurrent())
|
|
976
|
+
return changed();
|
|
977
|
+
if (!latest.ok)
|
|
978
|
+
return { ok: false, error: '暂时无法确认服务器的恢复能力,请稍后重试。' };
|
|
979
|
+
if (latest.value.tenantId !== tenantId)
|
|
980
|
+
return changed();
|
|
981
|
+
this.config = latest.value;
|
|
982
|
+
this.publish({ ...this.view, handleRecoveryPhoneEnabled: latest.value.handleRecoveryPhoneEnabled });
|
|
983
|
+
if (latest.value.tenantOnline === false) {
|
|
984
|
+
return { ok: false, error: '暂时无法确认服务器的恢复能力,请稍后重试。' };
|
|
985
|
+
}
|
|
986
|
+
if (!latest.value.handleRecoveryPhoneEnabled) {
|
|
987
|
+
return { ok: false, error: '当前服务器未启用手机号恢复,请加入新设备或联系管理员。' };
|
|
988
|
+
}
|
|
989
|
+
const current = () => scopeCurrent() && this.config?.handleRecoveryPhoneEnabled === true;
|
|
990
|
+
const result = await this.withPending('开始恢复身份', async () => {
|
|
991
|
+
const discarded = await call(() => this.remote.cancelDeviceJoin());
|
|
992
|
+
if (!discarded.ok)
|
|
993
|
+
return discarded;
|
|
994
|
+
if (!current())
|
|
995
|
+
return changed();
|
|
996
|
+
this.publish({ ...this.view, identityAccess: { choice: null, joining: false, recoveries: [] } });
|
|
997
|
+
if (request.phone === '')
|
|
998
|
+
return { ok: true, value: null };
|
|
999
|
+
return call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage);
|
|
1000
|
+
}, { publishFailure: false, recovery: true });
|
|
1001
|
+
if (!result.ok) {
|
|
1002
|
+
if (current())
|
|
1003
|
+
await this.continueRecoveryForHandle(request.fullHandle);
|
|
1004
|
+
return result;
|
|
1005
|
+
}
|
|
1006
|
+
if (result.value === null)
|
|
1007
|
+
return result;
|
|
1008
|
+
this.recordRecoveryOtp(result.value, tenantId);
|
|
1009
|
+
const sameFlow = this.config?.tenantId === tenantId
|
|
1010
|
+
&& this.drafts.getScope('recovery:retryDeadline') === flowScope
|
|
1011
|
+
&& this.drafts.epoch(flowScope) === flowEpoch;
|
|
1012
|
+
if (!sameFlow)
|
|
1013
|
+
return result;
|
|
1014
|
+
const retryAt = recoveryOtpRetryAt(result.value);
|
|
1015
|
+
this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null,
|
|
1016
|
+
recoveryOtpRetryAt: retryAt });
|
|
1017
|
+
await this.refreshRecoveryStatus();
|
|
1018
|
+
return result;
|
|
1019
|
+
}
|
|
1020
|
+
retireDeviceIdentityForRejoin() {
|
|
1021
|
+
return this.withPending('准备重新加入设备', async () => {
|
|
1022
|
+
const result = await call(() => this.remote.retireDeviceIdentityForRejoin());
|
|
1023
|
+
if (!result.ok)
|
|
1024
|
+
return result;
|
|
1025
|
+
this.publish({ ...this.view, sessionStatus: 'unregistered', identity: null, error: null });
|
|
1026
|
+
return { ok: true, value: undefined };
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
refreshDeviceManagement() {
|
|
1030
|
+
return call(() => this.remote.refreshDeviceManagement());
|
|
1031
|
+
}
|
|
1032
|
+
startDeviceJoinVerification(request) {
|
|
1033
|
+
return this.withPending('开始设备验证', () => call(() => this.remote.startDeviceJoinVerification(request)));
|
|
1034
|
+
}
|
|
1035
|
+
approveDeviceJoin(request) {
|
|
1036
|
+
return this.withPending('批准设备', () => call(() => this.remote.approveDeviceJoin(request)));
|
|
1037
|
+
}
|
|
1038
|
+
rejectDeviceJoin(request) {
|
|
1039
|
+
return this.withPending('拒绝设备', () => call(() => this.remote.rejectDeviceJoin(request)));
|
|
1040
|
+
}
|
|
1041
|
+
revokeDevice(request) {
|
|
1042
|
+
return this.withPending('撤销设备', () => call(() => this.remote.revokeDevice(request)));
|
|
1043
|
+
}
|
|
1044
|
+
prepareRootTransfer(request) {
|
|
1045
|
+
return this.withPending('准备授予管理权', () => call(() => this.remote.prepareRootTransfer(request)));
|
|
1046
|
+
}
|
|
1047
|
+
confirmRootTransfer(request) {
|
|
1048
|
+
return this.withPending('验证并授予管理权', () => call(() => this.remote.confirmRootTransfer(request)));
|
|
1049
|
+
}
|
|
650
1050
|
/**
|
|
651
1051
|
* Update the deployment identity's public display name.
|
|
652
1052
|
* @param displayName - replacement display name selected by the user.
|
|
@@ -686,105 +1086,135 @@ export class AwikiController {
|
|
|
686
1086
|
this.publish({ ...this.view, identity, profile: result.value, error: null });
|
|
687
1087
|
return result;
|
|
688
1088
|
}
|
|
689
|
-
/** Request a
|
|
1089
|
+
/** Request a dedicated OTP only within the selected account flow. */
|
|
690
1090
|
async sendRecoveryOtp(request) {
|
|
691
|
-
const
|
|
692
|
-
|
|
1091
|
+
const generation = this.generation;
|
|
1092
|
+
const revision = this.recoveryRevision;
|
|
1093
|
+
const tenantId = this.config?.tenantId;
|
|
1094
|
+
const flowScope = this.drafts.getScope('recovery:retryDeadline');
|
|
1095
|
+
const result = await this.withPending('发送恢复验证码', () => call(() => this.remote.sendRecoveryOtp(request), registrationOtpFailureMessage), { publishFailure: false, recovery: true });
|
|
1096
|
+
if (!result.ok) {
|
|
1097
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
1098
|
+
return this.recoveryLeft();
|
|
1099
|
+
await this.continueRecoveryForHandle(request.fullHandle);
|
|
693
1100
|
return result;
|
|
694
|
-
|
|
695
|
-
this.
|
|
696
|
-
|
|
1101
|
+
}
|
|
1102
|
+
this.recordRecoveryOtp(result.value, tenantId);
|
|
1103
|
+
if (!this.recoveryCurrent(generation, revision)
|
|
1104
|
+
|| this.config?.tenantId !== tenantId
|
|
1105
|
+
|| this.drafts.getScope('recovery:retryDeadline') !== flowScope)
|
|
1106
|
+
return result;
|
|
1107
|
+
this.publish({ ...this.view, recoveryOperationId: result.value.operationId, recoveryProgress: null,
|
|
1108
|
+
recoveryOtpRetryAt: recoveryOtpRetryAt(result.value) });
|
|
1109
|
+
await this.refreshRecoveryStatus();
|
|
1110
|
+
return this.recoveryCurrent(generation, revision) ? result : this.recoveryLeft();
|
|
697
1111
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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;
|
|
1112
|
+
recordRecoveryOtp(result, tenantId) {
|
|
1113
|
+
this.rememberRecoveryOperation(result.operationId, tenantId);
|
|
1114
|
+
storeRecoveryOtpRetryAt(result.operationId, recoveryOtpRetryAt(result), tenantId);
|
|
707
1115
|
}
|
|
708
|
-
/**
|
|
709
|
-
async
|
|
1116
|
+
/** Publish results only into the exact selected flow; Core work survives navigation. */
|
|
1117
|
+
async runRecovery(label, operation, enterWhenApplied = false) {
|
|
1118
|
+
const generation = this.generation;
|
|
1119
|
+
const revision = this.recoveryRevision;
|
|
710
1120
|
const operationId = this.view.recoveryOperationId;
|
|
711
1121
|
if (operationId === null)
|
|
712
|
-
return
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1122
|
+
return { ok: false, error: '没有可继续的身份恢复操作' };
|
|
1123
|
+
const result = await this.withPending(label, () => operation(operationId), { publishFailure: false, recovery: true });
|
|
1124
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
1125
|
+
return this.recoveryLeft();
|
|
1126
|
+
if (!result.ok) {
|
|
1127
|
+
// Reconcile authority after an ambiguous failure, never infer whether Commit occurred.
|
|
1128
|
+
const status = await call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage);
|
|
1129
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
1130
|
+
return this.recoveryLeft();
|
|
1131
|
+
this.publish({ ...this.view, recoveryProgress: status.ok ? status.value : this.view.recoveryProgress,
|
|
1132
|
+
accessError: status.ok ? null : IDENTITY_ACCESS_FAILURE_MESSAGE });
|
|
718
1133
|
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
1134
|
}
|
|
1135
|
+
this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
|
|
1136
|
+
if (result.value.phase === 'applied')
|
|
1137
|
+
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
1138
|
+
if (enterWhenApplied && result.value.phase === 'applied')
|
|
1139
|
+
return this.enterRecoveredSession();
|
|
725
1140
|
return result;
|
|
726
1141
|
}
|
|
727
|
-
|
|
1142
|
+
async prepareRecovery(request) {
|
|
1143
|
+
if (!this.view.recoveryProgress?.allowedActions?.includes('prepare'))
|
|
1144
|
+
return { ok: false, error: '请先检查恢复状态' };
|
|
1145
|
+
return this.runRecovery('验证恢复信息', operationId => call(() => this.remote.prepareRecovery({ ...request, operationId }), recoveryPreparationFailureMessage, recoveryCarrierFailureMessage));
|
|
1146
|
+
}
|
|
1147
|
+
async activateRecovery() {
|
|
1148
|
+
const progress = this.view.recoveryProgress;
|
|
1149
|
+
if (!progress?.allowedActions?.includes('activate'))
|
|
1150
|
+
return { ok: false, error: '请先完成恢复信息验证' };
|
|
1151
|
+
return this.runRecovery('恢复身份', operationId => call(() => this.remote.activateRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), true);
|
|
1152
|
+
}
|
|
1153
|
+
/** Read-only even for applied operations. Entering the identity is a separate action. */
|
|
728
1154
|
async refreshRecoveryStatus() {
|
|
1155
|
+
const generation = this.generation;
|
|
1156
|
+
const revision = this.recoveryRevision;
|
|
729
1157
|
const operationId = this.view.recoveryOperationId;
|
|
730
1158
|
if (operationId === null)
|
|
731
|
-
return
|
|
732
|
-
const result = await this.withPending('刷新恢复状态',
|
|
733
|
-
|
|
1159
|
+
return { ok: false, error: '没有可查询的身份恢复操作' };
|
|
1160
|
+
const result = await this.withPending('刷新恢复状态', async () => {
|
|
1161
|
+
const status = await call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage);
|
|
1162
|
+
return status.ok ? status : { ok: false, error: IDENTITY_ACCESS_FAILURE_MESSAGE };
|
|
1163
|
+
}, { publishFailure: false, recovery: true });
|
|
1164
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
1165
|
+
return this.recoveryLeft();
|
|
1166
|
+
if (!result.ok) {
|
|
1167
|
+
this.publish({ ...this.view, accessError: IDENTITY_ACCESS_FAILURE_MESSAGE });
|
|
734
1168
|
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
1169
|
}
|
|
1170
|
+
this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
|
|
1171
|
+
if (result.value.phase === 'applied')
|
|
1172
|
+
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
741
1173
|
return result;
|
|
742
1174
|
}
|
|
743
|
-
/** Resume only a Core-declared retryable or uncertain phase. */
|
|
744
1175
|
async resumeRecovery() {
|
|
745
|
-
const operationId = this.view.recoveryOperationId;
|
|
746
1176
|
const progress = this.view.recoveryProgress;
|
|
747
|
-
if (
|
|
748
|
-
return
|
|
749
|
-
|
|
750
|
-
|
|
1177
|
+
if (!progress?.allowedActions?.includes('resume'))
|
|
1178
|
+
return { ok: false, error: '当前恢复状态不能重试' };
|
|
1179
|
+
return this.runRecovery('继续恢复身份', operationId => call(() => this.remote.resumeRecovery({ operationId }), failure => recoveryContinuationFailureMessage(failure, progress.phase), recoveryCarrierFailureMessage), true);
|
|
1180
|
+
}
|
|
1181
|
+
async enterRecoveredSession() {
|
|
1182
|
+
const generation = this.generation;
|
|
1183
|
+
const revision = this.recoveryRevision;
|
|
1184
|
+
const progress = this.view.recoveryProgress;
|
|
1185
|
+
if (progress?.phase !== 'applied' || !progress.allowedActions?.includes('activate_identity')) {
|
|
1186
|
+
return { ok: false, error: '当前恢复身份尚不可进入,请重新检查。' };
|
|
751
1187
|
}
|
|
752
|
-
const result = await this.withPending('
|
|
1188
|
+
const result = await this.withPending('进入恢复后的身份', () => call(() => this.remote.enterRecoveredSession({ operationId: progress.operationId })), { publishFailure: false, recovery: true });
|
|
1189
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
1190
|
+
return this.recoveryLeft();
|
|
753
1191
|
if (!result.ok)
|
|
754
1192
|
return result;
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
await this.open();
|
|
760
|
-
}
|
|
761
|
-
return result;
|
|
1193
|
+
clearRecoveryOtpRetryAt(progress.operationId, this.config?.tenantId);
|
|
1194
|
+
this.leaveRecovery();
|
|
1195
|
+
const opened = await this.open();
|
|
1196
|
+
return opened.ok ? result : opened;
|
|
762
1197
|
}
|
|
763
|
-
/**
|
|
1198
|
+
/** Only Core-authorized pre-attempt cancellation destroys the pending operation. */
|
|
764
1199
|
async discardRecovery() {
|
|
1200
|
+
const generation = this.generation;
|
|
1201
|
+
const revision = this.recoveryRevision;
|
|
765
1202
|
const operationId = this.view.recoveryOperationId;
|
|
766
1203
|
if (operationId === null)
|
|
767
1204
|
return { ok: true, value: undefined };
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
return this.fail('当前恢复状态不能取消');
|
|
1205
|
+
if (!this.view.recoveryProgress?.allowedActions?.includes('discard_pre_attempt')) {
|
|
1206
|
+
return { ok: false, error: '当前恢复状态不能取消;可以返回入口,稍后继续。' };
|
|
771
1207
|
}
|
|
772
|
-
const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })), { publishFailure: false });
|
|
1208
|
+
const result = await this.withPending('取消身份恢复', () => call(() => this.remote.discardRecovery({ operationId })), { publishFailure: false, recovery: true });
|
|
1209
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
1210
|
+
return this.recoveryLeft();
|
|
773
1211
|
if (!result.ok)
|
|
774
1212
|
return result;
|
|
775
|
-
|
|
776
|
-
this.
|
|
1213
|
+
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
1214
|
+
this.leaveRecovery();
|
|
1215
|
+
void this.refreshIdentityAccess();
|
|
777
1216
|
return { ok: true, value: undefined };
|
|
778
1217
|
}
|
|
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
1218
|
/** Read the active deployment identity's public mailbox state. */
|
|
789
1219
|
getMailAccount() {
|
|
790
1220
|
return call(() => this.remote.getMailAccount(), mailFailureMessage);
|
|
@@ -805,16 +1235,6 @@ export class AwikiController {
|
|
|
805
1235
|
sendMail(request) {
|
|
806
1236
|
return call(() => this.remote.sendMail(request), mailFailureMessage);
|
|
807
1237
|
}
|
|
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
1238
|
/**
|
|
819
1239
|
* Load another page of the conversation roster.
|
|
820
1240
|
* @returns successful pagination or one display-safe failure.
|
|
@@ -834,33 +1254,10 @@ export class AwikiController {
|
|
|
834
1254
|
...this.view,
|
|
835
1255
|
conversations: appendUnique(this.view.conversations, conversations.visible, value => value.id),
|
|
836
1256
|
hiddenConversations: conversations.hidden,
|
|
837
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
838
1257
|
conversationsHasMore: result.value.hasMore && result.value.nextCursor !== undefined,
|
|
839
1258
|
});
|
|
840
1259
|
return { ok: true, value: undefined };
|
|
841
1260
|
}
|
|
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
1261
|
/** Hide one recent row locally without leaving a group or deleting history. */
|
|
865
1262
|
async hideConversation(conversationId) {
|
|
866
1263
|
const conversation = this.view.conversations.find(item => item.id === conversationId);
|
|
@@ -896,7 +1293,6 @@ export class AwikiController {
|
|
|
896
1293
|
refreshing: false,
|
|
897
1294
|
}
|
|
898
1295
|
: {}),
|
|
899
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
900
1296
|
error: null,
|
|
901
1297
|
});
|
|
902
1298
|
return { ok: true, value: undefined };
|
|
@@ -918,7 +1314,6 @@ export class AwikiController {
|
|
|
918
1314
|
...this.view,
|
|
919
1315
|
conversations: appendUnique([conversation], this.view.conversations, item => item.id),
|
|
920
1316
|
hiddenConversations: this.hiddenConversationsView(),
|
|
921
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
922
1317
|
error: null,
|
|
923
1318
|
});
|
|
924
1319
|
return { ok: true, value: undefined };
|
|
@@ -949,10 +1344,15 @@ export class AwikiController {
|
|
|
949
1344
|
return this.selectConversation(existing.id);
|
|
950
1345
|
}
|
|
951
1346
|
const generation = this.generation;
|
|
952
|
-
const resolved = await this.withPending('查找用户',
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1347
|
+
const resolved = await this.withPending('查找用户', async () => {
|
|
1348
|
+
const result = await callWithFailureCode(() => this.remote.resolvePeer({ peer }));
|
|
1349
|
+
if (!result.ok) {
|
|
1350
|
+
return { ok: false, error: result.failureCode === 'not-found' ? '该 Handle 不存在' : result.error };
|
|
1351
|
+
}
|
|
1352
|
+
return result;
|
|
1353
|
+
});
|
|
1354
|
+
if (!resolved.ok)
|
|
1355
|
+
return resolved;
|
|
956
1356
|
if (!this.current(generation))
|
|
957
1357
|
return { ok: true, value: undefined };
|
|
958
1358
|
if (resolved.value.did === identity.did)
|
|
@@ -1074,9 +1474,6 @@ export class AwikiController {
|
|
|
1074
1474
|
const generation = this.generation;
|
|
1075
1475
|
this.publish({ ...this.view, pending: '刷新群成员', error: null });
|
|
1076
1476
|
const result = await (async () => {
|
|
1077
|
-
if (this.view.groupAccess?.status !== 'available' && this.view.groupAccess?.status !== 'loading') {
|
|
1078
|
-
await this.resumeGroupRebindRecovery(this.generation);
|
|
1079
|
-
}
|
|
1080
1477
|
const refreshed = await this.loadGroupState(conversation, true, true);
|
|
1081
1478
|
return refreshed.ok ? { ok: true, value: undefined } : refreshed;
|
|
1082
1479
|
})();
|
|
@@ -1613,28 +2010,94 @@ export class AwikiController {
|
|
|
1613
2010
|
}
|
|
1614
2011
|
return result;
|
|
1615
2012
|
}
|
|
1616
|
-
/** Clear
|
|
2013
|
+
/** Clear current-tenant local data and only its owned browser projections. */
|
|
1617
2014
|
async clearLocalData(request) {
|
|
1618
2015
|
if (this.disposed)
|
|
1619
2016
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
2017
|
+
const generation = this.generation;
|
|
2018
|
+
const ownerDid = this.presentationCacheOwnerDid;
|
|
2019
|
+
const tenantId = this.config?.tenantId;
|
|
1620
2020
|
const result = await call(() => this.remote.clearLocalData(request));
|
|
1621
2021
|
if (!result.ok)
|
|
1622
2022
|
return result;
|
|
1623
|
-
|
|
2023
|
+
const owners = [...(result.value.clearedIdentityDids ?? []), ...(ownerDid === null ? [] : [ownerDid])];
|
|
2024
|
+
for (const owner of new Set(owners)) {
|
|
2025
|
+
await this.persistentImageCache.clear(owner).catch(() => undefined);
|
|
2026
|
+
try {
|
|
2027
|
+
clearMailBrowserCache(globalThis.localStorage, owner);
|
|
2028
|
+
}
|
|
2029
|
+
catch { }
|
|
2030
|
+
}
|
|
2031
|
+
clearRecoveryOtpRetryAt(this.view.recoveryOperationId, tenantId);
|
|
2032
|
+
this.rememberRecoveryOperation(null, tenantId);
|
|
2033
|
+
if (!this.current(generation))
|
|
2034
|
+
return result;
|
|
2035
|
+
this.drafts.clearScope();
|
|
1624
2036
|
this.close();
|
|
1625
|
-
|
|
1626
|
-
this.config = null;
|
|
2037
|
+
// Preserve tenantId for the confirm-time getConfig scope check without reopening.
|
|
1627
2038
|
this.conversationsCursor = undefined;
|
|
1628
2039
|
this.historyCursor = undefined;
|
|
1629
2040
|
this.unreadAtOpen.clear();
|
|
1630
2041
|
this.summaryBaselines.clear();
|
|
1631
2042
|
this.clearPresentationCache();
|
|
1632
|
-
|
|
1633
|
-
|
|
2043
|
+
this.publish({ ...INITIAL_VIEW, status: 'ready', accessLoading: false,
|
|
2044
|
+
attachmentMaxBytes: this.config?.attachmentMaxBytes ?? INITIAL_VIEW.attachmentMaxBytes,
|
|
2045
|
+
handleRecoveryPhoneEnabled: this.config?.handleRecoveryPhoneEnabled ?? false,
|
|
2046
|
+
identityAccess: { choice: null, joining: false, recoveries: [] } });
|
|
1634
2047
|
return result;
|
|
1635
2048
|
}
|
|
2049
|
+
/** Read the Integration without coupling Guest Gateway health to the main AWiki view. */
|
|
2050
|
+
getIntegration() {
|
|
2051
|
+
return callIntegration(() => this.remote.getIntegration());
|
|
2052
|
+
}
|
|
2053
|
+
createIntegration(request) {
|
|
2054
|
+
return callIntegration(() => this.remote.createIntegration(request));
|
|
2055
|
+
}
|
|
2056
|
+
updateIntegration(request) {
|
|
2057
|
+
return callIntegration(() => this.remote.updateIntegration(request));
|
|
2058
|
+
}
|
|
2059
|
+
rotateIntegrationId(request) {
|
|
2060
|
+
return callIntegration(() => this.remote.rotateIntegrationId(request));
|
|
2061
|
+
}
|
|
2062
|
+
closeIntegration(request) {
|
|
2063
|
+
return callIntegration(() => this.remote.closeIntegration(request));
|
|
2064
|
+
}
|
|
2065
|
+
reopenIntegration(request) {
|
|
2066
|
+
return callIntegration(() => this.remote.reopenIntegration(request));
|
|
2067
|
+
}
|
|
2068
|
+
/** Return only locally known groups for which the active identity is authoritative owner. */
|
|
2069
|
+
async listOwnedGroups() {
|
|
2070
|
+
const groups = [];
|
|
2071
|
+
const visited = new Set();
|
|
2072
|
+
let cursor;
|
|
2073
|
+
for (let pageIndex = 0; pageIndex < 20 && groups.length < 20; pageIndex += 1) {
|
|
2074
|
+
const conversations = await call(() => this.remote.listConversations({
|
|
2075
|
+
limit: 100,
|
|
2076
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
2077
|
+
}));
|
|
2078
|
+
if (!conversations.ok)
|
|
2079
|
+
return conversations;
|
|
2080
|
+
for (const conversation of conversations.value.items) {
|
|
2081
|
+
if (conversation.kind !== 'group' || visited.has(conversation.groupDid))
|
|
2082
|
+
continue;
|
|
2083
|
+
visited.add(conversation.groupDid);
|
|
2084
|
+
const snapshot = await call(() => this.remote.getGroup({ groupDid: conversation.groupDid }));
|
|
2085
|
+
if (!snapshot.ok)
|
|
2086
|
+
continue;
|
|
2087
|
+
if (snapshot.value.myRole === 'owner')
|
|
2088
|
+
groups.push(snapshot.value);
|
|
2089
|
+
if (groups.length >= 20)
|
|
2090
|
+
break;
|
|
2091
|
+
}
|
|
2092
|
+
cursor = conversations.value.nextCursor;
|
|
2093
|
+
if (cursor === undefined)
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
return { ok: true, value: groups };
|
|
2097
|
+
}
|
|
1636
2098
|
/** Stop timers, invalidate work, and drop subscribers during HMR unload. */
|
|
1637
2099
|
dispose() {
|
|
2100
|
+
this.drafts.clear();
|
|
1638
2101
|
this.disposed = true;
|
|
1639
2102
|
this.close();
|
|
1640
2103
|
this.listeners.clear();
|
|
@@ -1654,25 +2117,12 @@ export class AwikiController {
|
|
|
1654
2117
|
hiddenAt: hidden.hiddenAt,
|
|
1655
2118
|
});
|
|
1656
2119
|
}
|
|
1657
|
-
this.dismissedGroupRecoverySignature = preferences.dismissedGroupRecoverySignature;
|
|
1658
2120
|
}
|
|
1659
2121
|
hiddenConversationsView() {
|
|
1660
2122
|
return [...this.hiddenConversationPreferences.values()]
|
|
1661
2123
|
.sort((left, right) => right.hiddenAt - left.hiddenAt)
|
|
1662
2124
|
.map(item => item.conversation);
|
|
1663
2125
|
}
|
|
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
2126
|
async reconcileConversationPage(incoming, generation) {
|
|
1677
2127
|
const current = [...this.view.conversations, ...this.view.hiddenConversations];
|
|
1678
2128
|
const conversations = incoming.map(item => this.cacheConversation(item, current.find(candidate => candidate.id === item.id)));
|
|
@@ -1723,7 +2173,6 @@ export class AwikiController {
|
|
|
1723
2173
|
? refreshed.visible
|
|
1724
2174
|
: appendUnique(refreshed.visible, this.view.conversations, value => value.id),
|
|
1725
2175
|
hiddenConversations: refreshed.hidden,
|
|
1726
|
-
groupRecovery: this.currentGroupRecoveryView(),
|
|
1727
2176
|
conversationsHasMore: firstPage
|
|
1728
2177
|
? result.value.hasMore && result.value.nextCursor !== undefined
|
|
1729
2178
|
: this.view.conversationsHasMore,
|
|
@@ -1734,14 +2183,18 @@ export class AwikiController {
|
|
|
1734
2183
|
/** List the active identity's own conversations and detect a revoked local credential. */
|
|
1735
2184
|
async listConversationPage(request) {
|
|
1736
2185
|
const result = await callWithFailureCode(() => this.remote.listConversations(request));
|
|
2186
|
+
if (!result.ok && result.failureCode === 'device-rejoin-required') {
|
|
2187
|
+
this.enterBlockedIdentityState('device-rejoin-required');
|
|
2188
|
+
return { ok: false, error: '当前设备已被撤销,请重新申请加入。' };
|
|
2189
|
+
}
|
|
1737
2190
|
if (!result.ok && (result.failureCode === 'identity-recovery-required' || result.failureCode === 'forbidden')) {
|
|
1738
|
-
this.
|
|
2191
|
+
this.enterBlockedIdentityState('recovery-required');
|
|
1739
2192
|
return { ok: false, error: '当前设备的 AWiki 身份凭证已失效,请重新恢复身份。' };
|
|
1740
2193
|
}
|
|
1741
2194
|
return result.ok ? result : { ok: false, error: result.error };
|
|
1742
2195
|
}
|
|
1743
2196
|
/** Replace only visible browser projections; Core identity and SQLite state remain untouched. */
|
|
1744
|
-
|
|
2197
|
+
enterBlockedIdentityState(status) {
|
|
1745
2198
|
if (this.view.identity === null)
|
|
1746
2199
|
return;
|
|
1747
2200
|
this.close();
|
|
@@ -1750,14 +2203,11 @@ export class AwikiController {
|
|
|
1750
2203
|
this.groupMembersCursor = undefined;
|
|
1751
2204
|
this.unreadAtOpen.clear();
|
|
1752
2205
|
this.summaryBaselines.clear();
|
|
1753
|
-
this.groupRecoveryItems.clear();
|
|
1754
|
-
this.lastGroupRecoverySignature = undefined;
|
|
1755
|
-
this.lastGroupRecoverySummary = undefined;
|
|
1756
2206
|
this.clearImageAttachments();
|
|
1757
2207
|
this.publish({
|
|
1758
2208
|
...this.view,
|
|
1759
2209
|
status: 'ready',
|
|
1760
|
-
sessionStatus:
|
|
2210
|
+
sessionStatus: status,
|
|
1761
2211
|
conversations: Object.freeze([]),
|
|
1762
2212
|
hiddenConversations: Object.freeze([]),
|
|
1763
2213
|
conversationsHasMore: false,
|
|
@@ -1766,7 +2216,6 @@ export class AwikiController {
|
|
|
1766
2216
|
groupAccess: null,
|
|
1767
2217
|
groupMembers: Object.freeze([]),
|
|
1768
2218
|
groupMembersHasMore: false,
|
|
1769
|
-
groupRecovery: null,
|
|
1770
2219
|
messages: Object.freeze([]),
|
|
1771
2220
|
historyHasMore: false,
|
|
1772
2221
|
localPending: false,
|
|
@@ -1776,49 +2225,6 @@ export class AwikiController {
|
|
|
1776
2225
|
summaries: Object.freeze({}),
|
|
1777
2226
|
});
|
|
1778
2227
|
}
|
|
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
2228
|
async loadHistory(older) {
|
|
1823
2229
|
const conversationId = this.view.selectedConversationId;
|
|
1824
2230
|
if (conversationId === null)
|
|
@@ -1862,13 +2268,7 @@ export class AwikiController {
|
|
|
1862
2268
|
return;
|
|
1863
2269
|
this.polling = true;
|
|
1864
2270
|
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
|
-
}
|
|
2271
|
+
await this.refreshConversations(generation, true);
|
|
1872
2272
|
const selected = this.view.selectedConversationId;
|
|
1873
2273
|
if (selected === null || !this.current(generation))
|
|
1874
2274
|
return;
|
|
@@ -1882,6 +2282,19 @@ export class AwikiController {
|
|
|
1882
2282
|
this.publish({ ...this.view, error: 'AWiki 远端消息归属不一致,请重新打开会话。' });
|
|
1883
2283
|
return;
|
|
1884
2284
|
}
|
|
2285
|
+
const displayPeers = [...new Set([
|
|
2286
|
+
...this.view.messages.map(message => message.senderDid),
|
|
2287
|
+
...this.view.groupMembers.flatMap(member => member.did === undefined ? [] : [member.did]),
|
|
2288
|
+
])];
|
|
2289
|
+
const displayProfiles = [];
|
|
2290
|
+
for (let start = 0; start < displayPeers.length; start += 100) {
|
|
2291
|
+
const profileResult = await call(() => this.remote.getDisplayProfiles(displayPeers.slice(start, start + 100)));
|
|
2292
|
+
if (!this.current(generation) || this.view.selectedConversationId !== selected)
|
|
2293
|
+
return;
|
|
2294
|
+
if (profileResult.ok)
|
|
2295
|
+
displayProfiles.push(...profileResult.value);
|
|
2296
|
+
}
|
|
2297
|
+
const byDid = new Map(displayProfiles.filter(profile => profile.cacheHit).map(profile => [profile.did, profile]));
|
|
1885
2298
|
const existingIds = new Set(this.view.messages.map(message => message.id));
|
|
1886
2299
|
const incoming = result.value.items.filter(message => !existingIds.has(message.id));
|
|
1887
2300
|
const messages = mergeLatestMessages(this.view.messages, result.value.items);
|
|
@@ -1891,7 +2304,22 @@ export class AwikiController {
|
|
|
1891
2304
|
}
|
|
1892
2305
|
this.publish({
|
|
1893
2306
|
...this.view,
|
|
1894
|
-
messages
|
|
2307
|
+
messages: messages.map(message => {
|
|
2308
|
+
const profile = byDid.get(message.senderDid);
|
|
2309
|
+
if (profile === undefined)
|
|
2310
|
+
return message;
|
|
2311
|
+
const { senderDisplayName: _snapshot, ...rest } = message;
|
|
2312
|
+
const handle = profile.handle ?? message.senderHandle;
|
|
2313
|
+
return { ...rest, ...profile.displayName === undefined ? {} : { senderDisplayName: profile.displayName }, ...handle === undefined ? {} : { senderHandle: handle } };
|
|
2314
|
+
}),
|
|
2315
|
+
groupMembers: this.view.groupMembers.map(member => {
|
|
2316
|
+
const profile = member.did === undefined ? undefined : byDid.get(member.did);
|
|
2317
|
+
if (profile === undefined)
|
|
2318
|
+
return member;
|
|
2319
|
+
const { displayName: _snapshot, ...rest } = member;
|
|
2320
|
+
const handle = profile.handle ?? member.handle;
|
|
2321
|
+
return { ...rest, ...profile.displayName === undefined ? {} : { displayName: profile.displayName }, ...handle === undefined ? {} : { handle } };
|
|
2322
|
+
}),
|
|
1895
2323
|
summaries: this.staleSummaries(selected, incoming),
|
|
1896
2324
|
});
|
|
1897
2325
|
}
|
|
@@ -1899,20 +2327,34 @@ export class AwikiController {
|
|
|
1899
2327
|
this.polling = false;
|
|
1900
2328
|
}
|
|
1901
2329
|
}
|
|
2330
|
+
pendingRequest;
|
|
1902
2331
|
async withPending(label, operation, options = {}) {
|
|
1903
2332
|
if (this.disposed)
|
|
1904
2333
|
return { ok: false, error: 'AWiki 插件已卸载' };
|
|
2334
|
+
if (this.pendingRequest !== undefined)
|
|
2335
|
+
return { ok: false, error: '上一项操作仍在处理,请稍候再试。' };
|
|
2336
|
+
const token = Symbol(label);
|
|
2337
|
+
this.pendingRequest = token;
|
|
2338
|
+
this.pendingRecovery = options.recovery === true;
|
|
1905
2339
|
const generation = this.generation;
|
|
1906
2340
|
this.publish({ ...this.view, pending: label, error: null });
|
|
1907
|
-
|
|
1908
|
-
|
|
2341
|
+
try {
|
|
2342
|
+
const result = await operation();
|
|
2343
|
+
if (this.current(generation) && this.pendingRequest === token)
|
|
2344
|
+
this.publish({
|
|
2345
|
+
...this.view, error: result.ok || options.publishFailure === false ? null : result.error,
|
|
2346
|
+
});
|
|
1909
2347
|
return result;
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2348
|
+
}
|
|
2349
|
+
finally {
|
|
2350
|
+
if (this.pendingRequest === token) {
|
|
2351
|
+
this.pendingRequest = undefined;
|
|
2352
|
+
this.pendingRecovery = false;
|
|
2353
|
+
// Closing/reopening invalidates reads, but the original mutation still settles.
|
|
2354
|
+
if (!this.disposed)
|
|
2355
|
+
this.publish({ ...this.view, pending: null });
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
1916
2358
|
}
|
|
1917
2359
|
appendMessage(message) {
|
|
1918
2360
|
if (this.view.selectedConversationId !== message.conversationId)
|
|
@@ -1963,14 +2405,7 @@ export class AwikiController {
|
|
|
1963
2405
|
return selected === null ? undefined : this.view.conversations.find(value => value.id === selected);
|
|
1964
2406
|
}
|
|
1965
2407
|
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;
|
|
2408
|
+
const status = reason;
|
|
1974
2409
|
this.publish({
|
|
1975
2410
|
...this.view,
|
|
1976
2411
|
groupAccess: { groupDid: conversation.groupDid, status },
|
|
@@ -1984,11 +2419,7 @@ export class AwikiController {
|
|
|
1984
2419
|
return;
|
|
1985
2420
|
this.directProfiles.clear();
|
|
1986
2421
|
this.groupTitles.clear();
|
|
1987
|
-
this.groupRecoveryItems.clear();
|
|
1988
2422
|
this.hiddenConversationPreferences.clear();
|
|
1989
|
-
this.dismissedGroupRecoverySignature = undefined;
|
|
1990
|
-
this.lastGroupRecoverySignature = undefined;
|
|
1991
|
-
this.lastGroupRecoverySummary = undefined;
|
|
1992
2423
|
this.clearImageAttachments();
|
|
1993
2424
|
this.presentationCacheOwnerDid = ownerDid;
|
|
1994
2425
|
}
|
|
@@ -1996,11 +2427,7 @@ export class AwikiController {
|
|
|
1996
2427
|
clearPresentationCache() {
|
|
1997
2428
|
this.directProfiles.clear();
|
|
1998
2429
|
this.groupTitles.clear();
|
|
1999
|
-
this.groupRecoveryItems.clear();
|
|
2000
2430
|
this.hiddenConversationPreferences.clear();
|
|
2001
|
-
this.dismissedGroupRecoverySignature = undefined;
|
|
2002
|
-
this.lastGroupRecoverySignature = undefined;
|
|
2003
|
-
this.lastGroupRecoverySummary = undefined;
|
|
2004
2431
|
this.clearImageAttachments();
|
|
2005
2432
|
this.presentationCacheOwnerDid = null;
|
|
2006
2433
|
}
|