@xmanrui/dsh-im 4.21.2 → 4.23.0
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.en.md +10 -5
- package/README.md +10 -5
- package/docs/client-integration.md +81 -0
- package/lib/client.js +2565 -1134
- package/lib/index.js +292 -301
- package/package.json +9 -1
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +16 -3
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +5 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +62 -0
- package/plugin-src/client/index.js +122 -28
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/panel-error-boundary.js +21 -0
- package/plugin-src/client/styles.js +6 -0
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +5 -1
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +6 -1
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +5 -1
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +55 -6
- package/plugin-src/host/channels/shared/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/slack/production.mjs +5 -1
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +5 -1
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +5 -1
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +5 -1
- package/plugin-src/host/channels/whatsapp/production.mjs +5 -1
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +3 -1
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +692 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +593 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +8 -7
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +37 -21
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/default-workspace.mjs +28 -0
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +111 -20
- package/src/channels/shared/harness-session-binding.mjs +13 -8
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +90 -11
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-runtime.mjs +17 -13
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
|
|
1
2
|
import { randomUUID } from 'node:crypto';
|
|
2
3
|
import { connectionTestMessage } from '../shared/connection-test.mjs';
|
|
3
4
|
import { publicMessageFailure } from '../shared/message-failure.mjs';
|
|
@@ -115,6 +116,7 @@ function optionalNonEmptyString(value) {
|
|
|
115
116
|
* runtime lifecycles may proceed independently.
|
|
116
117
|
*/
|
|
117
118
|
export class MultiBotDshFeishuController {
|
|
119
|
+
#diagnostics;
|
|
118
120
|
#registerApp;
|
|
119
121
|
#verifyApp;
|
|
120
122
|
#credentials;
|
|
@@ -138,6 +140,7 @@ export class MultiBotDshFeishuController {
|
|
|
138
140
|
constructor({
|
|
139
141
|
registerApp,
|
|
140
142
|
verifyApp,
|
|
143
|
+
logger = console,
|
|
141
144
|
credentials,
|
|
142
145
|
configStore,
|
|
143
146
|
createRuntime,
|
|
@@ -159,6 +162,7 @@ export class MultiBotDshFeishuController {
|
|
|
159
162
|
|| callbackProbeTimeoutMs > MAX_CALLBACK_PROBE_TIMEOUT_MS) {
|
|
160
163
|
throw new TypeError('callbackProbeTimeoutMs must be between 1 and 600000ms');
|
|
161
164
|
}
|
|
165
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'feishu', logger });
|
|
162
166
|
this.#registerApp = registerApp;
|
|
163
167
|
this.#verifyApp = verifyApp;
|
|
164
168
|
this.#credentials = credentials;
|
|
@@ -191,12 +195,12 @@ export class MultiBotDshFeishuController {
|
|
|
191
195
|
}
|
|
192
196
|
let resolved;
|
|
193
197
|
try {
|
|
194
|
-
resolved = await this.#credentials.resolve(config.secretRef);
|
|
195
|
-
} catch {
|
|
196
|
-
this.#botErrors.set(config.id, {
|
|
198
|
+
resolved = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
|
|
199
|
+
} catch (error) {
|
|
200
|
+
this.#botErrors.set(config.id, this.#diagnostics.report(error, { reuse: true, botId: config.id, publicError: {
|
|
197
201
|
code: 'missing_credentials',
|
|
198
202
|
message: '无法读取机器人凭据,请检查凭据存储。',
|
|
199
|
-
});
|
|
203
|
+
} }).publicError);
|
|
200
204
|
return;
|
|
201
205
|
}
|
|
202
206
|
if (!resolved?.value) {
|
|
@@ -209,11 +213,11 @@ export class MultiBotDshFeishuController {
|
|
|
209
213
|
try {
|
|
210
214
|
await this.#startRuntime(config, resolved.value);
|
|
211
215
|
this.#botErrors.delete(config.id);
|
|
212
|
-
} catch {
|
|
213
|
-
this.#botErrors.set(config.id, {
|
|
216
|
+
} catch (error) {
|
|
217
|
+
this.#botErrors.set(config.id, this.#diagnostics.report(error, { reuse: true, botId: config.id, stage: 'connection.start', publicError: {
|
|
214
218
|
code: 'connection_failed',
|
|
215
219
|
message: '机器人暂时无法连接飞书,请重试。',
|
|
216
|
-
});
|
|
220
|
+
} }).publicError);
|
|
217
221
|
}
|
|
218
222
|
})));
|
|
219
223
|
if (attempted) this.#touch();
|
|
@@ -228,6 +232,7 @@ export class MultiBotDshFeishuController {
|
|
|
228
232
|
}
|
|
229
233
|
const record = { id, manager: null, botId: null, createdNew: false, cancelled: false };
|
|
230
234
|
record.manager = new RegistrationManager({
|
|
235
|
+
diagnostics: this.#diagnostics,
|
|
231
236
|
registerApp: this.#registerApp,
|
|
232
237
|
onCredentials: (result) => this.#serializeConfig(() => this.#acceptCredentials(record, result)),
|
|
233
238
|
});
|
|
@@ -290,6 +295,7 @@ export class MultiBotDshFeishuController {
|
|
|
290
295
|
},
|
|
291
296
|
};
|
|
292
297
|
record.manager = new CallbackRepairManager({
|
|
298
|
+
diagnostics: this.#diagnostics,
|
|
293
299
|
registerApp: this.#registerApp,
|
|
294
300
|
appId: target.appId,
|
|
295
301
|
domain: target.domain,
|
|
@@ -349,6 +355,7 @@ export class MultiBotDshFeishuController {
|
|
|
349
355
|
initiator: { actorOpenId: null, chatId: null },
|
|
350
356
|
};
|
|
351
357
|
record.manager = new GroupMessagePermissionManager({
|
|
358
|
+
diagnostics: this.#diagnostics,
|
|
352
359
|
registerApp: this.#registerApp,
|
|
353
360
|
appId: target.appId,
|
|
354
361
|
domain: target.domain,
|
|
@@ -407,6 +414,8 @@ export class MultiBotDshFeishuController {
|
|
|
407
414
|
return this.registrationStatus(attemptId) ?? this.status();
|
|
408
415
|
}
|
|
409
416
|
|
|
417
|
+
get diagnostics() { return this.#diagnostics; }
|
|
418
|
+
|
|
410
419
|
status(botId) {
|
|
411
420
|
return this.#status({
|
|
412
421
|
registration: this.#registrations.get(this.#latestRegistrationId) ?? null,
|
|
@@ -440,7 +449,7 @@ export class MultiBotDshFeishuController {
|
|
|
440
449
|
throw new Error('Bot id generator returned an invalid or duplicate id');
|
|
441
450
|
}
|
|
442
451
|
const secretRef = existing?.secretRef ?? secretRefFor(botId);
|
|
443
|
-
const previousSecret = await this.#credentials.resolve(secretRef).catch(() => undefined);
|
|
452
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(secretRef), 'credential-store').catch(() => undefined);
|
|
444
453
|
const config = {
|
|
445
454
|
...existing,
|
|
446
455
|
id: botId,
|
|
@@ -460,7 +469,7 @@ export class MultiBotDshFeishuController {
|
|
|
460
469
|
createdAt: existing?.createdAt ?? new Date().toISOString(),
|
|
461
470
|
};
|
|
462
471
|
|
|
463
|
-
await this.#credentials.set(secretRef, normalizedSecret);
|
|
472
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(secretRef, normalizedSecret), 'credential-store');
|
|
464
473
|
let saved;
|
|
465
474
|
try {
|
|
466
475
|
saved = await this.#configStore.saveBot(config);
|
|
@@ -473,11 +482,11 @@ export class MultiBotDshFeishuController {
|
|
|
473
482
|
try {
|
|
474
483
|
await this.#startRuntime(saved, normalizedSecret);
|
|
475
484
|
this.#botErrors.delete(botId);
|
|
476
|
-
} catch {
|
|
477
|
-
this.#botErrors.set(botId, {
|
|
485
|
+
} catch (error) {
|
|
486
|
+
this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
|
|
478
487
|
code: 'connection_failed',
|
|
479
488
|
message: '机器人已经绑定,但长连接未就绪,请点击重试。',
|
|
480
|
-
});
|
|
489
|
+
} }).publicError);
|
|
481
490
|
}
|
|
482
491
|
});
|
|
483
492
|
this.#touch();
|
|
@@ -501,8 +510,8 @@ export class MultiBotDshFeishuController {
|
|
|
501
510
|
}
|
|
502
511
|
let resolved;
|
|
503
512
|
try {
|
|
504
|
-
resolved = await this.#credentials.resolve(config.secretRef);
|
|
505
|
-
} catch {
|
|
513
|
+
resolved = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.secretRef), 'credential-store');
|
|
514
|
+
} catch (error) {
|
|
506
515
|
resolved = null;
|
|
507
516
|
}
|
|
508
517
|
if (!resolved?.value) {
|
|
@@ -516,11 +525,11 @@ export class MultiBotDshFeishuController {
|
|
|
516
525
|
try {
|
|
517
526
|
await this.#startRuntime(config, resolved.value);
|
|
518
527
|
this.#botErrors.delete(botId);
|
|
519
|
-
} catch {
|
|
520
|
-
this.#botErrors.set(botId, {
|
|
528
|
+
} catch (error) {
|
|
529
|
+
this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, stage: 'connection.start', publicError: {
|
|
521
530
|
code: 'connection_failed',
|
|
522
531
|
message: '机器人暂时无法连接飞书,请重试。',
|
|
523
|
-
});
|
|
532
|
+
} }).publicError);
|
|
524
533
|
}
|
|
525
534
|
this.#touch();
|
|
526
535
|
return this.status(botId);
|
|
@@ -691,7 +700,7 @@ export class MultiBotDshFeishuController {
|
|
|
691
700
|
const bots = this.#configStore.list().map((config) => {
|
|
692
701
|
const connection = connectionStatus(this.#runtimes.get(config.id));
|
|
693
702
|
const connected = isConnected(connection);
|
|
694
|
-
const error = this.#botErrors.get(config.id) ?? null;
|
|
703
|
+
const error = connectionStatus(this.#runtimes.get(config.id)).error ?? this.#botErrors.get(config.id) ?? null;
|
|
695
704
|
return {
|
|
696
705
|
botId: config.id,
|
|
697
706
|
phase: botPhase({ connected, error, connection }),
|
|
@@ -832,7 +841,7 @@ export class MultiBotDshFeishuController {
|
|
|
832
841
|
|
|
833
842
|
let previous;
|
|
834
843
|
try {
|
|
835
|
-
previous = await this.#credentials.resolve(current.secretRef);
|
|
844
|
+
previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store');
|
|
836
845
|
} catch (error) {
|
|
837
846
|
throw this.#callbackRepairError(
|
|
838
847
|
record,
|
|
@@ -844,9 +853,9 @@ export class MultiBotDshFeishuController {
|
|
|
844
853
|
if (previous?.value !== appSecret) {
|
|
845
854
|
record.stage = 'persisting_secret';
|
|
846
855
|
try {
|
|
847
|
-
await this.#credentials.set(current.secretRef, appSecret);
|
|
856
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(current.secretRef, appSecret), 'credential-store');
|
|
848
857
|
} catch (writeError) {
|
|
849
|
-
const observed = await this.#credentials.resolve(current.secretRef).catch(() => null);
|
|
858
|
+
const observed = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
|
|
850
859
|
if (observed?.value !== appSecret) {
|
|
851
860
|
throw this.#callbackRepairError(
|
|
852
861
|
record,
|
|
@@ -856,7 +865,7 @@ export class MultiBotDshFeishuController {
|
|
|
856
865
|
);
|
|
857
866
|
}
|
|
858
867
|
}
|
|
859
|
-
const persisted = await this.#credentials.resolve(current.secretRef).catch(() => null);
|
|
868
|
+
const persisted = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
|
|
860
869
|
if (persisted?.value !== appSecret) {
|
|
861
870
|
throw this.#callbackRepairError(
|
|
862
871
|
record,
|
|
@@ -887,10 +896,10 @@ export class MultiBotDshFeishuController {
|
|
|
887
896
|
try {
|
|
888
897
|
await this.#startRuntime(saved, appSecret);
|
|
889
898
|
} catch (error) {
|
|
890
|
-
this.#botErrors.set(record.botId, {
|
|
899
|
+
this.#botErrors.set(record.botId, this.#diagnostics.report(error, { reuse: true, botId: record.botId, stage: 'connection.start', publicError: {
|
|
891
900
|
code: 'connection_failed',
|
|
892
901
|
message: '群消息权限已开通,但机器人长连接未就绪,请点击重试。',
|
|
893
|
-
});
|
|
902
|
+
} }).publicError);
|
|
894
903
|
this.#touch();
|
|
895
904
|
throw this.#callbackRepairError(
|
|
896
905
|
record,
|
|
@@ -981,7 +990,7 @@ export class MultiBotDshFeishuController {
|
|
|
981
990
|
|
|
982
991
|
let previous;
|
|
983
992
|
try {
|
|
984
|
-
previous = await this.#credentials.resolve(current.secretRef);
|
|
993
|
+
previous = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store');
|
|
985
994
|
} catch (error) {
|
|
986
995
|
throw this.#callbackRepairError(
|
|
987
996
|
record,
|
|
@@ -994,9 +1003,9 @@ export class MultiBotDshFeishuController {
|
|
|
994
1003
|
if (credentialChanged) {
|
|
995
1004
|
record.stage = 'persisting_secret';
|
|
996
1005
|
try {
|
|
997
|
-
await this.#credentials.set(current.secretRef, appSecret);
|
|
1006
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(current.secretRef, appSecret), 'credential-store');
|
|
998
1007
|
} catch (writeError) {
|
|
999
|
-
const observed = await this.#credentials.resolve(current.secretRef).catch(() => null);
|
|
1008
|
+
const observed = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
|
|
1000
1009
|
if (observed?.value !== appSecret) {
|
|
1001
1010
|
throw this.#callbackRepairError(
|
|
1002
1011
|
record,
|
|
@@ -1006,7 +1015,7 @@ export class MultiBotDshFeishuController {
|
|
|
1006
1015
|
);
|
|
1007
1016
|
}
|
|
1008
1017
|
}
|
|
1009
|
-
const persisted = await this.#credentials.resolve(current.secretRef).catch(() => null);
|
|
1018
|
+
const persisted = await atConnectionStage('credential.read', () => this.#credentials.resolve(current.secretRef), 'credential-store').catch(() => null);
|
|
1010
1019
|
if (persisted?.value !== appSecret) {
|
|
1011
1020
|
throw this.#callbackRepairError(
|
|
1012
1021
|
record,
|
|
@@ -1029,10 +1038,10 @@ export class MultiBotDshFeishuController {
|
|
|
1029
1038
|
// The returned credential was already verified and persisted. Do
|
|
1030
1039
|
// not restore a potentially revoked old secret; reconnectBot can
|
|
1031
1040
|
// safely retry this forward state later.
|
|
1032
|
-
this.#botErrors.set(record.botId, {
|
|
1041
|
+
this.#botErrors.set(record.botId, this.#diagnostics.report(error, { reuse: true, botId: record.botId, publicError: {
|
|
1033
1042
|
code: 'connection_failed',
|
|
1034
1043
|
message: '机器人回调修复已保存,但长连接未就绪,请点击重试。',
|
|
1035
|
-
});
|
|
1044
|
+
} }).publicError);
|
|
1036
1045
|
this.#touch();
|
|
1037
1046
|
throw this.#callbackRepairError(
|
|
1038
1047
|
record,
|
|
@@ -1120,8 +1129,8 @@ export class MultiBotDshFeishuController {
|
|
|
1120
1129
|
}
|
|
1121
1130
|
const secretRef = existing?.secretRef ?? secretRefFor(botId);
|
|
1122
1131
|
const previousOwnership = this.#botOwnership.get(botId);
|
|
1123
|
-
const previousSecret = await this.#credentials.resolve(secretRef).catch(() => undefined);
|
|
1124
|
-
await this.#credentials.set(secretRef, appSecret);
|
|
1132
|
+
const previousSecret = await atConnectionStage('credential.read', () => this.#credentials.resolve(secretRef), 'credential-store').catch(() => undefined);
|
|
1133
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(secretRef, appSecret), 'credential-store');
|
|
1125
1134
|
let config;
|
|
1126
1135
|
try {
|
|
1127
1136
|
config = await this.#configStore.saveBot({
|
|
@@ -1219,29 +1228,29 @@ export class MultiBotDshFeishuController {
|
|
|
1219
1228
|
this.#botErrors.delete(botId);
|
|
1220
1229
|
} else {
|
|
1221
1230
|
await this.#withBotTransition(botId, () => this.#stopRuntime(botId));
|
|
1222
|
-
this.#botErrors.set(botId, {
|
|
1231
|
+
this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
|
|
1223
1232
|
code: 'deletion_pending',
|
|
1224
1233
|
message: '机器人正在等待完成本地删除,请重试移除。',
|
|
1225
|
-
});
|
|
1234
|
+
} }).publicError);
|
|
1226
1235
|
}
|
|
1227
1236
|
this.#touch();
|
|
1228
1237
|
throw error;
|
|
1229
1238
|
} catch (restoreError) {
|
|
1230
1239
|
if (restoreError === error) throw error;
|
|
1231
|
-
this.#botErrors.set(botId, {
|
|
1240
|
+
this.#botErrors.set(botId, this.#diagnostics.report(restoreError, { reuse: true, botId: botId, publicError: {
|
|
1232
1241
|
code: 'connection_failed',
|
|
1233
1242
|
message: '机器人连接更新失败,且原连接无法恢复,请重试。',
|
|
1234
|
-
});
|
|
1243
|
+
} }).publicError);
|
|
1235
1244
|
this.#touch();
|
|
1236
1245
|
throw new Error('Unable to restore the previous Feishu bot connection.', {
|
|
1237
1246
|
cause: restoreError,
|
|
1238
1247
|
});
|
|
1239
1248
|
}
|
|
1240
1249
|
}
|
|
1241
|
-
this.#botErrors.set(botId, {
|
|
1250
|
+
this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
|
|
1242
1251
|
code: 'connection_failed',
|
|
1243
1252
|
message: '机器人已经创建,但长连接未就绪,请点击重试。',
|
|
1244
|
-
});
|
|
1253
|
+
} }).publicError);
|
|
1245
1254
|
this.#touch();
|
|
1246
1255
|
throw error;
|
|
1247
1256
|
}
|
|
@@ -1249,12 +1258,12 @@ export class MultiBotDshFeishuController {
|
|
|
1249
1258
|
|
|
1250
1259
|
async #startRuntime(config, appSecret) {
|
|
1251
1260
|
await this.#stopRuntime(config.id);
|
|
1252
|
-
const runtime = await this.#createRuntime({
|
|
1261
|
+
const runtime = await atConnectionStage('runtime.prepare', () => this.#createRuntime({
|
|
1253
1262
|
botId: config.id,
|
|
1254
1263
|
config,
|
|
1255
1264
|
appSecret,
|
|
1256
1265
|
repair: this.#runtimeRepairCapability(config.id),
|
|
1257
|
-
});
|
|
1266
|
+
}));
|
|
1258
1267
|
this.#runtimes.set(config.id, runtime);
|
|
1259
1268
|
try {
|
|
1260
1269
|
await runtime.start();
|
|
@@ -1300,21 +1309,21 @@ export class MultiBotDshFeishuController {
|
|
|
1300
1309
|
}
|
|
1301
1310
|
await this.#stopRuntime(botId);
|
|
1302
1311
|
try {
|
|
1303
|
-
await this.#credentials.unset(config.secretRef);
|
|
1312
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.secretRef), 'credential-store');
|
|
1304
1313
|
} catch (error) {
|
|
1305
|
-
this.#botErrors.set(botId, {
|
|
1314
|
+
this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
|
|
1306
1315
|
code: 'credential_removal_failed',
|
|
1307
1316
|
message: '无法删除机器人凭据,请稍后重试。',
|
|
1308
|
-
});
|
|
1317
|
+
} }).publicError);
|
|
1309
1318
|
throw new Error('Unable to remove the Feishu credential.', { cause: error });
|
|
1310
1319
|
}
|
|
1311
1320
|
try {
|
|
1312
1321
|
await this.#deleteState({ botId, config });
|
|
1313
1322
|
} catch (error) {
|
|
1314
|
-
this.#botErrors.set(botId, {
|
|
1323
|
+
this.#botErrors.set(botId, this.#diagnostics.report(error, { reuse: true, botId: botId, publicError: {
|
|
1315
1324
|
code: 'state_cleanup_failed',
|
|
1316
1325
|
message: '无法删除机器人的本地会话数据,请稍后重试。',
|
|
1317
|
-
});
|
|
1326
|
+
} }).publicError);
|
|
1318
1327
|
throw new Error('Unable to remove the Feishu bot session state.', { cause: error });
|
|
1319
1328
|
}
|
|
1320
1329
|
await this.#configStore.removeBot(botId);
|
|
@@ -1323,8 +1332,8 @@ export class MultiBotDshFeishuController {
|
|
|
1323
1332
|
}
|
|
1324
1333
|
|
|
1325
1334
|
async #restoreCredential(secretRef, previous) {
|
|
1326
|
-
if (previous?.value) await this.#credentials.set(secretRef, previous.value);
|
|
1327
|
-
else await this.#credentials.unset(secretRef);
|
|
1335
|
+
if (previous?.value) await atConnectionStage('credential.save', () => this.#credentials.set(secretRef, previous.value), 'credential-store');
|
|
1336
|
+
else await atConnectionStage('credential.remove', () => this.#credentials.unset(secretRef), 'credential-store');
|
|
1328
1337
|
}
|
|
1329
1338
|
|
|
1330
1339
|
#requireBot(botId) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConnectionDiagnostics } from '../shared/connection-error.mjs';
|
|
1
2
|
const ACTIVE_STATES = new Set([
|
|
2
3
|
'starting',
|
|
3
4
|
'qr_ready',
|
|
@@ -74,6 +75,7 @@ function copyUserInfo(userInfo) {
|
|
|
74
75
|
* `onCredentials` callback.
|
|
75
76
|
*/
|
|
76
77
|
export class RegistrationManager {
|
|
78
|
+
#diagnostics;
|
|
77
79
|
#registerApp;
|
|
78
80
|
#onCredentials;
|
|
79
81
|
#now;
|
|
@@ -86,6 +88,7 @@ export class RegistrationManager {
|
|
|
86
88
|
constructor({
|
|
87
89
|
registerApp,
|
|
88
90
|
onCredentials,
|
|
91
|
+
diagnostics = createConnectionDiagnostics({ channel: 'feishu' }),
|
|
89
92
|
now = Date.now,
|
|
90
93
|
setTimeout: setTimeoutFn = globalThis.setTimeout,
|
|
91
94
|
clearTimeout: clearTimeoutFn = globalThis.clearTimeout,
|
|
@@ -100,6 +103,7 @@ export class RegistrationManager {
|
|
|
100
103
|
throw new TypeError('RegistrationManager clock dependencies must be functions');
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
this.#diagnostics = diagnostics;
|
|
103
107
|
this.#registerApp = registerApp;
|
|
104
108
|
this.#onCredentials = onCredentials;
|
|
105
109
|
this.#now = now;
|
|
@@ -263,13 +267,13 @@ export class RegistrationManager {
|
|
|
263
267
|
client_secret: clientSecret,
|
|
264
268
|
user_info: userInfo,
|
|
265
269
|
});
|
|
266
|
-
} catch {
|
|
270
|
+
} catch (error) {
|
|
267
271
|
if (this.#isCurrent(run)) {
|
|
268
272
|
this.#finishRun(run, REGISTRATION_STATES.ERROR, {
|
|
269
|
-
error: {
|
|
273
|
+
error: this.#diagnostics.report(error, { operation: 'provision.poll', stage: 'activation', publicError: {
|
|
270
274
|
code: 'credentials_callback_failed',
|
|
271
275
|
message: 'Unable to store the Feishu credentials.',
|
|
272
|
-
},
|
|
276
|
+
} }).publicError,
|
|
273
277
|
});
|
|
274
278
|
}
|
|
275
279
|
return;
|
|
@@ -297,7 +301,7 @@ export class RegistrationManager {
|
|
|
297
301
|
return;
|
|
298
302
|
}
|
|
299
303
|
this.#finishRun(run, REGISTRATION_STATES.ERROR, {
|
|
300
|
-
error: publicError(error),
|
|
304
|
+
error: this.#diagnostics.report(error, { operation: run.qrCodeUrl ? 'provision.poll' : 'provision.begin', publicError: publicError(error) }).publicError,
|
|
301
305
|
});
|
|
302
306
|
}
|
|
303
307
|
|
|
@@ -85,7 +85,7 @@ export class CallbackRepairManager {
|
|
|
85
85
|
#appId;
|
|
86
86
|
#domain;
|
|
87
87
|
|
|
88
|
-
constructor({ registerApp, onCredentials, appId, domain = 'feishu' } = {}) {
|
|
88
|
+
constructor({ registerApp, onCredentials, appId, domain = 'feishu', diagnostics } = {}) {
|
|
89
89
|
if (typeof registerApp !== 'function') throw new TypeError('registerApp is required');
|
|
90
90
|
if (typeof onCredentials !== 'function') throw new TypeError('onCredentials is required');
|
|
91
91
|
if (typeof appId !== 'string' || !appId.trim()) throw new TypeError('appId is required');
|
|
@@ -94,6 +94,7 @@ export class CallbackRepairManager {
|
|
|
94
94
|
this.#appId = appId.trim();
|
|
95
95
|
this.#domain = domain;
|
|
96
96
|
this.#manager = new RegistrationManager({
|
|
97
|
+
diagnostics,
|
|
97
98
|
registerApp: (options) => registerApp({
|
|
98
99
|
...options,
|
|
99
100
|
onQRCodeReady: (info) => {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createConnectionDiagnostics, atConnectionStage } from '../shared/connection-error.mjs';
|
|
1
2
|
import { sendRememberedConnectionTest } from '../shared/connection-test.mjs';
|
|
2
3
|
import { MacOSMessagesApi, normalizeIMessage } from './imessage-api.mjs';
|
|
3
4
|
import { createIMessageBridgeStatus, IMessageHarnessBridge } from './imessage-bridge.mjs';
|
|
@@ -26,6 +27,7 @@ export function createIMessageRuntimeStatus() {
|
|
|
26
27
|
|
|
27
28
|
export class IMessageRuntime {
|
|
28
29
|
#config; #token; #harness; #state; #contextEnhancement; #accessPolicy; #logger;
|
|
30
|
+
#diagnostics;
|
|
29
31
|
#replyTimeoutMs; #pollIntervalMs; #createApi; #status = createIMessageRuntimeStatus();
|
|
30
32
|
#api; #bridge; #abortController; #timer; #polling; #stopped = true;
|
|
31
33
|
constructor({ config, token, harness, state, contextEnhancement, accessPolicy, logger = console,
|
|
@@ -33,7 +35,7 @@ export class IMessageRuntime {
|
|
|
33
35
|
createApi = (options) => new MacOSMessagesApi(options) }) {
|
|
34
36
|
if (!config || !token || !harness || !state) throw new TypeError('IMessageRuntime requires config, token, Harness, and state');
|
|
35
37
|
this.#config = config; this.#token = token; this.#harness = harness; this.#state = state;
|
|
36
|
-
this.#contextEnhancement = contextEnhancement; this.#accessPolicy = accessPolicy; this.#logger = logger;
|
|
38
|
+
this.#contextEnhancement = contextEnhancement; this.#accessPolicy = accessPolicy; this.#logger = logger; this.#diagnostics = createConnectionDiagnostics({ channel: 'imessage', logger });
|
|
37
39
|
this.#replyTimeoutMs = replyTimeoutMs; this.#pollIntervalMs = pollIntervalMs; this.#createApi = createApi;
|
|
38
40
|
}
|
|
39
41
|
get status() { return structuredClone(this.#status); }
|
|
@@ -60,7 +62,7 @@ export class IMessageRuntime {
|
|
|
60
62
|
await this.stop(); this.#stopped = false; this.#status.startedAt = new Date().toISOString(); this.#status.connectionState = 'connecting';
|
|
61
63
|
this.#abortController = new AbortController();
|
|
62
64
|
try {
|
|
63
|
-
await this.#harness.ensureRunning(); this.#status.harnessReachable = true;
|
|
65
|
+
await atConnectionStage('harness.check', () => this.#harness.ensureRunning()); this.#status.harnessReachable = true;
|
|
64
66
|
this.#api = this.#createApi({ signal: this.#abortController.signal });
|
|
65
67
|
const permissions = await this.#api.getPermissions();
|
|
66
68
|
if (permissions.database !== 'granted' || permissions.automation !== 'granted') {
|
|
@@ -78,7 +80,8 @@ export class IMessageRuntime {
|
|
|
78
80
|
logger: this.#logger, replyTimeoutMs: this.#replyTimeoutMs, signal: this.#abortController.signal });
|
|
79
81
|
this.#status.ready = true; this.#status.connectionState = 'connected'; this.#status.lastConnectedAt = Date.now();
|
|
80
82
|
this.#schedulePoll(0); return this.status;
|
|
81
|
-
} catch (error) { this.#status.ready = false; this.#status.connectionState = 'failed'; this.#status.
|
|
83
|
+
} catch (error) { this.#status.ready = false; this.#status.connectionState = 'failed'; this.#status.error = this.#diagnostics.report(error, { operation: 'connection.restore', reuse: true, botId: this.#config?.botId, automatic: true }).publicError;
|
|
84
|
+
this.#status.lastError = this.#status.error.message; await this.stop(); throw error; }
|
|
82
85
|
}
|
|
83
86
|
async stop() { this.#stopped = true; clearTimeout(this.#timer); this.#timer = null; this.#abortController?.abort(); await this.#polling?.catch(() => {}); this.#polling = null; this.#bridge = null; this.#api = null; this.#status.ready = false; }
|
|
84
87
|
#schedulePoll(delay) { if (this.#stopped) return; this.#timer = setTimeout(() => { this.#polling = this.#poll().finally(() => this.#schedulePoll(this.#pollIntervalMs)); }, delay); this.#timer.unref?.(); }
|
|
@@ -92,7 +95,8 @@ export class IMessageRuntime {
|
|
|
92
95
|
if (message) await this.#bridge.accept(message);
|
|
93
96
|
if (Number.isSafeInteger(raw.rowid)) await this.#state.setCursor(raw.rowid);
|
|
94
97
|
}
|
|
95
|
-
this.#status.lastCheckedAt = Date.now(); this.#status.lastError = null;
|
|
96
|
-
} catch (error) { if (!this.#stopped) { this.#status.
|
|
98
|
+
this.#status.lastCheckedAt = Date.now(); this.#status.lastError = null; this.#status.error = null; this.#diagnostics.clear();
|
|
99
|
+
} catch (error) { if (!this.#stopped) { this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', botId: this.#config?.botId, automatic: true }).publicError;
|
|
100
|
+
this.#status.lastError = this.#status.error.message; } }
|
|
97
101
|
}
|
|
98
102
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { atConnectionStage, createConnectionDiagnostics } from '../shared/connection-error.mjs';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
|
|
3
4
|
import { normalizeOfficeBaseUrl, officeHookUrls } from './protocol.mjs';
|
|
@@ -15,6 +16,7 @@ export class OfficeController {
|
|
|
15
16
|
#credentials;
|
|
16
17
|
#store;
|
|
17
18
|
#logger;
|
|
19
|
+
#diagnostics;
|
|
18
20
|
#createRuntime;
|
|
19
21
|
#runtime = null;
|
|
20
22
|
#transition = Promise.resolve();
|
|
@@ -29,13 +31,16 @@ export class OfficeController {
|
|
|
29
31
|
this.#credentials = credentials;
|
|
30
32
|
this.#store = configStore;
|
|
31
33
|
this.#logger = logger;
|
|
34
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'office', logger });
|
|
32
35
|
this.#createRuntime = createRuntime ?? ((options) => new OfficeRuntime(options));
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
get diagnostics() { return this.#diagnostics; }
|
|
39
|
+
|
|
35
40
|
async initialize() {
|
|
36
41
|
const config = this.#store.get();
|
|
37
42
|
if (config) {
|
|
38
|
-
const credential = await this.#credentials.resolve(config.deviceTokenRef)
|
|
43
|
+
const credential = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.deviceTokenRef), 'credential-store');
|
|
39
44
|
if (credential?.value) await this.#start(config, credential.value);
|
|
40
45
|
}
|
|
41
46
|
return this.status();
|
|
@@ -50,7 +55,7 @@ export class OfficeController {
|
|
|
50
55
|
const baseUrl = normalizeOfficeBaseUrl(requestedBaseUrl).origin;
|
|
51
56
|
const tokenRef = officeTokenRef(baseUrl, deviceId);
|
|
52
57
|
const suppliedToken = clean(input.deviceToken);
|
|
53
|
-
const priorCredential = await this.#credentials.resolve(tokenRef)
|
|
58
|
+
const priorCredential = await atConnectionStage('credential.read', () => this.#credentials.resolve(tokenRef), 'credential-store');
|
|
54
59
|
const token = suppliedToken ?? priorCredential?.value;
|
|
55
60
|
if (!token || token.length < 32) throw new TypeError('Device Token must contain at least 32 characters');
|
|
56
61
|
const now = new Date().toISOString();
|
|
@@ -68,16 +73,16 @@ export class OfficeController {
|
|
|
68
73
|
});
|
|
69
74
|
if (!config) throw new TypeError('AI Office connector configuration is invalid');
|
|
70
75
|
|
|
71
|
-
await this.#credentials.set(tokenRef, token);
|
|
76
|
+
await atConnectionStage('credential.save', () => this.#credentials.set(tokenRef, token), 'credential-store');
|
|
72
77
|
try {
|
|
73
|
-
await this.#store.save(config);
|
|
78
|
+
await atConnectionStage('account.save', () => this.#store.save(config), 'account-config');
|
|
74
79
|
} catch (error) {
|
|
75
|
-
if (priorCredential?.value) await this.#credentials.set(tokenRef, priorCredential.value).catch(() => undefined);
|
|
76
|
-
else await this.#credentials.unset(tokenRef).catch(() => undefined);
|
|
80
|
+
if (priorCredential?.value) await atConnectionStage('credential.save', () => this.#credentials.set(tokenRef, priorCredential.value), 'credential-store').catch(() => undefined);
|
|
81
|
+
else await atConnectionStage('credential.remove', () => this.#credentials.unset(tokenRef), 'credential-store').catch(() => undefined);
|
|
77
82
|
throw error;
|
|
78
83
|
}
|
|
79
84
|
if (previous?.deviceTokenRef && previous.deviceTokenRef !== tokenRef) {
|
|
80
|
-
await this.#credentials.unset(previous.deviceTokenRef)
|
|
85
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(previous.deviceTokenRef), 'credential-store');
|
|
81
86
|
}
|
|
82
87
|
await this.#start(config, token);
|
|
83
88
|
return this.status();
|
|
@@ -104,18 +109,24 @@ export class OfficeController {
|
|
|
104
109
|
|
|
105
110
|
async remove() {
|
|
106
111
|
return this.#serial(async () => {
|
|
112
|
+
const warnings = [];
|
|
107
113
|
const config = this.#store.get();
|
|
108
114
|
await this.#stop();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
await atConnectionStage('account.remove', () => this.#store.clear(), 'account-config');
|
|
116
|
+
if (config?.deviceTokenRef) {
|
|
117
|
+
await atConnectionStage('credential.remove', () => this.#credentials.unset(config.deviceTokenRef), 'credential-store').catch(error => {
|
|
118
|
+
warnings.push(this.#diagnostics.report(error, { reuse: true, operation: 'connector.remove', warning: true,
|
|
119
|
+
publicError: { code: 'cleanup-failed', message: '连接已移除,但登录凭据清理失败。' } }).publicError);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return { ...await this.status(), ...(warnings.length ? { warnings } : {}) };
|
|
112
123
|
});
|
|
113
124
|
}
|
|
114
125
|
|
|
115
126
|
async status() {
|
|
116
127
|
const config = this.#store.get();
|
|
117
128
|
if (!config) return { schemaVersion: 1, configured: false, connected: false, state: 'unconfigured' };
|
|
118
|
-
const credential = await this.#credentials.resolve(config.deviceTokenRef)
|
|
129
|
+
const credential = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.deviceTokenRef), 'credential-store');
|
|
119
130
|
const runtime = this.#runtime?.status ?? null;
|
|
120
131
|
return {
|
|
121
132
|
schemaVersion: 1,
|
|
@@ -140,7 +151,7 @@ export class OfficeController {
|
|
|
140
151
|
async close() { await this.#transition.catch(() => undefined); await this.#stop(); }
|
|
141
152
|
|
|
142
153
|
async #resolveToken(config) {
|
|
143
|
-
const credential = await this.#credentials.resolve(config.deviceTokenRef)
|
|
154
|
+
const credential = await atConnectionStage('credential.read', () => this.#credentials.resolve(config.deviceTokenRef), 'credential-store');
|
|
144
155
|
if (!credential?.value) throw new Error('AI Office Device Token is missing');
|
|
145
156
|
return credential.value;
|
|
146
157
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { extractConnectionEvidence, createConnectionDiagnostics } from '../shared/connection-error.mjs';
|
|
1
2
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
3
|
|
|
3
4
|
import { t } from '../shared/i18n.mjs';
|
|
@@ -22,6 +23,7 @@ export class OfficeRuntime {
|
|
|
22
23
|
#config;
|
|
23
24
|
#token;
|
|
24
25
|
#logger;
|
|
26
|
+
#diagnostics;
|
|
25
27
|
#transport;
|
|
26
28
|
#sleep;
|
|
27
29
|
#controller = null;
|
|
@@ -41,6 +43,7 @@ export class OfficeRuntime {
|
|
|
41
43
|
this.#config = config;
|
|
42
44
|
this.#token = token;
|
|
43
45
|
this.#logger = logger;
|
|
46
|
+
this.#diagnostics = createConnectionDiagnostics({ channel: 'office', logger });
|
|
44
47
|
this.#sleep = sleepImpl;
|
|
45
48
|
this.#transport = transport ?? new OfficeTransport({
|
|
46
49
|
baseUrl: config.baseUrl, deviceId: config.deviceId, token,
|
|
@@ -88,7 +91,7 @@ export class OfficeRuntime {
|
|
|
88
91
|
this.#task = this.#run(this.#controller.signal).finally(() => { this.#task = null; });
|
|
89
92
|
this.#task.catch((error) => {
|
|
90
93
|
if (this.#controller?.signal.aborted) return;
|
|
91
|
-
this.#logger.error?.('[dsh-im:office] connector stopped:', error);
|
|
94
|
+
this.#logger.error?.('[dsh-im:office] connector stopped:', extractConnectionEvidence(error).details);
|
|
92
95
|
});
|
|
93
96
|
return this.status;
|
|
94
97
|
}
|
|
@@ -112,7 +115,7 @@ export class OfficeRuntime {
|
|
|
112
115
|
onOpen: () => {
|
|
113
116
|
this.#status.connected = true;
|
|
114
117
|
this.#status.state = 'connected';
|
|
115
|
-
this.#status.error = null;
|
|
118
|
+
this.#status.error = null; this.#diagnostics.clear();
|
|
116
119
|
streamOpened = true;
|
|
117
120
|
},
|
|
118
121
|
onEvent: async (event) => {
|
|
@@ -129,7 +132,7 @@ export class OfficeRuntime {
|
|
|
129
132
|
attemptController.abort();
|
|
130
133
|
this.#status.connected = false;
|
|
131
134
|
this.#status.state = 'reconnecting';
|
|
132
|
-
this.#status.error = safeConnectionError(error);
|
|
135
|
+
this.#status.error = this.#diagnostics.report(error, { operation: 'connection.monitor', automatic: true, publicError: safeConnectionError(error) }).publicError;
|
|
133
136
|
this.#status.reconnects += 1;
|
|
134
137
|
const delay = RETRY_DELAYS[Math.min(attempt, RETRY_DELAYS.length - 1)];
|
|
135
138
|
attempt += 1;
|
|
@@ -2,6 +2,7 @@ import { OFFICE_HOOK_PATHS, OFFICE_PROTOCOL_VERSION, officeHookUrls } from './pr
|
|
|
2
2
|
|
|
3
3
|
function safeTransportError(operation, response) {
|
|
4
4
|
const error = new Error(`AI Office ${operation} failed: HTTP ${response.status}`);
|
|
5
|
+
error.status = response.status;
|
|
5
6
|
error.code = response.status === 401 ? 'invalid-device-token'
|
|
6
7
|
: response.status === 404 ? 'office-hook-unavailable'
|
|
7
8
|
: response.status === 409 ? 'office-job-conflict' : 'office-transport-failed';
|