@xmanrui/dsh-im 0.14.0 → 0.16.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.
@@ -1,6 +1,10 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { connectionTestMessage } from '../shared/connection-test.mjs';
3
3
  import { RegistrationManager } from './registration-manager.mjs';
4
+ import {
5
+ CALLBACK_REPAIR_OPERATION,
6
+ CallbackRepairManager,
7
+ } from './repair-manager.mjs';
4
8
  import { REQUIRED_TENANT_SCOPES } from './plugin-controller.mjs';
5
9
 
6
10
  const ACTIVE_REGISTRATION_STATES = new Set([
@@ -8,6 +12,8 @@ const ACTIVE_REGISTRATION_STATES = new Set([
8
12
  ]);
9
13
  const MUTABLE_REGISTRATION_STATES = new Set([...ACTIVE_REGISTRATION_STATES, 'saving']);
10
14
  const ALL_VISIBLE_SENDERS = '*';
15
+ const DEFAULT_CALLBACK_PROBE_TIMEOUT_MS = 120_000;
16
+ const MAX_CALLBACK_PROBE_TIMEOUT_MS = 600_000;
11
17
 
12
18
  function idleConnection() {
13
19
  return {
@@ -60,6 +66,26 @@ function secretRefFor(botId) {
60
66
  return `DSH_FEISHU_APP_SECRET_${botId.slice(4).toUpperCase()}`;
61
67
  }
62
68
 
69
+ function configuredBotFingerprint(config) {
70
+ return JSON.stringify({
71
+ id: config.id,
72
+ appId: config.appId,
73
+ secretRef: config.secretRef,
74
+ ownerOpenIds: config.ownerOpenIds,
75
+ domain: config.domain,
76
+ botName: config.botName,
77
+ botOpenId: config.botOpenId,
78
+ activated: config.activated,
79
+ deletionPending: config.deletionPending === true,
80
+ connectedAt: config.connectedAt ?? null,
81
+ createdAt: config.createdAt ?? null,
82
+ });
83
+ }
84
+
85
+ function optionalNonEmptyString(value) {
86
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
87
+ }
88
+
63
89
  /**
64
90
  * Multi-account Feishu orchestration. Each bot owns its credential reference,
65
91
  * runtime and session store. Config commits are serialized, while unrelated
@@ -77,11 +103,13 @@ export class MultiBotDshFeishuController {
77
103
  #runtimes = new Map();
78
104
  #botErrors = new Map();
79
105
  #registrations = new Map();
106
+ #activeRepairs = new Map();
80
107
  #botOwnership = new Map();
81
108
  #latestRegistrationId = null;
82
109
  #configTransition = Promise.resolve();
83
110
  #botTransitions = new Map();
84
111
  #revision = 1;
112
+ #callbackProbeTimeoutMs;
85
113
  #closed = false;
86
114
 
87
115
  constructor({
@@ -93,6 +121,7 @@ export class MultiBotDshFeishuController {
93
121
  deleteState = async () => {},
94
122
  createBotId = makeBotId,
95
123
  createRegistrationId = makeRegistrationId,
124
+ callbackProbeTimeoutMs = DEFAULT_CALLBACK_PROBE_TIMEOUT_MS,
96
125
  }) {
97
126
  if (typeof registerApp !== 'function') throw new Error('registerApp is required');
98
127
  if (typeof verifyApp !== 'function') throw new Error('verifyApp is required');
@@ -102,6 +131,11 @@ export class MultiBotDshFeishuController {
102
131
  }
103
132
  if (typeof createRuntime !== 'function') throw new Error('createRuntime is required');
104
133
  if (typeof deleteState !== 'function') throw new Error('deleteState must be a function');
134
+ if (!Number.isFinite(callbackProbeTimeoutMs)
135
+ || callbackProbeTimeoutMs <= 0
136
+ || callbackProbeTimeoutMs > MAX_CALLBACK_PROBE_TIMEOUT_MS) {
137
+ throw new TypeError('callbackProbeTimeoutMs must be between 1 and 600000ms');
138
+ }
105
139
  this.#registerApp = registerApp;
106
140
  this.#verifyApp = verifyApp;
107
141
  this.#credentials = credentials;
@@ -110,6 +144,7 @@ export class MultiBotDshFeishuController {
110
144
  this.#deleteState = deleteState;
111
145
  this.#createBotId = createBotId;
112
146
  this.#createRegistrationId = createRegistrationId;
147
+ this.#callbackProbeTimeoutMs = callbackProbeTimeoutMs;
113
148
  }
114
149
 
115
150
  async initialize() {
@@ -187,12 +222,70 @@ export class MultiBotDshFeishuController {
187
222
  preset: false,
188
223
  scopes: { tenant: [...REQUIRED_TENANT_SCOPES] },
189
224
  events: { items: { tenant: ['im.message.receive_v1'] } },
225
+ callbacks: { items: ['card.action.trigger'] },
190
226
  },
191
227
  });
192
228
  this.#touch();
193
229
  return this.registrationStatus(id);
194
230
  }
195
231
 
232
+ startCallbackRepair(botId, { actorOpenId, chatId } = {}) {
233
+ this.#assertOpen();
234
+ const target = this.#requireBot(botId);
235
+ if (target.deletionPending) throw new Error('Cannot repair a Feishu bot pending deletion');
236
+
237
+ const activeId = this.#activeRepairs.get(botId);
238
+ const active = activeId ? this.#registrations.get(activeId) : null;
239
+ if (active && MUTABLE_REGISTRATION_STATES.has(active.manager.status().state)) {
240
+ return this.registrationStatus(active.id);
241
+ }
242
+ this.#activeRepairs.delete(botId);
243
+
244
+ const id = this.#createRegistrationId();
245
+ if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(id) || this.#registrations.has(id)) {
246
+ throw new Error('Registration id generator returned an invalid or duplicate id');
247
+ }
248
+ const record = {
249
+ id,
250
+ operation: CALLBACK_REPAIR_OPERATION,
251
+ manager: null,
252
+ botId,
253
+ createdNew: false,
254
+ cancelled: false,
255
+ remoteCommitted: false,
256
+ processing: null,
257
+ publicError: null,
258
+ stage: 'authorizing',
259
+ target: structuredClone(target),
260
+ targetFingerprint: configuredBotFingerprint(target),
261
+ initiator: {
262
+ actorOpenId: optionalNonEmptyString(actorOpenId),
263
+ chatId: optionalNonEmptyString(chatId),
264
+ },
265
+ };
266
+ record.manager = new CallbackRepairManager({
267
+ registerApp: this.#registerApp,
268
+ appId: target.appId,
269
+ domain: target.domain,
270
+ onCredentials: (result) => {
271
+ record.remoteCommitted = true;
272
+ const processing = this.#acceptCallbackRepair(record, result);
273
+ const tracked = processing.finally(() => {
274
+ if (record.processing === tracked) record.processing = null;
275
+ });
276
+ record.processing = tracked;
277
+ return tracked;
278
+ },
279
+ });
280
+ this.#registrations.set(id, record);
281
+ this.#activeRepairs.set(botId, id);
282
+ this.#latestRegistrationId = id;
283
+ this.#trimRegistrations();
284
+ record.manager.start();
285
+ this.#touch();
286
+ return this.registrationStatus(id);
287
+ }
288
+
196
289
  hasRegistration(attemptId) {
197
290
  return this.#registrations.has(attemptId);
198
291
  }
@@ -206,7 +299,14 @@ export class MultiBotDshFeishuController {
206
299
  async cancelRegistration(attemptId = this.#latestRegistrationId) {
207
300
  const record = this.#registrations.get(attemptId);
208
301
  if (!record) return this.status();
209
- if (!MUTABLE_REGISTRATION_STATES.has(record.manager.status().state)) {
302
+ const state = record.manager.status().state;
303
+ // Once registerApp has returned, the platform-side update has committed.
304
+ // A repair must finish converging the returned credential and callback
305
+ // probe; cancelling here cannot roll that remote mutation back.
306
+ if (record.operation === CALLBACK_REPAIR_OPERATION && state === 'saving') {
307
+ return this.registrationStatus(attemptId);
308
+ }
309
+ if (!MUTABLE_REGISTRATION_STATES.has(state)) {
210
310
  return this.registrationStatus(attemptId);
211
311
  }
212
312
  record.cancelled = true;
@@ -397,8 +497,15 @@ export class MultiBotDshFeishuController {
397
497
  async close() {
398
498
  if (this.#closed) return;
399
499
  this.#closed = true;
500
+ const repairProcessing = [];
400
501
  for (const record of this.#registrations.values()) {
401
- if (MUTABLE_REGISTRATION_STATES.has(record.manager.status().state)) {
502
+ const state = record.manager.status().state;
503
+ if (record.operation === CALLBACK_REPAIR_OPERATION && state === 'saving') {
504
+ // Stop projecting an in-flight repair, but do not request its local
505
+ // credential rollback after the remote update has committed.
506
+ record.manager.cancel();
507
+ if (record.processing) repairProcessing.push(record.processing);
508
+ } else if (MUTABLE_REGISTRATION_STATES.has(state)) {
402
509
  record.cancelled = true;
403
510
  record.manager.cancel();
404
511
  }
@@ -406,6 +513,15 @@ export class MultiBotDshFeishuController {
406
513
  await this.#configTransition;
407
514
  await Promise.allSettled([...this.#botTransitions.values()]);
408
515
  await Promise.allSettled([...this.#runtimes.keys()].map((id) => this.#stopRuntime(id)));
516
+ await Promise.allSettled(repairProcessing);
517
+ // A committed repair can be between SDK completion and its serialized
518
+ // credential/runtime transition when close begins. Waiting for the repair
519
+ // can therefore create a replacement runtime after the first drain. Drain
520
+ // both transition queues again, then stop every runtime created by that
521
+ // late forward-convergence work.
522
+ await this.#configTransition;
523
+ await Promise.allSettled([...this.#botTransitions.values()]);
524
+ await Promise.allSettled([...this.#runtimes.keys()].map((id) => this.#stopRuntime(id)));
409
525
  }
410
526
 
411
527
  #status({ registration, selectedBotId } = {}) {
@@ -461,9 +577,226 @@ export class MultiBotDshFeishuController {
461
577
  ...snapshot,
462
578
  attempt: record.id,
463
579
  ...(record.botId ? { botId: record.botId } : {}),
580
+ ...(record.operation ? { operation: record.operation } : {}),
581
+ ...(record.stage ? { stage: record.stage } : {}),
582
+ ...(snapshot.state === 'error' && record.publicError
583
+ ? { error: { ...record.publicError } }
584
+ : {}),
464
585
  };
465
586
  }
466
587
 
588
+ async #acceptCallbackRepair(record, result) {
589
+ const appId = result.client_id;
590
+ const appSecret = result.client_secret;
591
+ const ownerOpenId = optionalNonEmptyString(result.user_info?.open_id);
592
+ const tenantBrand = result.user_info?.tenant_brand;
593
+ const target = record.target;
594
+
595
+ if (record.cancelled) {
596
+ throw this.#callbackRepairError(
597
+ record,
598
+ 'abort',
599
+ 'Callback repair was cancelled before local activation.',
600
+ );
601
+ }
602
+ if (appId !== target.appId) {
603
+ throw this.#callbackRepairError(
604
+ record,
605
+ 'repair_app_mismatch',
606
+ 'Feishu returned credentials for a different app.',
607
+ );
608
+ }
609
+ if (!ownerOpenId) {
610
+ throw this.#callbackRepairError(
611
+ record,
612
+ 'repair_owner_missing',
613
+ 'Feishu returned no repair operator identity.',
614
+ );
615
+ }
616
+ if (tenantBrand !== undefined && tenantBrand !== target.domain) {
617
+ throw this.#callbackRepairError(
618
+ record,
619
+ 'repair_domain_mismatch',
620
+ 'Feishu returned credentials for a different account domain.',
621
+ );
622
+ }
623
+ if (record.initiator.actorOpenId && record.initiator.actorOpenId !== ownerOpenId) {
624
+ throw this.#callbackRepairError(
625
+ record,
626
+ 'repair_owner_mismatch',
627
+ 'The Feishu repair was confirmed by a different operator.',
628
+ );
629
+ }
630
+ if (!target.ownerOpenIds.includes(ALL_VISIBLE_SENDERS)
631
+ && !target.ownerOpenIds.includes(ownerOpenId)) {
632
+ throw this.#callbackRepairError(
633
+ record,
634
+ 'repair_owner_mismatch',
635
+ 'The Feishu repair operator is not an owner of this configured bot.',
636
+ );
637
+ }
638
+
639
+ record.stage = 'verifying_identity';
640
+ let verified;
641
+ try {
642
+ verified = await this.#verifyApp({
643
+ appId,
644
+ appSecret,
645
+ domain: target.domain,
646
+ });
647
+ } catch (error) {
648
+ throw this.#callbackRepairError(
649
+ record,
650
+ 'repair_credentials_invalid',
651
+ 'Feishu could not verify the repaired app credentials.',
652
+ error,
653
+ );
654
+ }
655
+ if (target.botOpenId && verified?.openId !== target.botOpenId) {
656
+ throw this.#callbackRepairError(
657
+ record,
658
+ 'repair_bot_mismatch',
659
+ 'The repaired Feishu app belongs to a different bot identity.',
660
+ );
661
+ }
662
+
663
+ const runtime = await this.#serializeConfig(() => this.#withBotTransition(
664
+ record.botId,
665
+ async () => {
666
+ const current = this.#configStore.getBot(record.botId);
667
+ if (!current
668
+ || current.deletionPending
669
+ || configuredBotFingerprint(current) !== record.targetFingerprint) {
670
+ throw this.#callbackRepairError(
671
+ record,
672
+ 'repair_target_changed',
673
+ 'The Feishu bot changed while its callback repair was in progress.',
674
+ );
675
+ }
676
+
677
+ let previous;
678
+ try {
679
+ previous = await this.#credentials.resolve(current.secretRef);
680
+ } catch (error) {
681
+ throw this.#callbackRepairError(
682
+ record,
683
+ 'credential_update_failed',
684
+ 'Unable to read the current Feishu credential.',
685
+ error,
686
+ );
687
+ }
688
+ const credentialChanged = previous?.value !== appSecret;
689
+ if (credentialChanged) {
690
+ record.stage = 'persisting_secret';
691
+ try {
692
+ await this.#credentials.set(current.secretRef, appSecret);
693
+ } catch (writeError) {
694
+ const observed = await this.#credentials.resolve(current.secretRef).catch(() => null);
695
+ if (observed?.value !== appSecret) {
696
+ throw this.#callbackRepairError(
697
+ record,
698
+ 'credential_update_failed',
699
+ 'Unable to store the repaired Feishu credential.',
700
+ writeError,
701
+ );
702
+ }
703
+ }
704
+ const persisted = await this.#credentials.resolve(current.secretRef).catch(() => null);
705
+ if (persisted?.value !== appSecret) {
706
+ throw this.#callbackRepairError(
707
+ record,
708
+ 'credential_state_unknown',
709
+ 'The repaired Feishu credential could not be confirmed after writing.',
710
+ );
711
+ }
712
+ }
713
+
714
+ let currentRuntime;
715
+ // Callback subscriptions are delivered over the long connection.
716
+ // Always replace it after the platform-side callback update commits,
717
+ // even when registerApp returns the same secret and the old socket
718
+ // still reports healthy, so the probe never runs on stale metadata.
719
+ record.stage = 'restarting';
720
+ try {
721
+ await this.#startRuntime(current, appSecret);
722
+ currentRuntime = this.#runtimes.get(record.botId);
723
+ } catch (error) {
724
+ // The returned credential was already verified and persisted. Do
725
+ // not restore a potentially revoked old secret; reconnectBot can
726
+ // safely retry this forward state later.
727
+ this.#botErrors.set(record.botId, {
728
+ code: 'connection_failed',
729
+ message: '机器人回调修复已保存,但长连接未就绪,请点击重试。',
730
+ });
731
+ this.#touch();
732
+ throw this.#callbackRepairError(
733
+ record,
734
+ 'repair_connection_failed',
735
+ 'The repaired Feishu runtime could not be started.',
736
+ error,
737
+ );
738
+ }
739
+ if (!currentRuntime) {
740
+ throw this.#callbackRepairError(
741
+ record,
742
+ 'repair_connection_failed',
743
+ 'The repaired Feishu runtime is unavailable.',
744
+ );
745
+ }
746
+ this.#botErrors.delete(record.botId);
747
+ this.#touch();
748
+ return currentRuntime;
749
+ },
750
+ ));
751
+
752
+ if (typeof runtime.beginCardActionProbe !== 'function') {
753
+ throw this.#callbackRepairError(
754
+ record,
755
+ 'card_action_probe_unavailable',
756
+ 'The Feishu runtime cannot verify card callbacks.',
757
+ );
758
+ }
759
+ record.stage = 'awaiting_callback';
760
+ try {
761
+ const proof = await runtime.beginCardActionProbe({
762
+ expectedOperatorOpenId: ownerOpenId,
763
+ timeoutMs: this.#callbackProbeTimeoutMs,
764
+ ...(record.initiator.chatId ? { chatId: record.initiator.chatId } : {}),
765
+ });
766
+ if (proof?.verified !== true) {
767
+ const error = new Error('Feishu runtime returned no callback proof');
768
+ error.code = 'card_action_probe_failed';
769
+ throw error;
770
+ }
771
+ } catch (error) {
772
+ const code = error?.code === 'card_action_probe_timeout'
773
+ ? 'card_action_probe_timeout'
774
+ : error?.code === 'card_action_probe_unavailable'
775
+ ? 'card_action_probe_unavailable'
776
+ : error?.code === 'card_action_probe_send_failed'
777
+ ? 'card_action_probe_send_failed'
778
+ : 'card_action_probe_failed';
779
+ throw this.#callbackRepairError(
780
+ record,
781
+ code,
782
+ code === 'card_action_probe_timeout'
783
+ ? 'Timed out waiting for the Feishu callback verification button.'
784
+ : 'The Feishu card callback probe failed.',
785
+ error,
786
+ );
787
+ }
788
+ record.stage = 'verified';
789
+ record.publicError = null;
790
+ this.#touch();
791
+ }
792
+
793
+ #callbackRepairError(record, code, message, cause) {
794
+ record.publicError = { code, message };
795
+ const error = new Error(message, cause ? { cause } : undefined);
796
+ error.code = code;
797
+ return error;
798
+ }
799
+
467
800
  async #acceptCredentials(record, result) {
468
801
  if (record.cancelled) throw new Error('Registration was cancelled');
469
802
  const appId = result.client_id;
@@ -610,6 +943,7 @@ export class MultiBotDshFeishuController {
610
943
  botId: config.id,
611
944
  config,
612
945
  appSecret,
946
+ repair: this.#runtimeRepairCapability(config.id),
613
947
  });
614
948
  this.#runtimes.set(config.id, runtime);
615
949
  try {
@@ -621,6 +955,27 @@ export class MultiBotDshFeishuController {
621
955
  }
622
956
  }
623
957
 
958
+ #runtimeRepairCapability(botId) {
959
+ const ownedAttempt = (attemptId) => {
960
+ const record = this.#registrations.get(attemptId);
961
+ return record?.operation === CALLBACK_REPAIR_OPERATION && record.botId === botId
962
+ ? record
963
+ : null;
964
+ };
965
+ return Object.freeze({
966
+ start: ({ actorOpenId, chatId } = {}) => this.startCallbackRepair(botId, {
967
+ actorOpenId,
968
+ chatId,
969
+ }),
970
+ status: ({ attemptId } = {}) => ownedAttempt(attemptId)
971
+ ? this.registrationStatus(attemptId)
972
+ : null,
973
+ cancel: async ({ attemptId } = {}) => ownedAttempt(attemptId)
974
+ ? this.cancelRegistration(attemptId)
975
+ : this.status(botId),
976
+ });
977
+ }
978
+
624
979
  async #stopRuntime(botId) {
625
980
  const runtime = this.#runtimes.get(botId);
626
981
  this.#runtimes.delete(botId);
@@ -103,6 +103,7 @@ export class DshFeishuController {
103
103
  preset: false,
104
104
  scopes: { tenant: [...REQUIRED_TENANT_SCOPES] },
105
105
  events: { items: { tenant: ['im.message.receive_v1'] } },
106
+ callbacks: { items: ['card.action.trigger'] },
106
107
  },
107
108
  });
108
109
  return this.status();
@@ -0,0 +1,109 @@
1
+ import { RegistrationManager } from './registration-manager.mjs';
2
+
3
+ export const CARD_ACTION_CALLBACK = 'card.action.trigger';
4
+ export const CALLBACK_REPAIR_OPERATION = 'callback_repair';
5
+
6
+ function accountsDomain(domain) {
7
+ return domain === 'lark' ? 'accounts.larksuite.com' : 'accounts.feishu.cn';
8
+ }
9
+
10
+ function launcherDomain(domain) {
11
+ return domain === 'lark' ? 'open.larksuite.com' : 'open.feishu.cn';
12
+ }
13
+
14
+ /**
15
+ * The SDK owns the rest of the verification URL. The repair flow accepts only
16
+ * its target account host and singleton SDK/app/addon parameters, and it can
17
+ * never fall back to the create-only flow. This catches regressions such as a
18
+ * literal `{{client_id}}` before the broken URL reaches the browser.
19
+ */
20
+ export function assertCallbackRepairUrl(value, expectedAppId, domain = 'feishu') {
21
+ let url;
22
+ try {
23
+ url = new URL(value);
24
+ } catch {
25
+ throw new Error('Feishu callback repair returned an invalid verification URL');
26
+ }
27
+ const supportedDomain = domain === 'feishu' || domain === 'lark';
28
+ const clientIds = url.searchParams.getAll('clientID');
29
+ const transportProviders = url.searchParams.getAll('tp');
30
+ const addons = url.searchParams.getAll('addons');
31
+ const hasPlaceholder = String(expectedAppId).includes('{{')
32
+ || String(expectedAppId).includes('}}')
33
+ || [...url.searchParams.values()].some((item) => (
34
+ item.includes('{{') || item.includes('}}')
35
+ ));
36
+ if (!supportedDomain
37
+ || url.protocol !== 'https:'
38
+ // registerApp begins on accounts.* but the SDK deliberately returns the
39
+ // user-facing /page/launcher URL on open.*.
40
+ || url.hostname !== launcherDomain(domain)
41
+ || url.port !== ''
42
+ || url.username !== ''
43
+ || url.password !== ''
44
+ || transportProviders.length !== 1
45
+ || transportProviders[0] !== 'sdk'
46
+ || clientIds.length !== 1
47
+ || clientIds[0] !== expectedAppId
48
+ || url.searchParams.has('createOnly')
49
+ || addons.length !== 1
50
+ || !addons[0]?.trim()
51
+ || hasPlaceholder) {
52
+ throw new Error('Feishu callback repair returned an unsafe verification URL');
53
+ }
54
+ return url.toString();
55
+ }
56
+
57
+ /**
58
+ * One targeted update attempt for an existing Feishu app. It intentionally
59
+ * shares RegistrationManager's polling/state implementation while fixing the
60
+ * update manifest in one place so callers cannot accidentally add scopes,
61
+ * events, presets, or createOnly.
62
+ */
63
+ export class CallbackRepairManager {
64
+ #manager;
65
+ #appId;
66
+ #domain;
67
+
68
+ constructor({ registerApp, onCredentials, appId, domain = 'feishu' } = {}) {
69
+ if (typeof registerApp !== 'function') throw new TypeError('registerApp is required');
70
+ if (typeof onCredentials !== 'function') throw new TypeError('onCredentials is required');
71
+ if (typeof appId !== 'string' || !appId.trim()) throw new TypeError('appId is required');
72
+ if (domain !== 'feishu' && domain !== 'lark') throw new TypeError('domain is invalid');
73
+
74
+ this.#appId = appId.trim();
75
+ this.#domain = domain;
76
+ this.#manager = new RegistrationManager({
77
+ registerApp: (options) => registerApp({
78
+ ...options,
79
+ onQRCodeReady: (info) => {
80
+ assertCallbackRepairUrl(info?.url, this.#appId, this.#domain);
81
+ options.onQRCodeReady(info);
82
+ },
83
+ }),
84
+ onCredentials,
85
+ });
86
+ }
87
+
88
+ start() {
89
+ return this.#manager.start({
90
+ source: 'deepseek-harness-card-action-repair',
91
+ domain: accountsDomain(this.#domain),
92
+ appId: this.#appId,
93
+ addons: {
94
+ preset: false,
95
+ callbacks: { items: [CARD_ACTION_CALLBACK] },
96
+ },
97
+ });
98
+ }
99
+
100
+ status() {
101
+ return this.#manager.status();
102
+ }
103
+
104
+ cancel() {
105
+ return this.#manager.cancel();
106
+ }
107
+ }
108
+
109
+ export default CallbackRepairManager;
@@ -85,7 +85,7 @@ async function selectedWorkspacePath(value) {
85
85
  }
86
86
  }
87
87
 
88
- async function workspacePathSnapshot(harness) {
88
+ export async function workspacePathSnapshot(harness) {
89
89
  const listed = await harness.listWorkspaces();
90
90
  const currentValue = typeof harness?.currentWorkspace === 'function'
91
91
  ? harness.currentWorkspace()
@@ -148,7 +148,7 @@ async function runWorkspaceListCommand(match, harness) {
148
148
  }
149
149
  }
150
150
 
151
- async function resolveSessionListWorkspace(selector, harness) {
151
+ export async function resolveSessionListWorkspace(selector, harness) {
152
152
  if (!selector) {
153
153
  if (typeof harness?.currentWorkspace !== 'function') {
154
154
  return { error: '当前机器人没有可用的工作区。' };
@@ -184,7 +184,7 @@ function authenticatedHeaders(token) {
184
184
  function baseInfo() {
185
185
  return {
186
186
  channel_version: WEIXIN_PROTOCOL_VERSION,
187
- bot_agent: 'DeepSeekHarness/0.14.0',
187
+ bot_agent: 'DeepSeekHarness/0.16.0',
188
188
  };
189
189
  }
190
190