@dsh-enhanced/lark-channel 0.1.3

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.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +342 -0
  3. package/bin/dsh-lark-setup.js +8 -0
  4. package/bin/dsh-supervised-growth-setup.js +8 -0
  5. package/cordis.patch.yml +21 -0
  6. package/lib/adapter.d.ts +135 -0
  7. package/lib/adapter.d.ts.map +1 -0
  8. package/lib/adapter.js +965 -0
  9. package/lib/adapter.js.map +1 -0
  10. package/lib/approval.d.ts +26 -0
  11. package/lib/approval.d.ts.map +1 -0
  12. package/lib/approval.js +104 -0
  13. package/lib/approval.js.map +1 -0
  14. package/lib/config.d.ts +21 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +56 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/group-thread.d.ts +11 -0
  19. package/lib/group-thread.d.ts.map +1 -0
  20. package/lib/group-thread.js +22 -0
  21. package/lib/group-thread.js.map +1 -0
  22. package/lib/index.d.ts +27 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +30 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/launchd.d.ts +44 -0
  27. package/lib/launchd.d.ts.map +1 -0
  28. package/lib/launchd.js +205 -0
  29. package/lib/launchd.js.map +1 -0
  30. package/lib/model-picker.d.ts +24 -0
  31. package/lib/model-picker.d.ts.map +1 -0
  32. package/lib/model-picker.js +95 -0
  33. package/lib/model-picker.js.map +1 -0
  34. package/lib/normalize.d.ts +12 -0
  35. package/lib/normalize.d.ts.map +1 -0
  36. package/lib/normalize.js +77 -0
  37. package/lib/normalize.js.map +1 -0
  38. package/lib/permission-picker.d.ts +29 -0
  39. package/lib/permission-picker.d.ts.map +1 -0
  40. package/lib/permission-picker.js +145 -0
  41. package/lib/permission-picker.js.map +1 -0
  42. package/lib/progress.d.ts +15 -0
  43. package/lib/progress.d.ts.map +1 -0
  44. package/lib/progress.js +137 -0
  45. package/lib/progress.js.map +1 -0
  46. package/lib/resident.d.ts +13 -0
  47. package/lib/resident.d.ts.map +1 -0
  48. package/lib/resident.js +41 -0
  49. package/lib/resident.js.map +1 -0
  50. package/lib/sdk.d.ts +131 -0
  51. package/lib/sdk.d.ts.map +1 -0
  52. package/lib/sdk.js +945 -0
  53. package/lib/sdk.js.map +1 -0
  54. package/lib/service.d.ts +30 -0
  55. package/lib/service.d.ts.map +1 -0
  56. package/lib/service.js +157 -0
  57. package/lib/service.js.map +1 -0
  58. package/lib/setup-materialization.d.ts +15 -0
  59. package/lib/setup-materialization.d.ts.map +1 -0
  60. package/lib/setup-materialization.js +221 -0
  61. package/lib/setup-materialization.js.map +1 -0
  62. package/lib/setup-profile.d.ts +23 -0
  63. package/lib/setup-profile.d.ts.map +1 -0
  64. package/lib/setup-profile.js +1126 -0
  65. package/lib/setup-profile.js.map +1 -0
  66. package/lib/setup.d.ts +206 -0
  67. package/lib/setup.d.ts.map +1 -0
  68. package/lib/setup.js +2143 -0
  69. package/lib/setup.js.map +1 -0
  70. package/lib/supervised-growth-profile.d.ts +74 -0
  71. package/lib/supervised-growth-profile.d.ts.map +1 -0
  72. package/lib/supervised-growth-profile.js +485 -0
  73. package/lib/supervised-growth-profile.js.map +1 -0
  74. package/lib/supervised-growth-setup.d.ts +85 -0
  75. package/lib/supervised-growth-setup.d.ts.map +1 -0
  76. package/lib/supervised-growth-setup.js +371 -0
  77. package/lib/supervised-growth-setup.js.map +1 -0
  78. package/lib/systemd.d.ts +39 -0
  79. package/lib/systemd.d.ts.map +1 -0
  80. package/lib/systemd.js +142 -0
  81. package/lib/systemd.js.map +1 -0
  82. package/lib/tool-approval.d.ts +22 -0
  83. package/lib/tool-approval.d.ts.map +1 -0
  84. package/lib/tool-approval.js +107 -0
  85. package/lib/tool-approval.js.map +1 -0
  86. package/lib/types.d.ts +158 -0
  87. package/lib/types.d.ts.map +1 -0
  88. package/lib/types.js +11 -0
  89. package/lib/types.js.map +1 -0
  90. package/lib/version.d.ts +2 -0
  91. package/lib/version.d.ts.map +1 -0
  92. package/lib/version.js +2 -0
  93. package/lib/version.js.map +1 -0
  94. package/lib/windows-task.d.ts +39 -0
  95. package/lib/windows-task.d.ts.map +1 -0
  96. package/lib/windows-task.js +117 -0
  97. package/lib/windows-task.js.map +1 -0
  98. package/lib/ws-card-callback.d.ts +7 -0
  99. package/lib/ws-card-callback.d.ts.map +1 -0
  100. package/lib/ws-card-callback.js +51 -0
  101. package/lib/ws-card-callback.js.map +1 -0
  102. package/package.json +96 -0
package/lib/adapter.js ADDED
@@ -0,0 +1,965 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { LarkApprovalError, signLarkApprovalAction, verifyLarkApprovalAction, verifyLarkApprovalActionForRecovery, } from './approval.js';
3
+ import { LarkModelPickerError, parseLarkModelPickerCallback, signLarkModelPickerAction, verifyLarkModelPickerAction, } from './model-picker.js';
4
+ import { isLarkTopLevelSenderThread } from './group-thread.js';
5
+ import { normalizeLarkMessage } from './normalize.js';
6
+ import { LarkPermissionPickerError, parseLarkPermissionPickerCallback, signLarkPermissionPickerAction, verifyLarkPermissionPickerAction, } from './permission-picker.js';
7
+ import { LarkProgressPresenter } from './progress.js';
8
+ import { LARK_APPROVAL_CARD_MAX_BYTES, renderLarkMessage } from './sdk.js';
9
+ import { LarkToolApprovalError, signLarkToolApprovalAction, verifyLarkToolApprovalAction, } from './tool-approval.js';
10
+ import { LarkTransportError, } from './types.js';
11
+ const LARK_TOOL_APPROVAL_REASON_MAX_BYTES = 2 * 1_024;
12
+ const LARK_TOOL_APPROVAL_ARGUMENTS_MAX_BYTES = 16 * 1_024;
13
+ const LARK_TOOL_APPROVAL_MAX_TIMER_MS = 2_147_483_647;
14
+ const larkCallbackIdentifier = /^[A-Za-z0-9][A-Za-z0-9._@:-]{0,255}$/u;
15
+ const larkProviderMessageIdentifier = /^[A-Za-z0-9][A-Za-z0-9._@/-]{0,255}$/u;
16
+ function hasUsableApprovalSecret(secret) {
17
+ return secret !== undefined && Buffer.byteLength(secret, 'utf8') >= 16;
18
+ }
19
+ function boundedReviewText(value, maxBytes) {
20
+ return value === undefined || (Buffer.byteLength(value, 'utf8') <= maxBytes
21
+ && !/(?:\p{Cc}|\p{Bidi_Control})/u.test(value.replaceAll('\n', '').replaceAll('\r', '').replaceAll('\t', '')));
22
+ }
23
+ function sameToolApprovalPayload(left, right) {
24
+ return left.version === right.version
25
+ && left.channel === right.channel
26
+ && left.account === right.account
27
+ && left.tenant === right.tenant
28
+ && left.operationId === right.operationId
29
+ && left.bindingId === right.bindingId
30
+ && left.chatId === right.chatId
31
+ && left.ownerUser === right.ownerUser
32
+ && left.actionHash === right.actionHash
33
+ && left.toolName === right.toolName
34
+ && left.callId === right.callId
35
+ && left.expiresAt === right.expiresAt;
36
+ }
37
+ function failureCode(code) {
38
+ return `lark-${code.replaceAll('_', '-')}`;
39
+ }
40
+ function modelPickerFallbackText(intent) {
41
+ const picker = intent.modelPicker;
42
+ if (picker === undefined)
43
+ return intent.text;
44
+ return [
45
+ '飞书未接受模型选择卡片,已回退为文字目录。',
46
+ intent.text,
47
+ '',
48
+ '可用模型:',
49
+ ...picker.models.map(model => `- ${model.provider}/${model.id}${model.name === model.id ? '' : ` — ${model.name}`}`),
50
+ '',
51
+ '切换:/model use <provider/model>',
52
+ '恢复默认:/model reset',
53
+ ].join('\n');
54
+ }
55
+ function permissionPickerCard(intent, secret, capability) {
56
+ const current = intent.current === 'ask'
57
+ ? '请求批准(ask)'
58
+ : intent.current === 'auto'
59
+ ? '帮我批准(auto)'
60
+ : intent.current === 'full'
61
+ ? '完全访问权限(full)'
62
+ : '自定义安全组合(custom)';
63
+ const callbackValue = (level) => ({
64
+ permissionPicker: signLarkPermissionPickerAction(secret, {
65
+ version: 2,
66
+ ...capability,
67
+ operationId: intent.operationId,
68
+ bindingVersion: intent.bindingVersion,
69
+ sessionId: intent.sessionId,
70
+ issuedAt: intent.issuedAt,
71
+ expiresAt: intent.expiresAt,
72
+ expectedStateHash: intent.expectedStateHash,
73
+ emergencyStopVersion: intent.emergencyStopVersion,
74
+ level,
75
+ }),
76
+ });
77
+ return {
78
+ title: '选择权限模式',
79
+ body: `当前:${current}。请选择新档位。`,
80
+ current: intent.current,
81
+ callbackValues: {
82
+ ask: callbackValue('ask'),
83
+ auto: callbackValue('auto'),
84
+ full: callbackValue('full'),
85
+ },
86
+ };
87
+ }
88
+ function sendFailure(error) {
89
+ if (!(error instanceof LarkTransportError)) {
90
+ return { outcome: 'unknown', failureCode: 'lark-unknown' };
91
+ }
92
+ if (error.code === 'send_timeout' || error.code === 'unknown') {
93
+ return { outcome: 'unknown', failureCode: failureCode(error.code) };
94
+ }
95
+ const retryable = error.code === 'rate_limited' || error.code === 'not_connected';
96
+ return {
97
+ outcome: 'not-sent',
98
+ failureCode: failureCode(error.code),
99
+ retryable,
100
+ ...(error.retryAfterMs === undefined ? {} : { retryAfterMs: error.retryAfterMs }),
101
+ };
102
+ }
103
+ function routeParts(route) {
104
+ const separator = route.indexOf('/');
105
+ if (separator <= 0 || separator === route.length - 1)
106
+ return undefined;
107
+ return { provider: route.slice(0, separator), model: route.slice(separator + 1) };
108
+ }
109
+ function defaultEffortOption(effortIds) {
110
+ let value = '__default__';
111
+ while (effortIds.includes(value))
112
+ value += '_';
113
+ return value;
114
+ }
115
+ function resolveModelPicker(picker, preferred = {}) {
116
+ const modelsByProvider = new Map();
117
+ for (const model of picker.models) {
118
+ const models = modelsByProvider.get(model.provider) ?? [];
119
+ models.push(model);
120
+ modelsByProvider.set(model.provider, models);
121
+ }
122
+ const provider = [preferred.provider, picker.current.provider, ...picker.providers.map(value => value.id)]
123
+ .find(value => value !== undefined && (modelsByProvider.get(value)?.length ?? 0) > 0);
124
+ if (provider === undefined)
125
+ throw new LarkModelPickerError('invalid', 'model picker has no selectable provider');
126
+ const providerModels = modelsByProvider.get(provider);
127
+ const currentRoute = `${picker.current.provider}/${picker.current.model}`;
128
+ const route = [preferred.route, currentRoute]
129
+ .find(value => value !== undefined && providerModels.some(model => `${model.provider}/${model.id}` === value))
130
+ ?? `${providerModels[0].provider}/${providerModels[0].id}`;
131
+ const selectedModel = providerModels.find(model => `${model.provider}/${model.id}` === route);
132
+ const effortNames = new Map(picker.efforts.map(effort => [effort.id, effort.name]));
133
+ const linkedEfforts = (selectedModel.effortIds ?? [])
134
+ .flatMap(id => effortNames.has(id) ? [{ value: id, label: effortNames.get(id) }] : []);
135
+ const defaultEffort = defaultEffortOption(linkedEfforts.map(effort => effort.value));
136
+ const preferredEffort = preferred.effort;
137
+ const initialEffort = preferredEffort === null
138
+ ? defaultEffort
139
+ : preferredEffort !== undefined && linkedEfforts.some(effort => effort.value === preferredEffort)
140
+ ? preferredEffort
141
+ : route === currentRoute && picker.current.reasoningEffort !== undefined
142
+ && linkedEfforts.some(effort => effort.value === picker.current.reasoningEffort)
143
+ ? picker.current.reasoningEffort
144
+ : defaultEffort;
145
+ return { modelsByProvider, provider, providerModels, route, selectedModel,
146
+ linkedEfforts, defaultEffort, initialEffort };
147
+ }
148
+ function resolvedModelRoute(resolved) {
149
+ return { provider: resolved.provider, model: resolved.selectedModel.id,
150
+ ...(resolved.initialEffort === resolved.defaultEffort ? {} : { reasoningEffort: resolved.initialEffort }) };
151
+ }
152
+ function modelPickerCard(picker, secret, capability, preferred = {}) {
153
+ const { modelsByProvider, provider, providerModels, route, selectedModel, linkedEfforts, defaultEffort, initialEffort } = resolveModelPicker(picker, preferred);
154
+ const currentRoute = `${picker.current.provider}/${picker.current.model}`;
155
+ const signedState = {
156
+ version: 3,
157
+ ...capability,
158
+ provider,
159
+ model: selectedModel.id,
160
+ effort: initialEffort === defaultEffort ? null : initialEffort,
161
+ };
162
+ const callbackValue = (action) => ({
163
+ modelPicker: signLarkModelPickerAction(secret, { ...signedState, action }),
164
+ });
165
+ const currentEffort = picker.current.reasoningEffort === undefined ? '' : `,effort:${picker.current.reasoningEffort}`;
166
+ return {
167
+ title: '选择会话模型',
168
+ body: `当前模型:${currentRoute}${currentEffort}\n\n选择将在下一条消息生效,并保留当前上下文。`,
169
+ providerOptions: picker.providers
170
+ .filter(value => (modelsByProvider.get(value.id)?.length ?? 0) > 0)
171
+ .map(value => ({ value: value.id, label: value.name })),
172
+ modelOptions: providerModels.map(model => ({ value: `${model.provider}/${model.id}`, label: model.name })),
173
+ effortOptions: [
174
+ { value: defaultEffort, label: linkedEfforts.length === 0
175
+ ? '默认(该模型无 effort 档位)'
176
+ : '默认(由模型决定)' },
177
+ ...linkedEfforts,
178
+ ],
179
+ initialProvider: provider,
180
+ initialModel: route,
181
+ initialEffort,
182
+ callbackValues: {
183
+ provider: callbackValue('provider'),
184
+ model: callbackValue('model'),
185
+ effort: callbackValue('effort'),
186
+ confirm: callbackValue('confirm'),
187
+ },
188
+ };
189
+ }
190
+ function callbackCard(card) {
191
+ const rendered = renderLarkMessage({ modelPicker: card });
192
+ if (rendered.msgType !== 'interactive') {
193
+ throw new LarkModelPickerError('invalid', 'model picker callback did not render a card');
194
+ }
195
+ const value = JSON.parse(rendered.content);
196
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
197
+ throw new LarkModelPickerError('invalid', 'model picker callback card is invalid');
198
+ }
199
+ return { card: { type: 'raw', data: value } };
200
+ }
201
+ export class LarkDeliveryAdapter {
202
+ config;
203
+ transport;
204
+ channel = 'lark';
205
+ account;
206
+ capabilities;
207
+ now;
208
+ approvalSecret;
209
+ settleApproval;
210
+ recoverApprovalSettlement;
211
+ settleModelSelection;
212
+ settlePermissionSelection;
213
+ loadModelPicker;
214
+ advanceModelPicker;
215
+ statusReactions;
216
+ progressPresenter;
217
+ pendingToolApprovals = new Map();
218
+ toolApprovalTombstones = new Map();
219
+ state = 'disconnected';
220
+ gapGeneration = 0;
221
+ lastErrorCode;
222
+ constructor(config, transport, options = {}) {
223
+ this.config = config;
224
+ this.transport = transport;
225
+ this.account = config.account;
226
+ const permissionPickerAvailable = hasUsableApprovalSecret(options.approvalSecret)
227
+ && options.settlePermissionSelection !== undefined;
228
+ this.capabilities = Object.freeze({
229
+ reconcileUnknownSend: false,
230
+ receipts: [],
231
+ formats: permissionPickerAvailable
232
+ ? ['plain', 'markdown', 'approval', 'model-picker', 'permission-picker']
233
+ : ['plain', 'markdown', 'approval', 'model-picker'],
234
+ inboundImages: typeof transport.downloadMessageImage === 'function',
235
+ toolApprovals: hasUsableApprovalSecret(options.approvalSecret),
236
+ });
237
+ this.now = options.now ?? Date.now;
238
+ this.approvalSecret = options.approvalSecret;
239
+ this.settleApproval = options.settleApproval;
240
+ this.recoverApprovalSettlement = options.recoverApprovalSettlement;
241
+ this.settleModelSelection = options.settleModelSelection;
242
+ this.settlePermissionSelection = options.settlePermissionSelection;
243
+ this.loadModelPicker = options.loadModelPicker;
244
+ this.advanceModelPicker = options.advanceModelPicker;
245
+ this.statusReactions = options.statusReactions ?? true;
246
+ this.progressPresenter = new LarkProgressPresenter(transport, options.showProgress ?? true, error => this.recordPresentationFailure(error));
247
+ }
248
+ async start(context) {
249
+ this.state = 'connecting';
250
+ const unsubscribe = this.transport.subscribe({
251
+ message: async (message) => {
252
+ const normalized = normalizeLarkMessage(this.config, message, this.now());
253
+ if (normalized.outcome !== 'accept')
254
+ return;
255
+ const accepted = await context.accept(normalized.envelope);
256
+ if (this.statusReactions && !accepted.duplicate && accepted.status === 'queued') {
257
+ // The durable Inbox acknowledgement is the critical path. A slow or
258
+ // unavailable reaction endpoint must not hold the WebSocket handler open.
259
+ void this.addReaction(message.messageId, 'Get');
260
+ }
261
+ },
262
+ cardAction: async (action) => this.handleCardAction(action),
263
+ reconnecting: () => {
264
+ this.cancelPendingToolApprovals('unavailable');
265
+ this.gapGeneration += 1;
266
+ this.state = 'reconnecting';
267
+ },
268
+ reconnected: () => { this.state = 'connected-with-gap'; },
269
+ error: error => {
270
+ this.lastErrorCode = error.code;
271
+ if (error.code === 'not_connected') {
272
+ this.state = 'disconnected';
273
+ this.cancelPendingToolApprovals('unavailable');
274
+ }
275
+ },
276
+ });
277
+ try {
278
+ await this.transport.connect();
279
+ this.state = 'connected';
280
+ }
281
+ catch (error) {
282
+ unsubscribe();
283
+ this.state = 'disconnected';
284
+ this.cancelPendingToolApprovals('unavailable', true);
285
+ await this.transport.disconnect().catch(() => { });
286
+ throw error;
287
+ }
288
+ let active = true;
289
+ return async () => {
290
+ if (!active)
291
+ return;
292
+ active = false;
293
+ unsubscribe();
294
+ this.state = 'disconnected';
295
+ this.cancelPendingToolApprovals('unavailable', true);
296
+ await this.transport.disconnect();
297
+ };
298
+ }
299
+ health() {
300
+ return {
301
+ state: this.state,
302
+ gapGeneration: this.gapGeneration,
303
+ ...(this.lastErrorCode === undefined ? {} : { lastErrorCode: this.lastErrorCode }),
304
+ };
305
+ }
306
+ async progress(intent) {
307
+ if (!this.matchesTarget(intent.target))
308
+ return;
309
+ await this.progressPresenter.publish(intent);
310
+ }
311
+ async requestToolApproval(input, signal) {
312
+ if (signal.aborted)
313
+ return 'cancelled';
314
+ if (!hasUsableApprovalSecret(this.approvalSecret)
315
+ || (this.state !== 'connected' && this.state !== 'connected-with-gap')
316
+ || !this.matchesTarget(input.target)
317
+ || !larkCallbackIdentifier.test(input.target.conversation.chat)
318
+ || !larkCallbackIdentifier.test(input.target.principal.user)
319
+ || input.target.conversation.kind !== 'dm'
320
+ || input.target.conversation.thread !== undefined
321
+ || typeof input.callId !== 'string'
322
+ || typeof input.arguments !== 'string'
323
+ || !boundedReviewText(input.reason, LARK_TOOL_APPROVAL_REASON_MAX_BYTES)
324
+ || !boundedReviewText(input.arguments, LARK_TOOL_APPROVAL_ARGUMENTS_MAX_BYTES)) {
325
+ return 'unavailable';
326
+ }
327
+ let now;
328
+ try {
329
+ now = this.now();
330
+ }
331
+ catch {
332
+ return 'unavailable';
333
+ }
334
+ if (!Number.isSafeInteger(now) || now < 0 || !Number.isSafeInteger(input.expiresAt)
335
+ || !Number.isSafeInteger(input.expiresAt - now) || now >= input.expiresAt) {
336
+ return 'unavailable';
337
+ }
338
+ const previousTombstone = this.toolApprovalTombstones.get(input.operationId);
339
+ if (previousTombstone !== undefined) {
340
+ if (now < previousTombstone.expiresAt)
341
+ return 'unavailable';
342
+ const expiredPending = this.pendingToolApprovals.get(input.operationId);
343
+ if (expiredPending !== undefined)
344
+ this.settleToolApproval(expiredPending, 'unavailable');
345
+ this.deleteToolApprovalTombstone(input.operationId, previousTombstone);
346
+ }
347
+ const payload = {
348
+ version: 1,
349
+ channel: this.channel,
350
+ account: this.account,
351
+ tenant: this.config.tenant,
352
+ operationId: input.operationId,
353
+ bindingId: input.bindingId,
354
+ chatId: input.target.conversation.chat,
355
+ ownerUser: input.target.principal.user,
356
+ actionHash: input.actionHash,
357
+ toolName: input.toolName,
358
+ callId: input.callId,
359
+ expiresAt: input.expiresAt,
360
+ };
361
+ let card;
362
+ try {
363
+ card = { toolApproval: {
364
+ title: '工具调用需要确认',
365
+ toolName: input.toolName,
366
+ ...(input.reason === undefined ? {} : { reason: input.reason }),
367
+ arguments: input.arguments,
368
+ allowValue: { toolApproval: signLarkToolApprovalAction(this.approvalSecret, { ...payload, decision: 'allowed-once' }) },
369
+ rejectValue: { toolApproval: signLarkToolApprovalAction(this.approvalSecret, { ...payload, decision: 'rejected' }) },
370
+ } };
371
+ if (Buffer.byteLength(renderLarkMessage(card).content, 'utf8') > LARK_APPROVAL_CARD_MAX_BYTES) {
372
+ return 'unavailable';
373
+ }
374
+ }
375
+ catch {
376
+ return 'unavailable';
377
+ }
378
+ let resolve;
379
+ const outcome = new Promise(settle => { resolve = settle; });
380
+ let pending;
381
+ const abort = () => { this.settleToolApproval(pending, 'cancelled'); };
382
+ pending = { payload, signal, abort, resolve };
383
+ const tombstone = { expiresAt: input.expiresAt };
384
+ this.pendingToolApprovals.set(input.operationId, pending);
385
+ this.toolApprovalTombstones.set(input.operationId, tombstone);
386
+ signal.addEventListener('abort', abort, { once: true });
387
+ if (signal.aborted) {
388
+ this.settleToolApproval(pending, 'cancelled');
389
+ return outcome;
390
+ }
391
+ this.armToolApprovalTombstone(input.operationId, tombstone);
392
+ if (this.pendingToolApprovals.get(input.operationId) !== pending)
393
+ return outcome;
394
+ const sendOptions = {
395
+ requestKey: `tool-approval:${input.operationId}:${input.actionHash}`,
396
+ };
397
+ let sending;
398
+ try {
399
+ sending = Promise.resolve(this.transport.send(input.target.conversation.chat, card, sendOptions));
400
+ }
401
+ catch (error) {
402
+ this.recordPresentationFailure(error);
403
+ this.settleToolApproval(pending, 'unavailable');
404
+ return outcome;
405
+ }
406
+ void sending.then(result => {
407
+ if (this.pendingToolApprovals.get(input.operationId) !== pending)
408
+ return;
409
+ if (!larkProviderMessageIdentifier.test(result.messageId)) {
410
+ this.settleToolApproval(pending, 'unavailable');
411
+ return;
412
+ }
413
+ pending.providerMessageId = result.messageId;
414
+ const earlyAction = pending.earlyAction;
415
+ delete pending.earlyAction;
416
+ if (earlyAction?.messageId === result.messageId) {
417
+ this.settleToolApproval(pending, earlyAction.decision);
418
+ }
419
+ }, error => {
420
+ this.recordPresentationFailure(error);
421
+ this.settleToolApproval(pending, 'unavailable');
422
+ });
423
+ return outcome;
424
+ }
425
+ async readInboundImage(input, signal) {
426
+ if (input.attachment.resourceType !== 'image') {
427
+ return { outcome: 'not-downloaded', failureCode: 'lark-image-type-unsupported', retryable: false };
428
+ }
429
+ const identifier = /^[A-Za-z0-9][A-Za-z0-9._@-]{0,255}$/u;
430
+ if (!identifier.test(input.eventId) || !identifier.test(input.attachment.providerRef)
431
+ || !Number.isSafeInteger(input.maxBytes) || input.maxBytes < 1) {
432
+ return { outcome: 'not-downloaded', failureCode: 'lark-image-reference-invalid', retryable: false };
433
+ }
434
+ if (signal.aborted) {
435
+ return { outcome: 'not-downloaded', failureCode: 'lark-image-aborted', retryable: true };
436
+ }
437
+ const download = this.transport.downloadMessageImage;
438
+ if (typeof download !== 'function') {
439
+ return { outcome: 'not-downloaded', failureCode: 'lark-image-download-unavailable', retryable: false };
440
+ }
441
+ try {
442
+ const image = await download.call(this.transport, input.eventId, input.attachment.providerRef, { maxBytes: input.maxBytes, signal });
443
+ if (signal.aborted) {
444
+ return { outcome: 'not-downloaded', failureCode: 'lark-image-aborted', retryable: true };
445
+ }
446
+ return {
447
+ outcome: 'downloaded',
448
+ data: image.data,
449
+ mediaType: image.mediaType,
450
+ };
451
+ }
452
+ catch (error) {
453
+ if (signal.aborted) {
454
+ return { outcome: 'not-downloaded', failureCode: 'lark-image-aborted', retryable: true };
455
+ }
456
+ const code = error instanceof LarkTransportError ? error.code : 'unknown';
457
+ const retryable = code !== 'format_error' && code !== 'permission_denied' && code !== 'target_revoked';
458
+ return {
459
+ outcome: 'not-downloaded',
460
+ failureCode: `lark-image-${code.replaceAll('_', '-')}`,
461
+ retryable,
462
+ ...(error instanceof LarkTransportError && error.retryAfterMs !== undefined
463
+ ? { retryAfterMs: error.retryAfterMs }
464
+ : {}),
465
+ };
466
+ }
467
+ }
468
+ async send(intent, signal) {
469
+ const conversation = intent.target.conversation;
470
+ if (!this.matchesTarget(intent.target)) {
471
+ return { outcome: 'not-sent', failureCode: 'lark-route-mismatch', retryable: false };
472
+ }
473
+ if (signal.aborted)
474
+ return { outcome: 'not-sent', failureCode: 'lark-aborted', retryable: true };
475
+ let input;
476
+ if (intent.format === 'approval') {
477
+ if (intent.approval === undefined || this.approvalSecret === undefined || this.settleApproval === undefined) {
478
+ return { outcome: 'not-sent', failureCode: 'lark-approval-unavailable', retryable: false };
479
+ }
480
+ if (Buffer.byteLength(intent.text, 'utf8') > this.config.maxTextBytes) {
481
+ return { outcome: 'not-sent', failureCode: 'lark-approval-too-large', retryable: false };
482
+ }
483
+ if (createHash('sha256').update(intent.text).digest('hex') !== intent.approval.diffHash) {
484
+ return { outcome: 'not-sent', failureCode: 'lark-approval-diff-mismatch', retryable: false };
485
+ }
486
+ if (this.now() >= intent.approval.expiresAt) {
487
+ return { outcome: 'not-sent', failureCode: 'lark-approval-expired', retryable: false };
488
+ }
489
+ const common = {
490
+ version: 2,
491
+ channel: conversation.channel,
492
+ account: conversation.account,
493
+ tenant: conversation.tenant,
494
+ operationId: intent.approval.operationId,
495
+ bindingId: intent.bindingId,
496
+ proposalId: intent.approval.proposalId,
497
+ expectedVersion: intent.approval.expectedVersion,
498
+ expiresAt: intent.approval.expiresAt,
499
+ chatId: conversation.chat,
500
+ diffHash: intent.approval.diffHash,
501
+ };
502
+ input = { approval: {
503
+ title: intent.approval.title,
504
+ body: intent.text,
505
+ approveValue: { approval: signLarkApprovalAction(this.approvalSecret, { ...common, decision: 'approved' }) },
506
+ rejectValue: { approval: signLarkApprovalAction(this.approvalSecret, { ...common, decision: 'rejected' }) },
507
+ } };
508
+ if (Buffer.byteLength(renderLarkMessage(input).content, 'utf8') > LARK_APPROVAL_CARD_MAX_BYTES) {
509
+ return { outcome: 'not-sent', failureCode: 'lark-approval-too-large', retryable: false };
510
+ }
511
+ }
512
+ else if (intent.format === 'permission-picker') {
513
+ if (intent.permissionPicker === undefined || this.approvalSecret === undefined
514
+ || this.settlePermissionSelection === undefined) {
515
+ return { outcome: 'not-sent', failureCode: 'lark-permission-picker-unavailable', retryable: false };
516
+ }
517
+ if (this.now() >= intent.permissionPicker.expiresAt) {
518
+ return { outcome: 'not-sent', failureCode: 'lark-permission-picker-expired', retryable: false };
519
+ }
520
+ input = { permissionPicker: permissionPickerCard(intent.permissionPicker, this.approvalSecret, {
521
+ channel: conversation.channel,
522
+ account: conversation.account,
523
+ tenant: conversation.tenant,
524
+ bindingId: intent.bindingId,
525
+ chatId: conversation.chat,
526
+ ownerUser: intent.target.principal.user,
527
+ }) };
528
+ }
529
+ else if (intent.format === 'model-picker') {
530
+ if (intent.modelPicker === undefined || this.approvalSecret === undefined
531
+ || this.settleModelSelection === undefined || this.loadModelPicker === undefined
532
+ || this.advanceModelPicker === undefined) {
533
+ return { outcome: 'not-sent', failureCode: 'lark-model-picker-unavailable', retryable: false };
534
+ }
535
+ if (this.now() >= intent.modelPicker.expiresAt) {
536
+ return { outcome: 'not-sent', failureCode: 'lark-model-picker-expired', retryable: false };
537
+ }
538
+ const picker = intent.modelPicker;
539
+ input = { modelPicker: modelPickerCard(picker, this.approvalSecret, {
540
+ operationId: picker.operationId,
541
+ bindingId: intent.bindingId,
542
+ expiresAt: picker.expiresAt,
543
+ chatId: conversation.chat,
544
+ revision: 0,
545
+ }) };
546
+ }
547
+ else {
548
+ input = intent.format === 'markdown' ? { markdown: intent.text } : { text: intent.text };
549
+ }
550
+ // A synthetic lane represents all standalone group messages from one
551
+ // sender. Replying to the inbound event would make Lark create a provider
552
+ // thread; the user's next reply would then carry a real rootId and be
553
+ // normalized into a different Delivery conversation/session. Keep this
554
+ // lane top-level so later standalone messages retain the same context.
555
+ const syntheticTopLevelGroup = conversation.kind === 'group'
556
+ && isLarkTopLevelSenderThread(conversation.thread);
557
+ const providerReplyTarget = syntheticTopLevelGroup
558
+ ? undefined
559
+ : (intent.replyToEventId
560
+ ?? (conversation.kind === 'group' ? conversation.thread : undefined));
561
+ const options = {
562
+ requestKey: intent.idempotencyKey,
563
+ ...(providerReplyTarget === undefined ? {} : { replyTo: providerReplyTarget }),
564
+ ...(conversation.kind === 'group' && providerReplyTarget !== undefined ? { replyInThread: true } : {}),
565
+ };
566
+ let result;
567
+ try {
568
+ result = await this.transport.send(conversation.chat, input, options);
569
+ }
570
+ catch (error) {
571
+ if (!(error instanceof LarkTransportError) || error.code !== 'format_error') {
572
+ return sendFailure(error);
573
+ }
574
+ const fallback = intent.format === 'permission-picker' && intent.permissionPicker !== undefined
575
+ ? { text: intent.text, suffix: 'permission-picker-fallback' }
576
+ : intent.format === 'model-picker' && intent.modelPicker !== undefined
577
+ ? { text: modelPickerFallbackText(intent), suffix: 'model-picker-fallback' }
578
+ : undefined;
579
+ if (fallback === undefined)
580
+ return sendFailure(error);
581
+ try {
582
+ result = await this.transport.send(conversation.chat, { text: fallback.text }, {
583
+ ...options,
584
+ requestKey: `${intent.idempotencyKey}:${fallback.suffix}`,
585
+ });
586
+ }
587
+ catch (fallbackError) {
588
+ return sendFailure(fallbackError);
589
+ }
590
+ }
591
+ if (!/^[A-Za-z0-9][A-Za-z0-9._@/-]{0,255}$/u.test(result.messageId)) {
592
+ return { outcome: 'unknown', failureCode: 'invalid-provider-result' };
593
+ }
594
+ if (this.statusReactions && intent.replyToEventId !== undefined) {
595
+ void this.addReaction(intent.replyToEventId, 'DONE');
596
+ }
597
+ return { outcome: 'accepted', providerMessageId: result.messageId };
598
+ }
599
+ matchesTarget(target) {
600
+ const { conversation, principal } = target;
601
+ return conversation.channel === this.channel
602
+ && principal.channel === this.channel
603
+ && conversation.account === this.account
604
+ && principal.account === this.account
605
+ && conversation.tenant === this.config.tenant
606
+ && principal.tenant === this.config.tenant;
607
+ }
608
+ armToolApprovalTombstone(operationId, tombstone) {
609
+ const expire = () => {
610
+ if (this.toolApprovalTombstones.get(operationId) !== tombstone)
611
+ return;
612
+ let remaining;
613
+ try {
614
+ remaining = tombstone.expiresAt - this.now();
615
+ }
616
+ catch {
617
+ const pending = this.pendingToolApprovals.get(operationId);
618
+ if (pending !== undefined)
619
+ this.settleToolApproval(pending, 'unavailable');
620
+ tombstone.timer = setTimeout(expire, 1_000);
621
+ tombstone.timer.unref();
622
+ return;
623
+ }
624
+ if (!Number.isSafeInteger(remaining)) {
625
+ const pending = this.pendingToolApprovals.get(operationId);
626
+ if (pending !== undefined)
627
+ this.settleToolApproval(pending, 'unavailable');
628
+ tombstone.timer = setTimeout(expire, 1_000);
629
+ tombstone.timer.unref();
630
+ return;
631
+ }
632
+ if (remaining <= 0) {
633
+ const pending = this.pendingToolApprovals.get(operationId);
634
+ if (pending !== undefined)
635
+ this.settleToolApproval(pending, 'unavailable');
636
+ this.deleteToolApprovalTombstone(operationId, tombstone);
637
+ return;
638
+ }
639
+ tombstone.timer = setTimeout(expire, Math.min(remaining, LARK_TOOL_APPROVAL_MAX_TIMER_MS));
640
+ tombstone.timer.unref();
641
+ };
642
+ expire();
643
+ }
644
+ deleteToolApprovalTombstone(operationId, tombstone) {
645
+ if (this.toolApprovalTombstones.get(operationId) !== tombstone)
646
+ return false;
647
+ this.toolApprovalTombstones.delete(operationId);
648
+ if (tombstone.timer !== undefined)
649
+ clearTimeout(tombstone.timer);
650
+ return true;
651
+ }
652
+ settleToolApproval(pending, outcome) {
653
+ if (this.pendingToolApprovals.get(pending.payload.operationId) !== pending)
654
+ return false;
655
+ this.pendingToolApprovals.delete(pending.payload.operationId);
656
+ pending.signal.removeEventListener('abort', pending.abort);
657
+ pending.resolve(outcome);
658
+ return true;
659
+ }
660
+ cancelPendingToolApprovals(outcome, clearTombstones = false) {
661
+ for (const pending of this.pendingToolApprovals.values()) {
662
+ this.settleToolApproval(pending, outcome);
663
+ }
664
+ if (!clearTombstones)
665
+ return;
666
+ for (const [operationId, tombstone] of this.toolApprovalTombstones) {
667
+ this.deleteToolApprovalTombstone(operationId, tombstone);
668
+ }
669
+ }
670
+ async addReaction(messageId, emojiType) {
671
+ try {
672
+ await this.transport.addReaction(messageId, emojiType);
673
+ }
674
+ catch (error) {
675
+ this.recordPresentationFailure(error);
676
+ }
677
+ }
678
+ recordPresentationFailure(error) {
679
+ this.lastErrorCode = error instanceof LarkTransportError ? error.code : 'unknown';
680
+ }
681
+ async handleCardAction(action) {
682
+ try {
683
+ const value = action.value;
684
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
685
+ throw new LarkApprovalError('invalid', 'Lark card action value is invalid');
686
+ }
687
+ if (typeof value.toolApproval === 'string') {
688
+ if (Object.keys(value).length !== 1) {
689
+ throw new LarkToolApprovalError('invalid', 'Lark tool approval action value is invalid');
690
+ }
691
+ return this.handleToolApprovalAction(action, value.toolApproval);
692
+ }
693
+ if (Object.prototype.hasOwnProperty.call(value, 'permissionPicker')) {
694
+ return await this.handlePermissionPickerAction(action, parseLarkPermissionPickerCallback(value));
695
+ }
696
+ if (typeof value.modelPicker === 'string') {
697
+ return await this.handleModelPickerAction(action, parseLarkModelPickerCallback(value));
698
+ }
699
+ if (Object.keys(value).length !== 1 || typeof value.approval !== 'string'
700
+ || this.approvalSecret === undefined) {
701
+ throw new LarkApprovalError('invalid', 'Lark approval action value is invalid');
702
+ }
703
+ const token = value.approval;
704
+ let expired = false;
705
+ let payload;
706
+ try {
707
+ payload = verifyLarkApprovalAction(this.approvalSecret, token, this.now());
708
+ }
709
+ catch (error) {
710
+ if (!(error instanceof LarkApprovalError) || error.code !== 'expired')
711
+ throw error;
712
+ payload = verifyLarkApprovalActionForRecovery(this.approvalSecret, token);
713
+ expired = true;
714
+ }
715
+ if (payload.channel !== this.channel || payload.account !== this.account
716
+ || payload.tenant !== this.config.tenant || payload.chatId !== action.chatId) {
717
+ throw new LarkApprovalError('invalid', 'Lark approval route does not match');
718
+ }
719
+ const callbackEventId = createHash('sha256')
720
+ .update(`${action.messageId}\0${action.operatorId}\0${token}`)
721
+ .digest('hex');
722
+ const settlement = {
723
+ operationId: payload.operationId,
724
+ callbackEventId,
725
+ callbackChatId: action.chatId,
726
+ bindingId: payload.bindingId,
727
+ principal: { channel: 'lark', account: this.account, tenant: this.config.tenant, user: action.operatorId },
728
+ proposalId: payload.proposalId,
729
+ expectedVersion: payload.expectedVersion,
730
+ diffHash: payload.diffHash,
731
+ decision: payload.decision,
732
+ reason: `Lark owner ${payload.decision}`,
733
+ };
734
+ if (expired) {
735
+ if (this.recoverApprovalSettlement === undefined
736
+ || await this.recoverApprovalSettlement(settlement) === undefined) {
737
+ throw new LarkApprovalError('invalid', 'expired Lark approval settlement is not recoverable');
738
+ }
739
+ }
740
+ else {
741
+ if (this.settleApproval === undefined) {
742
+ throw new LarkApprovalError('invalid', 'Lark approval settlement is unavailable');
743
+ }
744
+ await this.settleApproval(settlement);
745
+ }
746
+ }
747
+ catch (error) {
748
+ if (error instanceof LarkPermissionPickerError) {
749
+ this.lastErrorCode = 'format_error';
750
+ return { toast: { type: 'warning', content: error.code === 'expired'
751
+ ? '权限卡片已过期,请重新发送 /permission'
752
+ : '权限卡片无效或已失效,请重新发送 /permission' } };
753
+ }
754
+ if (error instanceof LarkApprovalError || error instanceof LarkModelPickerError
755
+ || error instanceof LarkToolApprovalError) {
756
+ this.lastErrorCode = 'format_error';
757
+ return;
758
+ }
759
+ throw error;
760
+ }
761
+ }
762
+ async handlePermissionPickerAction(action, callback) {
763
+ if (!hasUsableApprovalSecret(this.approvalSecret) || this.settlePermissionSelection === undefined
764
+ || action.tag !== 'button' || !larkProviderMessageIdentifier.test(action.messageId)) {
765
+ throw new LarkPermissionPickerError('invalid', 'Lark permission picker callback is unavailable');
766
+ }
767
+ const token = callback.permissionPicker;
768
+ const payload = verifyLarkPermissionPickerAction(this.approvalSecret, token, this.now());
769
+ if (payload.channel !== this.channel || payload.account !== this.account
770
+ || payload.tenant !== this.config.tenant || payload.chatId !== action.chatId
771
+ || payload.ownerUser !== action.operatorId) {
772
+ throw new LarkPermissionPickerError('invalid', 'Lark permission picker route does not match');
773
+ }
774
+ const callbackEventId = createHash('sha256')
775
+ .update(`${action.messageId}\0${action.operatorId}\0${token}`)
776
+ .digest('hex');
777
+ try {
778
+ await this.settlePermissionSelection({
779
+ operationId: payload.operationId,
780
+ callbackEventId,
781
+ callbackChatId: action.chatId,
782
+ cardMessageId: action.messageId,
783
+ bindingId: payload.bindingId,
784
+ bindingVersion: payload.bindingVersion,
785
+ sessionId: payload.sessionId,
786
+ principal: {
787
+ channel: payload.channel,
788
+ account: payload.account,
789
+ tenant: payload.tenant,
790
+ user: payload.ownerUser,
791
+ },
792
+ issuedAt: payload.issuedAt,
793
+ expiresAt: payload.expiresAt,
794
+ expectedStateHash: payload.expectedStateHash,
795
+ emergencyStopVersion: payload.emergencyStopVersion,
796
+ targetLevel: payload.level,
797
+ });
798
+ return { toast: { type: 'success', content: '权限切换已受理' } };
799
+ }
800
+ catch (error) {
801
+ this.recordPresentationFailure(error);
802
+ return { toast: { type: 'warning', content: '权限卡片状态已更新,请重新发送 /permission' } };
803
+ }
804
+ }
805
+ handleToolApprovalAction(action, token) {
806
+ if (!hasUsableApprovalSecret(this.approvalSecret) || action.tag !== 'button'
807
+ || !larkProviderMessageIdentifier.test(action.messageId)) {
808
+ throw new LarkToolApprovalError('invalid', 'Lark tool approval callback is unavailable');
809
+ }
810
+ const payload = verifyLarkToolApprovalAction(this.approvalSecret, token, this.now());
811
+ if (payload.channel !== this.channel || payload.account !== this.account
812
+ || payload.tenant !== this.config.tenant || payload.chatId !== action.chatId
813
+ || payload.ownerUser !== action.operatorId) {
814
+ throw new LarkToolApprovalError('invalid', 'Lark tool approval route does not match');
815
+ }
816
+ const pending = this.pendingToolApprovals.get(payload.operationId);
817
+ if (pending === undefined || !sameToolApprovalPayload(pending.payload, payload)) {
818
+ throw new LarkToolApprovalError('invalid', 'Lark tool approval request does not match');
819
+ }
820
+ if (pending.providerMessageId === undefined) {
821
+ const earlyAction = pending.earlyAction;
822
+ if (earlyAction !== undefined && (earlyAction.messageId !== action.messageId
823
+ || earlyAction.decision !== payload.decision)) {
824
+ throw new LarkToolApprovalError('invalid', 'Lark tool approval request does not match');
825
+ }
826
+ pending.earlyAction = { messageId: action.messageId, decision: payload.decision };
827
+ return { toast: { type: 'info', content: '正在确认工具审批卡片' } };
828
+ }
829
+ if (pending.providerMessageId !== action.messageId) {
830
+ throw new LarkToolApprovalError('invalid', 'Lark tool approval request does not match');
831
+ }
832
+ if (!this.settleToolApproval(pending, payload.decision)) {
833
+ throw new LarkToolApprovalError('invalid', 'Lark tool approval request is no longer pending');
834
+ }
835
+ return { toast: { type: payload.decision === 'allowed-once' ? 'success' : 'info',
836
+ content: payload.decision === 'allowed-once' ? '已仅允许本次工具调用' : '已拒绝工具调用' } };
837
+ }
838
+ async handleModelPickerAction(action, callback) {
839
+ if (this.approvalSecret === undefined || this.settleModelSelection === undefined) {
840
+ throw new LarkModelPickerError('invalid', 'Lark model picker callback is unavailable');
841
+ }
842
+ const token = callback.modelPicker;
843
+ const payload = verifyLarkModelPickerAction(this.approvalSecret, token, this.now());
844
+ if (payload.chatId !== action.chatId)
845
+ throw new LarkModelPickerError('invalid', 'Lark model picker chat does not match');
846
+ if (payload.action !== 'confirm') {
847
+ const selectedOption = action.option;
848
+ if (this.loadModelPicker === undefined || this.advanceModelPicker === undefined
849
+ || typeof selectedOption !== 'string') {
850
+ throw new LarkModelPickerError('invalid', 'Lark model picker navigation is unavailable');
851
+ }
852
+ if (action.tag !== 'select_static') {
853
+ throw new LarkModelPickerError('invalid', 'Lark model picker navigation action is invalid');
854
+ }
855
+ let picker;
856
+ try {
857
+ picker = await this.loadModelPicker({
858
+ operationId: payload.operationId,
859
+ callbackChatId: action.chatId,
860
+ bindingId: payload.bindingId,
861
+ principal: { channel: 'lark', account: this.account, tenant: this.config.tenant, user: action.operatorId },
862
+ });
863
+ }
864
+ catch (error) {
865
+ this.recordPresentationFailure(error);
866
+ return { toast: { type: 'warning', content: '模型选择已结束,请重新发送 /model' } };
867
+ }
868
+ if (picker === undefined || picker.operationId !== payload.operationId || picker.expiresAt !== payload.expiresAt) {
869
+ return { toast: { type: 'warning', content: '模型选择已结束,请重新发送 /model' } };
870
+ }
871
+ const capability = {
872
+ operationId: payload.operationId,
873
+ bindingId: payload.bindingId,
874
+ expiresAt: payload.expiresAt,
875
+ chatId: payload.chatId,
876
+ };
877
+ let preferred;
878
+ if (payload.action === 'provider') {
879
+ if (!picker.providers.some(provider => provider.id === selectedOption)
880
+ || !picker.models.some(model => model.provider === selectedOption)) {
881
+ throw new LarkModelPickerError('invalid', 'Lark model picker provider is unavailable');
882
+ }
883
+ preferred = {
884
+ provider: selectedOption,
885
+ effort: null,
886
+ };
887
+ }
888
+ else if (payload.action === 'model') {
889
+ const selected = routeParts(selectedOption);
890
+ if (selected === undefined || selected.provider !== payload.provider
891
+ || !picker.models.some(model => model.provider === selected.provider && model.id === selected.model)) {
892
+ throw new LarkModelPickerError('invalid', 'Lark model picker model is unavailable');
893
+ }
894
+ preferred = {
895
+ provider: selected.provider,
896
+ route: selectedOption,
897
+ effort: null,
898
+ };
899
+ }
900
+ else {
901
+ const selectedModel = picker.models.find(model => model.provider === payload.provider && model.id === payload.model);
902
+ const defaultEffort = selectedModel === undefined ? undefined : defaultEffortOption(selectedModel.effortIds);
903
+ if (selectedModel === undefined || (selectedOption !== defaultEffort && !selectedModel.effortIds.includes(selectedOption))) {
904
+ throw new LarkModelPickerError('invalid', 'Lark model picker effort is unavailable');
905
+ }
906
+ preferred = {
907
+ provider: payload.provider,
908
+ route: `${payload.provider}/${payload.model}`,
909
+ effort: selectedOption === defaultEffort ? null : selectedOption,
910
+ };
911
+ }
912
+ const next = resolvedModelRoute(resolveModelPicker(picker, preferred));
913
+ let advanced;
914
+ try {
915
+ advanced = await this.advanceModelPicker({
916
+ operationId: payload.operationId,
917
+ callbackChatId: action.chatId,
918
+ bindingId: payload.bindingId,
919
+ principal: { channel: 'lark', account: this.account, tenant: this.config.tenant, user: action.operatorId },
920
+ expected: { revision: payload.revision, provider: payload.provider, model: payload.model,
921
+ ...(payload.effort === null ? {} : { reasoningEffort: payload.effort }) },
922
+ next,
923
+ });
924
+ }
925
+ catch (error) {
926
+ this.recordPresentationFailure(error);
927
+ return { toast: { type: 'warning', content: '模型选择已结束,请重新发送 /model' } };
928
+ }
929
+ const state = advanced.state;
930
+ return callbackCard(modelPickerCard(picker, this.approvalSecret, {
931
+ ...capability, revision: state.revision,
932
+ }, {
933
+ provider: state.provider,
934
+ route: `${state.provider}/${state.model}`,
935
+ effort: state.reasoningEffort ?? null,
936
+ }));
937
+ }
938
+ if (action.tag !== 'button') {
939
+ throw new LarkModelPickerError('invalid', 'Lark model picker confirmation action is invalid');
940
+ }
941
+ const callbackEventId = createHash('sha256')
942
+ .update(`${action.messageId}\0${action.operatorId}\0${token}`)
943
+ .digest('hex');
944
+ try {
945
+ await this.settleModelSelection({
946
+ operationId: payload.operationId,
947
+ callbackEventId,
948
+ callbackChatId: action.chatId,
949
+ bindingId: payload.bindingId,
950
+ principal: { channel: 'lark', account: this.account, tenant: this.config.tenant, user: action.operatorId },
951
+ provider: payload.provider,
952
+ modelProvider: payload.provider,
953
+ model: payload.model,
954
+ expectedRevision: payload.revision,
955
+ ...(payload.effort === null ? {} : { reasoningEffort: payload.effort }),
956
+ });
957
+ return { toast: { type: 'success', content: '模型切换已受理' } };
958
+ }
959
+ catch (error) {
960
+ this.recordPresentationFailure(error);
961
+ return { toast: { type: 'warning', content: '卡片状态已更新,请重新发送 /model' } };
962
+ }
963
+ }
964
+ }
965
+ //# sourceMappingURL=adapter.js.map