@awiki/dsh-plugin 0.3.10 → 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 +4 -3
- package/README.zh.md +4 -3
- package/lib/client.js +1351 -158
- package/lib/client.js.map +1 -1
- package/lib/index.js +172 -93
- package/lib/model-proxy-contract.js +8 -8
- package/lib/typert.host.js +56 -56
- package/lib/typert.remote-client.js +56 -56
- package/lib/types/client/AwikiIdentityAccess.d.ts +6 -4
- package/lib/types/client/AwikiIdentityAccess.d.ts.map +1 -1
- package/lib/types/client/AwikiIdentityAccess.js +111 -43
- package/lib/types/client/AwikiIdentityAccess.js.map +1 -1
- package/lib/types/client/AwikiOverlay.js +1 -1
- package/lib/types/client/AwikiOverlay.js.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.d.ts +1 -0
- package/lib/types/client/AwikiRecoveryForm.d.ts.map +1 -1
- package/lib/types/client/AwikiRecoveryForm.js +3 -2
- package/lib/types/client/AwikiRecoveryForm.js.map +1 -1
- package/lib/types/client/AwikiSettingsSection.d.ts +1 -1
- package/lib/types/client/AwikiSettingsSection.d.ts.map +1 -1
- 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 +1 -1
- package/lib/types/client/awiki-client-bridge.js.map +1 -1
- package/lib/types/client/controller.d.ts +8 -0
- package/lib/types/client/controller.d.ts.map +1 -1
- package/lib/types/client/controller.js +274 -53
- package/lib/types/client/controller.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 +2 -1
- package/lib/types/client/index.js.map +1 -1
- package/lib/types/client/otp-cooldown.d.ts +1 -0
- package/lib/types/client/otp-cooldown.d.ts.map +1 -1
- package/lib/types/client/otp-cooldown.js +12 -1
- package/lib/types/client/otp-cooldown.js.map +1 -1
- package/lib/types/client/settings-controller.d.ts +4 -2
- package/lib/types/client/settings-controller.d.ts.map +1 -1
- package/lib/types/client/settings-controller.js +24 -8
- package/lib/types/client/settings-controller.js.map +1 -1
- package/lib/types/client/slots.d.ts +1 -0
- package/lib/types/client/slots.d.ts.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/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +35 -9
- package/lib/types/index.js.map +1 -1
- package/lib/types/listener.d.ts.map +1 -1
- package/lib/types/listener.js +2 -1
- package/lib/types/listener.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 +8 -8
- package/lib/types/model-proxy-contract.js.map +1 -1
- package/lib/types/package-version.generated.d.ts +2 -2
- package/lib/types/package-version.generated.d.ts.map +1 -1
- package/lib/types/package-version.generated.js +2 -2
- package/lib/types/package-version.generated.js.map +1 -1
- 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/settings-rpc-contract.d.ts +14 -14
- package/lib/types/settings-rpc-contract.d.ts.map +1 -1
- package/lib/types/settings-rpc-contract.js +14 -14
- package/lib/types/settings-rpc-contract.js.map +1 -1
- package/lib/types/settings-rpc.d.ts.map +1 -1
- package/lib/types/settings-rpc.js +2 -2
- 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/types.d.ts +1 -1
- package/lib/types/types.d.ts.map +1 -1
- package/lib/types/update-policy.d.ts +2 -2
- package/lib/types/update-policy.d.ts.map +1 -1
- package/package.json +82 -73
|
@@ -1,6 +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";
|
|
3
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
|
+
}
|
|
4
31
|
/** Turn a registration rejection into an actionable message without exposing remote response text. */
|
|
5
32
|
function registrationFailureMessage(failure) {
|
|
6
33
|
switch (failure.code) {
|
|
@@ -14,6 +41,8 @@ function registrationFailureMessage(failure) {
|
|
|
14
41
|
return '验证码状态已失效,请重新获取验证码后再注册。';
|
|
15
42
|
case 'handle-unavailable':
|
|
16
43
|
return '该 Handle 刚刚已被注册。请返回身份入口,再按恢复流程重新获取验证码。';
|
|
44
|
+
case 'short-handle-invite-required':
|
|
45
|
+
return SHORT_HANDLE_INVITE_MESSAGE;
|
|
17
46
|
case 'conflict':
|
|
18
47
|
return '注册冲突:服务端可能已收到上次注册请求,或该手机号 / Handle 已绑定其他身份。请保留当前页面并再次提交;若仍失败,请勿清除本机身份数据,联系管理员并提供失败时间。';
|
|
19
48
|
case 'rate-limited':
|
|
@@ -27,12 +56,14 @@ function registrationFailureMessage(failure) {
|
|
|
27
56
|
case 'remote':
|
|
28
57
|
return 'AWiki 服务暂时无法完成注册,请稍后重试;若持续失败,请联系管理员并提供失败时间。';
|
|
29
58
|
default:
|
|
30
|
-
return
|
|
59
|
+
return 'AWiki 服务暂时无法完成注册,请稍后重试;若持续失败,请联系管理员并提供失败时间。';
|
|
31
60
|
}
|
|
32
61
|
}
|
|
33
62
|
/** Turn a verification-code request failure into a safe next action. */
|
|
34
63
|
function registrationOtpFailureMessage(failure) {
|
|
35
64
|
switch (failure.code) {
|
|
65
|
+
case 'short-handle-invite-required':
|
|
66
|
+
return SHORT_HANDLE_INVITE_MESSAGE;
|
|
36
67
|
case 'rate-limited':
|
|
37
68
|
return '验证码发送过于频繁,请等待限流解除后再重新获取。';
|
|
38
69
|
case 'invalid-request':
|
|
@@ -62,7 +93,7 @@ function recoveryPreparationFailureMessage(failure) {
|
|
|
62
93
|
case 'remote':
|
|
63
94
|
return 'AWiki 服务暂时无法验证恢复信息,请稍后重试。';
|
|
64
95
|
default:
|
|
65
|
-
return
|
|
96
|
+
return 'AWiki 服务暂时无法验证恢复信息,请稍后重试。';
|
|
66
97
|
}
|
|
67
98
|
}
|
|
68
99
|
function recoveryContinuationFailureMessage(failure, phase) {
|
|
@@ -84,7 +115,7 @@ function recoveryContinuationFailureMessage(failure, phase) {
|
|
|
84
115
|
case 'forbidden':
|
|
85
116
|
return 'AWiki 服务暂时未能完成恢复。可以重新检查结果,或返回入口稍后继续。';
|
|
86
117
|
default:
|
|
87
|
-
return
|
|
118
|
+
return '身份恢复暂未完成。请保留当前恢复操作并重新检查状态,不要重新获取验证码或创建新身份。';
|
|
88
119
|
}
|
|
89
120
|
}
|
|
90
121
|
function identityAccessInspectionFailureMessage(failure) {
|
|
@@ -96,9 +127,10 @@ function identityAccessInspectionFailureMessage(failure) {
|
|
|
96
127
|
case 'remote':
|
|
97
128
|
return 'AWiki 服务暂时无法确认该 Handle 的状态,请稍后重试。';
|
|
98
129
|
default:
|
|
99
|
-
return
|
|
130
|
+
return 'AWiki 服务暂时无法确认该 Handle 的状态,请稍后重试。';
|
|
100
131
|
}
|
|
101
132
|
}
|
|
133
|
+
const IDENTITY_ACCESS_FAILURE_MESSAGE = '暂时无法读取本机身份状态,请稍后重新检查。';
|
|
102
134
|
const INITIAL_VIEW = Object.freeze({
|
|
103
135
|
status: 'cold',
|
|
104
136
|
sessionStatus: 'unregistered',
|
|
@@ -126,6 +158,7 @@ const INITIAL_VIEW = Object.freeze({
|
|
|
126
158
|
accessError: null,
|
|
127
159
|
recoveryOperationId: null,
|
|
128
160
|
recoveryProgress: null,
|
|
161
|
+
recoveryOtpRetryAt: null,
|
|
129
162
|
});
|
|
130
163
|
/** Turn a closed Host summary failure into one actionable Chinese message. */
|
|
131
164
|
function summaryFailureMessage(failure) {
|
|
@@ -135,7 +168,7 @@ function summaryFailureMessage(failure) {
|
|
|
135
168
|
case 'summary-cancelled': return 'AI 总结已取消,请重新生成。';
|
|
136
169
|
case 'summary-invalid-output': return '模型没有返回有效的结构化摘要,请重新生成。';
|
|
137
170
|
case 'summary-failed': return '暂时无法生成 AI 总结,请检查模型连接后重试。';
|
|
138
|
-
default: return
|
|
171
|
+
default: return '暂时无法生成 AI 总结,请检查模型连接后重试。';
|
|
139
172
|
}
|
|
140
173
|
}
|
|
141
174
|
/** Turn closed mail failures into safe, actionable browser messages. */
|
|
@@ -163,7 +196,7 @@ function groupCreateFailureMessage(failure) {
|
|
|
163
196
|
case 'network': return '无法连接 AWiki 服务,请检查网络后重试。';
|
|
164
197
|
case 'conflict': return '群聊创建状态发生冲突,请刷新群聊列表后确认是否已经创建。';
|
|
165
198
|
case 'remote': return 'AWiki 服务暂时无法创建群聊,请稍后重试。';
|
|
166
|
-
default: return
|
|
199
|
+
default: return 'AWiki 服务暂时无法创建群聊,请稍后重试。';
|
|
167
200
|
}
|
|
168
201
|
}
|
|
169
202
|
/** Keep group authorization failures actionable without exposing raw service text. */
|
|
@@ -182,7 +215,7 @@ function groupReadFailureMessage(failure) {
|
|
|
182
215
|
case 'remote':
|
|
183
216
|
return 'AWiki 暂时无法读取这个群聊,请稍后重试。';
|
|
184
217
|
default:
|
|
185
|
-
return
|
|
218
|
+
return 'AWiki 暂时无法读取这个群聊,请稍后重试。';
|
|
186
219
|
}
|
|
187
220
|
}
|
|
188
221
|
function conversationPreferenceFailureMessage(_failure) {
|
|
@@ -217,21 +250,18 @@ async function callGroupRead(operation) {
|
|
|
217
250
|
}
|
|
218
251
|
}
|
|
219
252
|
/** Flatten the carrier and business result once for every controller caller. */
|
|
220
|
-
async function call(operation, failureMessage =
|
|
253
|
+
async function call(operation, failureMessage = genericFailureMessage, carrierFailureMessage = genericCarrierFailureMessage) {
|
|
221
254
|
try {
|
|
222
255
|
const carried = await operation();
|
|
223
256
|
if (!carried.ok)
|
|
224
|
-
return { ok: false, error: carrierFailureMessage(carried.error
|
|
257
|
+
return { ok: false, error: carrierFailureMessage(carried.error) };
|
|
225
258
|
if (!carried.value.ok) {
|
|
226
259
|
return { ok: false, error: failureMessage(carried.value.error) };
|
|
227
260
|
}
|
|
228
261
|
return { ok: true, value: carried.value.value };
|
|
229
262
|
}
|
|
230
|
-
catch
|
|
231
|
-
return {
|
|
232
|
-
ok: false,
|
|
233
|
-
error: error instanceof Error ? `AWiki 调用失败:${error.message}` : 'AWiki 调用失败',
|
|
234
|
-
};
|
|
263
|
+
catch {
|
|
264
|
+
return { ok: false, error: GENERIC_THROW_FAILURE_MESSAGE };
|
|
235
265
|
}
|
|
236
266
|
}
|
|
237
267
|
/** Flatten the carrier and the isolated Guest Integration business result. */
|
|
@@ -239,9 +269,22 @@ async function callIntegration(operation) {
|
|
|
239
269
|
try {
|
|
240
270
|
const carried = await operation();
|
|
241
271
|
if (!carried.ok)
|
|
242
|
-
return { ok: false, error:
|
|
243
|
-
if (!carried.value.ok)
|
|
244
|
-
|
|
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
|
+
}
|
|
245
288
|
return { ok: true, value: carried.value.value };
|
|
246
289
|
}
|
|
247
290
|
catch {
|
|
@@ -249,31 +292,28 @@ async function callIntegration(operation) {
|
|
|
249
292
|
}
|
|
250
293
|
}
|
|
251
294
|
/** Preserve only the stable business code for controller-level state transitions. */
|
|
252
|
-
async function callWithFailureCode(operation) {
|
|
295
|
+
async function callWithFailureCode(operation, failureMessage = genericFailureMessage) {
|
|
253
296
|
try {
|
|
254
297
|
const carried = await operation();
|
|
255
298
|
if (!carried.ok)
|
|
256
|
-
return { ok: false, error:
|
|
299
|
+
return { ok: false, error: genericCarrierFailureMessage(carried.error) };
|
|
257
300
|
if (!carried.value.ok) {
|
|
258
301
|
return {
|
|
259
302
|
ok: false,
|
|
260
|
-
error:
|
|
303
|
+
error: failureMessage(carried.value.error),
|
|
261
304
|
failureCode: carried.value.error.code,
|
|
262
305
|
};
|
|
263
306
|
}
|
|
264
307
|
return { ok: true, value: carried.value.value };
|
|
265
308
|
}
|
|
266
|
-
catch
|
|
267
|
-
return {
|
|
268
|
-
ok: false,
|
|
269
|
-
error: error instanceof Error ? `AWiki 调用失败:${error.message}` : 'AWiki 调用失败',
|
|
270
|
-
};
|
|
309
|
+
catch {
|
|
310
|
+
return { ok: false, error: GENERIC_THROW_FAILURE_MESSAGE };
|
|
271
311
|
}
|
|
272
312
|
}
|
|
273
|
-
function recoveryCarrierFailureMessage(
|
|
274
|
-
return
|
|
313
|
+
function recoveryCarrierFailureMessage(failure) {
|
|
314
|
+
return failure.code === 'invalid-response'
|
|
275
315
|
? '恢复信息已验证,但暂时无法读取恢复状态。请稍后重试。'
|
|
276
|
-
:
|
|
316
|
+
: '暂时无法连接 AWiki Host,无法完成身份恢复。请稍后重试。';
|
|
277
317
|
}
|
|
278
318
|
/** Append unique values while retaining existing references. */
|
|
279
319
|
function appendUnique(current, incoming, id) {
|
|
@@ -373,11 +413,15 @@ const GROUP_HISTORY_RETRY_DELAYS_MS = [250, 750, 1_500, 2_500];
|
|
|
373
413
|
/** Runtime-only decoded-byte budget that prevents repeat Host calls while browsing. */
|
|
374
414
|
const BROWSER_IMAGE_ATTACHMENT_CACHE_MAX_BYTES = 32 * 1024 * 1024;
|
|
375
415
|
const RECOVERY_OPERATION_STORAGE_KEY = 'awiki.handle-recovery.operation.v1';
|
|
416
|
+
const RECOVERY_OTP_RETRY_STORAGE_KEY = 'awiki.handle-recovery.retry-at.v1';
|
|
376
417
|
function recoveryOperationStorageKey(tenantId) {
|
|
377
418
|
return tenantId === undefined || tenantId === ''
|
|
378
419
|
? RECOVERY_OPERATION_STORAGE_KEY
|
|
379
420
|
: `${RECOVERY_OPERATION_STORAGE_KEY}.${encodeURIComponent(tenantId)}`;
|
|
380
421
|
}
|
|
422
|
+
function recoveryTenantKey(tenantId) {
|
|
423
|
+
return tenantId === undefined || tenantId === '' ? 'default' : tenantId;
|
|
424
|
+
}
|
|
381
425
|
function storeRecoveryOperation(operationId, tenantId) {
|
|
382
426
|
try {
|
|
383
427
|
const key = recoveryOperationStorageKey(tenantId);
|
|
@@ -390,6 +434,49 @@ function storeRecoveryOperation(operationId, tenantId) {
|
|
|
390
434
|
// Recovery remains durable in Core even when browser storage is unavailable.
|
|
391
435
|
}
|
|
392
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
|
+
}
|
|
393
480
|
/** Wait between group-history readiness probes without retaining controller state. */
|
|
394
481
|
function delay(milliseconds) {
|
|
395
482
|
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
@@ -439,6 +526,8 @@ export class AwikiController {
|
|
|
439
526
|
drafts = new AwikiDraftStore();
|
|
440
527
|
view = INITIAL_VIEW;
|
|
441
528
|
listeners = new Set();
|
|
529
|
+
/** Runtime-only selected operations survive tenant round trips without reopening stale browser hints after reload. */
|
|
530
|
+
recoveryOperationsByTenant = new Map();
|
|
442
531
|
config = null;
|
|
443
532
|
conversationsCursor;
|
|
444
533
|
historyCursor;
|
|
@@ -521,6 +610,9 @@ export class AwikiController {
|
|
|
521
610
|
&& previous.sessionStatus === session.value.status && previous.identity?.did === identity?.did;
|
|
522
611
|
this.drafts.setScope(config.value.tenantId ?? 'default', identity?.did ?? session.value.status);
|
|
523
612
|
this.activatePresentationCache(identity);
|
|
613
|
+
const selectedRecovery = previousTenant === config.value.tenantId
|
|
614
|
+
? previous.recoveryOperationId
|
|
615
|
+
: this.recoveryOperationsByTenant.get(recoveryTenantKey(config.value.tenantId)) ?? null;
|
|
524
616
|
this.publish({
|
|
525
617
|
...(sameOwner ? this.view : INITIAL_VIEW),
|
|
526
618
|
status: 'ready',
|
|
@@ -529,7 +621,8 @@ export class AwikiController {
|
|
|
529
621
|
error: null,
|
|
530
622
|
attachmentMaxBytes: config.value.attachmentMaxBytes,
|
|
531
623
|
handleRecoveryPhoneEnabled: config.value.handleRecoveryPhoneEnabled,
|
|
532
|
-
recoveryOperationId:
|
|
624
|
+
recoveryOperationId: selectedRecovery,
|
|
625
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(selectedRecovery, config.value.tenantId),
|
|
533
626
|
});
|
|
534
627
|
await this.refreshIdentityAccess();
|
|
535
628
|
if (!this.current(generation))
|
|
@@ -684,7 +777,10 @@ export class AwikiController {
|
|
|
684
777
|
* @returns challenge retry metadata or one display-safe failure.
|
|
685
778
|
*/
|
|
686
779
|
async sendRegistrationOtp(request) {
|
|
687
|
-
|
|
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;
|
|
688
784
|
}
|
|
689
785
|
/** Discover Core-owned work without selecting an account or activating a session. */
|
|
690
786
|
refreshIdentityAccess() {
|
|
@@ -705,28 +801,36 @@ export class AwikiController {
|
|
|
705
801
|
if (!this.recoveryCurrent(generation, revision))
|
|
706
802
|
return this.recoveryLeft();
|
|
707
803
|
if (!result.ok) {
|
|
708
|
-
this.publish({ ...this.view, accessLoading: false, accessError:
|
|
709
|
-
return
|
|
804
|
+
this.publish({ ...this.view, accessLoading: false, accessError: IDENTITY_ACCESS_FAILURE_MESSAGE });
|
|
805
|
+
return { ok: false, error: IDENTITY_ACCESS_FAILURE_MESSAGE };
|
|
710
806
|
}
|
|
711
807
|
const candidates = result.value.recoveries;
|
|
712
808
|
const selected = this.view.recoveryOperationId;
|
|
713
|
-
this.publish({ ...this.view, identityAccess: result.value
|
|
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) });
|
|
714
812
|
if (selected !== null) {
|
|
715
813
|
const status = await callWithFailureCode(() => this.remote.getRecoveryStatus({ operationId: selected }));
|
|
716
814
|
if (!this.recoveryCurrent(generation, revision))
|
|
717
815
|
return this.recoveryLeft();
|
|
718
816
|
if (!status.ok) {
|
|
719
817
|
if (status.failureCode === 'not-found' && candidates.length === 0) {
|
|
720
|
-
|
|
721
|
-
this.
|
|
818
|
+
clearRecoveryOtpRetryAt(selected, this.config?.tenantId);
|
|
819
|
+
this.rememberRecoveryOperation(null, this.config?.tenantId);
|
|
820
|
+
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, recoveryOtpRetryAt: null });
|
|
722
821
|
}
|
|
723
822
|
else {
|
|
724
|
-
this.publish({ ...this.view, accessLoading: false, accessError:
|
|
725
|
-
return { ok: false, error:
|
|
823
|
+
this.publish({ ...this.view, accessLoading: false, accessError: IDENTITY_ACCESS_FAILURE_MESSAGE });
|
|
824
|
+
return { ok: false, error: IDENTITY_ACCESS_FAILURE_MESSAGE };
|
|
726
825
|
}
|
|
727
826
|
}
|
|
728
827
|
else {
|
|
729
|
-
|
|
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 });
|
|
730
834
|
}
|
|
731
835
|
}
|
|
732
836
|
this.publish({ ...this.view, accessLoading: false, accessError: null });
|
|
@@ -740,7 +844,9 @@ export class AwikiController {
|
|
|
740
844
|
this.leaveRecovery();
|
|
741
845
|
else
|
|
742
846
|
this.recoveryRevision++;
|
|
743
|
-
this.
|
|
847
|
+
this.rememberRecoveryOperation(operationId, this.config?.tenantId);
|
|
848
|
+
this.publish({ ...this.view, recoveryOperationId: operationId, recoveryProgress: null,
|
|
849
|
+
recoveryOtpRetryAt: storedRecoveryOtpRetryAt(operationId, this.config?.tenantId) });
|
|
744
850
|
const result = await this.refreshRecoveryStatus();
|
|
745
851
|
return result.ok ? { ok: true, value: undefined } : result;
|
|
746
852
|
}
|
|
@@ -752,6 +858,16 @@ export class AwikiController {
|
|
|
752
858
|
recoveryLeft() {
|
|
753
859
|
return { ok: false, error: '已离开此恢复流程;进度保留在本机。' };
|
|
754
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
|
+
}
|
|
755
871
|
/** Navigation only. Never cancel or delete the Core-owned operation. */
|
|
756
872
|
leaveRecovery() {
|
|
757
873
|
this.recoveryRevision++;
|
|
@@ -761,7 +877,7 @@ export class AwikiController {
|
|
|
761
877
|
this.pendingRequest = undefined;
|
|
762
878
|
this.pendingRecovery = false;
|
|
763
879
|
this.drafts.clearFlow();
|
|
764
|
-
|
|
880
|
+
this.rememberRecoveryOperation(null, this.config?.tenantId);
|
|
765
881
|
this.publish({ ...this.view, recoveryOperationId: null, recoveryProgress: null, accessError: null,
|
|
766
882
|
accessLoading: false, pending: wasPending ? null : this.view.pending, error: null });
|
|
767
883
|
}
|
|
@@ -839,6 +955,68 @@ export class AwikiController {
|
|
|
839
955
|
return result.ok ? { ok: true, value: undefined } : result;
|
|
840
956
|
});
|
|
841
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
|
+
}
|
|
842
1020
|
retireDeviceIdentityForRejoin() {
|
|
843
1021
|
return this.withPending('准备重新加入设备', async () => {
|
|
844
1022
|
const result = await call(() => this.remote.retireDeviceIdentityForRejoin());
|
|
@@ -912,17 +1090,29 @@ export class AwikiController {
|
|
|
912
1090
|
async sendRecoveryOtp(request) {
|
|
913
1091
|
const generation = this.generation;
|
|
914
1092
|
const revision = this.recoveryRevision;
|
|
1093
|
+
const tenantId = this.config?.tenantId;
|
|
1094
|
+
const flowScope = this.drafts.getScope('recovery:retryDeadline');
|
|
915
1095
|
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
1096
|
if (!result.ok) {
|
|
1097
|
+
if (!this.recoveryCurrent(generation, revision))
|
|
1098
|
+
return this.recoveryLeft();
|
|
919
1099
|
await this.continueRecoveryForHandle(request.fullHandle);
|
|
920
1100
|
return result;
|
|
921
1101
|
}
|
|
922
|
-
this.
|
|
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) });
|
|
923
1109
|
await this.refreshRecoveryStatus();
|
|
924
1110
|
return this.recoveryCurrent(generation, revision) ? result : this.recoveryLeft();
|
|
925
1111
|
}
|
|
1112
|
+
recordRecoveryOtp(result, tenantId) {
|
|
1113
|
+
this.rememberRecoveryOperation(result.operationId, tenantId);
|
|
1114
|
+
storeRecoveryOtpRetryAt(result.operationId, recoveryOtpRetryAt(result), tenantId);
|
|
1115
|
+
}
|
|
926
1116
|
/** Publish results only into the exact selected flow; Core work survives navigation. */
|
|
927
1117
|
async runRecovery(label, operation, enterWhenApplied = false) {
|
|
928
1118
|
const generation = this.generation;
|
|
@@ -935,14 +1125,16 @@ export class AwikiController {
|
|
|
935
1125
|
return this.recoveryLeft();
|
|
936
1126
|
if (!result.ok) {
|
|
937
1127
|
// Reconcile authority after an ambiguous failure, never infer whether Commit occurred.
|
|
938
|
-
const status = await call(() => this.remote.getRecoveryStatus({ operationId }));
|
|
1128
|
+
const status = await call(() => this.remote.getRecoveryStatus({ operationId }), undefined, recoveryCarrierFailureMessage);
|
|
939
1129
|
if (!this.recoveryCurrent(generation, revision))
|
|
940
1130
|
return this.recoveryLeft();
|
|
941
1131
|
this.publish({ ...this.view, recoveryProgress: status.ok ? status.value : this.view.recoveryProgress,
|
|
942
|
-
accessError: status.ok ? null :
|
|
1132
|
+
accessError: status.ok ? null : IDENTITY_ACCESS_FAILURE_MESSAGE });
|
|
943
1133
|
return result;
|
|
944
1134
|
}
|
|
945
1135
|
this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
|
|
1136
|
+
if (result.value.phase === 'applied')
|
|
1137
|
+
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
946
1138
|
if (enterWhenApplied && result.value.phase === 'applied')
|
|
947
1139
|
return this.enterRecoveredSession();
|
|
948
1140
|
return result;
|
|
@@ -960,7 +1152,25 @@ export class AwikiController {
|
|
|
960
1152
|
}
|
|
961
1153
|
/** Read-only even for applied operations. Entering the identity is a separate action. */
|
|
962
1154
|
async refreshRecoveryStatus() {
|
|
963
|
-
|
|
1155
|
+
const generation = this.generation;
|
|
1156
|
+
const revision = this.recoveryRevision;
|
|
1157
|
+
const operationId = this.view.recoveryOperationId;
|
|
1158
|
+
if (operationId === null)
|
|
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 });
|
|
1168
|
+
return result;
|
|
1169
|
+
}
|
|
1170
|
+
this.publish({ ...this.view, recoveryProgress: result.value, accessError: null });
|
|
1171
|
+
if (result.value.phase === 'applied')
|
|
1172
|
+
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
1173
|
+
return result;
|
|
964
1174
|
}
|
|
965
1175
|
async resumeRecovery() {
|
|
966
1176
|
const progress = this.view.recoveryProgress;
|
|
@@ -980,6 +1190,7 @@ export class AwikiController {
|
|
|
980
1190
|
return this.recoveryLeft();
|
|
981
1191
|
if (!result.ok)
|
|
982
1192
|
return result;
|
|
1193
|
+
clearRecoveryOtpRetryAt(progress.operationId, this.config?.tenantId);
|
|
983
1194
|
this.leaveRecovery();
|
|
984
1195
|
const opened = await this.open();
|
|
985
1196
|
return opened.ok ? result : opened;
|
|
@@ -999,6 +1210,7 @@ export class AwikiController {
|
|
|
999
1210
|
return this.recoveryLeft();
|
|
1000
1211
|
if (!result.ok)
|
|
1001
1212
|
return result;
|
|
1213
|
+
clearRecoveryOtpRetryAt(operationId, this.config?.tenantId);
|
|
1002
1214
|
this.leaveRecovery();
|
|
1003
1215
|
void this.refreshIdentityAccess();
|
|
1004
1216
|
return { ok: true, value: undefined };
|
|
@@ -1132,10 +1344,15 @@ export class AwikiController {
|
|
|
1132
1344
|
return this.selectConversation(existing.id);
|
|
1133
1345
|
}
|
|
1134
1346
|
const generation = this.generation;
|
|
1135
|
-
const resolved = await this.withPending('查找用户',
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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;
|
|
1139
1356
|
if (!this.current(generation))
|
|
1140
1357
|
return { ok: true, value: undefined };
|
|
1141
1358
|
if (resolved.value.did === identity.did)
|
|
@@ -1811,18 +2028,22 @@ export class AwikiController {
|
|
|
1811
2028
|
}
|
|
1812
2029
|
catch { }
|
|
1813
2030
|
}
|
|
1814
|
-
|
|
2031
|
+
clearRecoveryOtpRetryAt(this.view.recoveryOperationId, tenantId);
|
|
2032
|
+
this.rememberRecoveryOperation(null, tenantId);
|
|
1815
2033
|
if (!this.current(generation))
|
|
1816
2034
|
return result;
|
|
1817
2035
|
this.drafts.clearScope();
|
|
1818
2036
|
this.close();
|
|
1819
|
-
|
|
2037
|
+
// Preserve tenantId for the confirm-time getConfig scope check without reopening.
|
|
1820
2038
|
this.conversationsCursor = undefined;
|
|
1821
2039
|
this.historyCursor = undefined;
|
|
1822
2040
|
this.unreadAtOpen.clear();
|
|
1823
2041
|
this.summaryBaselines.clear();
|
|
1824
2042
|
this.clearPresentationCache();
|
|
1825
|
-
this.publish({ ...INITIAL_VIEW, status: 'ready', accessLoading: false,
|
|
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: [] } });
|
|
1826
2047
|
return result;
|
|
1827
2048
|
}
|
|
1828
2049
|
/** Read the Integration without coupling Guest Gateway health to the main AWiki view. */
|