dsh-lark-agent 0.1.2 → 0.2.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.md +6 -2
- package/dist/bot-menu.d.ts +1 -0
- package/dist/bot-menu.js +2 -0
- package/dist/bot-service.js +4 -2
- package/dist/bridge.d.ts +42 -1
- package/dist/bridge.js +340 -18
- package/dist/card-actions.js +1 -1
- package/dist/client.cjs +3 -3
- package/dist/config.d.ts +8 -0
- package/dist/config.js +8 -2
- package/dist/dsh-runtime.d.ts +75 -2
- package/dist/dsh-runtime.js +346 -23
- package/dist/interaction-cards.d.ts +23 -0
- package/dist/interaction-cards.js +119 -0
- package/dist/lark-cli.d.ts +24 -0
- package/dist/lark-cli.js +92 -4
- package/dist/live-reply.d.ts +87 -0
- package/dist/live-reply.js +389 -0
- package/dist/onboarding/controller.js +9 -7
- package/dist/onboarding/lark-platform.js +5 -5
- package/dist/permission-controls.d.ts +13 -0
- package/dist/permission-controls.js +26 -0
- package/dist/permission-preferences.d.ts +26 -0
- package/dist/permission-preferences.js +169 -0
- package/dist/presentation.d.ts +7 -0
- package/dist/replies.d.ts +5 -0
- package/dist/replies.js +9 -1
- package/dist/rich-scopes.d.ts +4 -0
- package/dist/rich-scopes.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { cardMarkdown, renderPanel, safePlainText } from './replies.js';
|
|
2
|
+
const code = (value) => '`' + value.replace(/`/g, 'ˋ') + '`';
|
|
3
|
+
/** A fence longer than any backtick run inside keeps the arguments one code block. */
|
|
4
|
+
function fenced(text) {
|
|
5
|
+
const marks = '`'.repeat(Math.max(2, ...Array.from(text.matchAll(/`+/g), match => match[0].length)) + 1);
|
|
6
|
+
return `${marks}\n${text}\n${marks}`;
|
|
7
|
+
}
|
|
8
|
+
/** Buttons answer one short single-choice question in one tap; anything else uses a form. */
|
|
9
|
+
export function simpleQuestion(questions) {
|
|
10
|
+
const only = questions[0];
|
|
11
|
+
return questions.length === 1 && !only.multiSelect && only.options.length >= 1 && only.options.length <= 6;
|
|
12
|
+
}
|
|
13
|
+
/** The decision card. Without tokens it shows the settled outcome and no buttons. */
|
|
14
|
+
export function approvalCard(prompt, tokens, outcome) {
|
|
15
|
+
return renderPanel({
|
|
16
|
+
title: tokens ? '需要你确认' : '确认结果',
|
|
17
|
+
summary: `是否允许使用工具 ${prompt.toolName}`,
|
|
18
|
+
sections: [
|
|
19
|
+
{ title: '工具', text: code(prompt.toolName) },
|
|
20
|
+
...(prompt.reason ? [{ title: '原因', text: prompt.reason }] : []),
|
|
21
|
+
...(prompt.detail ? [{ title: '参数', text: fenced(prompt.detail) }] : []),
|
|
22
|
+
...(outcome ? [{ text: `**${outcome}**` }] : []),
|
|
23
|
+
],
|
|
24
|
+
...(tokens ? { actions: [{ label: '允许一次', value: { token: tokens.allow }, primary: true }, { label: '拒绝', value: { token: tokens.deny } }],
|
|
25
|
+
actionColumns: 2, note: '只允许这一次,不改变电脑上的权限设置。5 分钟内没有确认会按拒绝处理。' } : {}),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
function questionText(question) {
|
|
29
|
+
return [`**${question.question}**`,
|
|
30
|
+
...question.options.filter(option => option.description).map(option => `- ${option.label}:${option.description}`),
|
|
31
|
+
...(question.detail ? [question.detail] : [])].join('\n');
|
|
32
|
+
}
|
|
33
|
+
/** A question card: buttons for one short choice, otherwise a form. Without tokens it shows the outcome. */
|
|
34
|
+
export function questionCard(questions, tokens, outcome) {
|
|
35
|
+
const first = questions[0];
|
|
36
|
+
const title = questions.length === 1 && first.header ? first.header : '需要你选择';
|
|
37
|
+
const form = Boolean(tokens?.reply);
|
|
38
|
+
const base = renderPanel({
|
|
39
|
+
title, summary: `${title}:${first.question}`,
|
|
40
|
+
sections: [
|
|
41
|
+
...(form ? [{ text: questions.length > 1 ? `请回答下面 ${questions.length} 个问题,然后提交。` : '请选择或填写后提交。' }]
|
|
42
|
+
: questions.map(question => ({ text: questionText(question) }))),
|
|
43
|
+
...(outcome ? [{ text: `**${outcome}**` }] : []),
|
|
44
|
+
],
|
|
45
|
+
...(tokens?.choose ? { actions: [...first.options.map((option, index) => ({ label: option.label, value: { token: tokens.choose[index] }, primary: index === 0 })),
|
|
46
|
+
{ label: '跳过', value: { token: tokens.choose[first.options.length] } }], actionColumns: 1 } : {}),
|
|
47
|
+
...(tokens ? { note: '10 分钟内没有回答,Harness 会按未回答继续。' } : {}),
|
|
48
|
+
});
|
|
49
|
+
if (!form || base.kind !== 'card')
|
|
50
|
+
return base;
|
|
51
|
+
const plain = (content) => ({ tag: 'plain_text', content: safePlainText(content) });
|
|
52
|
+
base.card.body.elements.push({ tag: 'form', name: 'dsh_result_form', elements: [
|
|
53
|
+
// Field names come from positions, never from the model-chosen question IDs.
|
|
54
|
+
...questions.flatMap((question, index) => [
|
|
55
|
+
{ tag: 'markdown', content: cardMarkdown(questionText(question)) },
|
|
56
|
+
...(question.options.length ? [{ tag: question.multiSelect ? 'multi_select_static' : 'select_static', name: `q${index}`,
|
|
57
|
+
placeholder: plain(question.multiSelect ? '可多选' : '请选择'), required: false, width: 'fill',
|
|
58
|
+
options: question.options.map((option, choice) => ({ text: plain(option.label), value: `o${choice}` })) }] : []),
|
|
59
|
+
{ tag: 'input', name: `c${index}`, placeholder: plain(question.options.length ? '其他想法(可选)' : '在这里输入'),
|
|
60
|
+
input_type: 'text', max_length: 1000, required: false, width: 'fill' },
|
|
61
|
+
]),
|
|
62
|
+
{ tag: 'button', name: 'dsh_submit', form_name: 'dsh_result_form', text: plain('提交'), type: 'primary', action_type: 'form_submit',
|
|
63
|
+
form_action_type: 'submit', behaviors: [{ type: 'callback', value: { token: tokens.reply } }] },
|
|
64
|
+
] });
|
|
65
|
+
// An oversized question cannot be asked by card; the caller then lets the model ask in text.
|
|
66
|
+
if (Buffer.byteLength(JSON.stringify({ msg_type: 'interactive', content: JSON.stringify(base.card) })) > 27_000)
|
|
67
|
+
return { kind: 'text', text: base.fallbackText };
|
|
68
|
+
return base;
|
|
69
|
+
}
|
|
70
|
+
/** Map a click or form back to the host's answer shape, echoing the original option labels. */
|
|
71
|
+
export function questionAnswers(questions, answer) {
|
|
72
|
+
if (answer.verb === 'choose') {
|
|
73
|
+
if (!simpleQuestion(questions) || answer.index === undefined)
|
|
74
|
+
return;
|
|
75
|
+
const question = questions[0];
|
|
76
|
+
if (answer.index === question.options.length)
|
|
77
|
+
return [{ id: question.id, selected: [] }];
|
|
78
|
+
const option = question.options[answer.index];
|
|
79
|
+
return option ? [{ id: question.id, selected: [option.label] }] : undefined;
|
|
80
|
+
}
|
|
81
|
+
if (answer.verb !== 'reply' || !answer.formValues || typeof answer.formValues !== 'object' || Array.isArray(answer.formValues))
|
|
82
|
+
return;
|
|
83
|
+
const values = answer.formValues;
|
|
84
|
+
const fields = new Set(questions.flatMap((question, index) => [...(question.options.length ? [`q${index}`] : []), `c${index}`]));
|
|
85
|
+
if (Object.keys(values).some(key => !fields.has(key)))
|
|
86
|
+
return;
|
|
87
|
+
const answers = [];
|
|
88
|
+
for (const [index, question] of questions.entries()) {
|
|
89
|
+
const raw = values[`q${index}`];
|
|
90
|
+
const picked = raw === undefined || raw === null || raw === '' ? [] : Array.isArray(raw) ? raw : [raw];
|
|
91
|
+
if (!question.multiSelect && picked.length > 1)
|
|
92
|
+
return;
|
|
93
|
+
let selected = [];
|
|
94
|
+
for (const value of picked) {
|
|
95
|
+
const choice = typeof value === 'string' ? /^o([0-9]{1,2})$/.exec(value)?.[1] : undefined;
|
|
96
|
+
const option = choice === undefined ? undefined : question.options[Number(choice)];
|
|
97
|
+
if (!option || selected.includes(option.label))
|
|
98
|
+
return;
|
|
99
|
+
selected.push(option.label);
|
|
100
|
+
}
|
|
101
|
+
const custom = values[`c${index}`];
|
|
102
|
+
if (custom !== undefined && custom !== null && (typeof custom !== 'string' || custom.length > 1000))
|
|
103
|
+
return;
|
|
104
|
+
const text = typeof custom === 'string' ? custom.trim() : '';
|
|
105
|
+
// For a single choice, typed text replaces the selection, as the host defines.
|
|
106
|
+
if (text && !question.multiSelect)
|
|
107
|
+
selected = [];
|
|
108
|
+
answers.push({ id: question.id, selected, ...(text ? { custom: text } : {}) });
|
|
109
|
+
}
|
|
110
|
+
return answers;
|
|
111
|
+
}
|
|
112
|
+
export function answeredText(questions, answers) {
|
|
113
|
+
if (questions.length !== 1)
|
|
114
|
+
return '已提交回答。';
|
|
115
|
+
const [answer] = answers;
|
|
116
|
+
const value = answer?.custom ?? answer?.selected.join('、');
|
|
117
|
+
return value ? `已回答:${value}` : '已跳过这个问题。';
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=interaction-cards.js.map
|
package/dist/lark-cli.d.ts
CHANGED
|
@@ -58,6 +58,30 @@ export declare class LarkCli {
|
|
|
58
58
|
/** Replace one bot-owned loading/interactive card, never retry an unknown update. */
|
|
59
59
|
updateFeedback(messageId: string, payload: ReplyPayload): Promise<void>;
|
|
60
60
|
private feedbackContent;
|
|
61
|
+
/** Create a CardKit card entity. Nothing reaches a chat yet, so a failure needs no delivery guard. */
|
|
62
|
+
createCardEntity(card: Record<string, unknown>): Promise<string>;
|
|
63
|
+
/** Reply with a card entity. An unconfirmed reply may still have been delivered; never resend it. */
|
|
64
|
+
replyCardEntity(messageId: string, cardId: string, options: {
|
|
65
|
+
idempotencyKey: string;
|
|
66
|
+
inThread: boolean;
|
|
67
|
+
}): Promise<void>;
|
|
68
|
+
/** Stream one element's complete text. Any later update supersedes a failed one. */
|
|
69
|
+
streamCardText(cardId: string, elementId: string, content: string, sequence: number): Promise<void>;
|
|
70
|
+
/** Replace a card entity. Applying the same state twice is harmless, so callers may retry with a newer sequence. */
|
|
71
|
+
updateCardEntity(cardId: string, card: Record<string, unknown>, sequence: number): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Reply with one local file the caller has already validated. Only a
|
|
74
|
+
* `./basename` crosses argv, resolved from the file's own directory, so it
|
|
75
|
+
* can never be read as an upload key, a URL or another flag.
|
|
76
|
+
*/
|
|
77
|
+
replyFile(messageId: string, path: string, options: {
|
|
78
|
+
idempotencyKey: string;
|
|
79
|
+
inThread: boolean;
|
|
80
|
+
image: boolean;
|
|
81
|
+
}): Promise<void>;
|
|
82
|
+
private cardEntityData;
|
|
83
|
+
private static cardId;
|
|
84
|
+
private static sequence;
|
|
61
85
|
private confirmFeedback;
|
|
62
86
|
/** Menu events have no message ID: create a message for the verified actor instead. */
|
|
63
87
|
sendRichUser(openId: string, payload: ReplyPayload, options: {
|
package/dist/lark-cli.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, isAbsolute } from 'node:path';
|
|
2
4
|
import { StringDecoder } from 'node:string_decoder';
|
|
3
5
|
const DEFAULT_EVENT_KEY = 'im.message.receive_v1';
|
|
4
6
|
const MAX_OUTPUT_BYTES = 1_048_576;
|
|
@@ -126,11 +128,12 @@ export class LarkCli {
|
|
|
126
128
|
this.readyTimeoutMs = timeout(options.readyTimeoutMs, 30_000);
|
|
127
129
|
this.commandTimeoutMs = timeout(options.commandTimeoutMs, 30_000);
|
|
128
130
|
}
|
|
129
|
-
launch(args) {
|
|
131
|
+
launch(args, cwd) {
|
|
130
132
|
return spawn(this.executable, ['--profile', this.profile, ...args], {
|
|
131
133
|
shell: false,
|
|
132
134
|
windowsHide: true,
|
|
133
135
|
stdio: 'pipe',
|
|
136
|
+
...(cwd ? { cwd } : {}),
|
|
134
137
|
});
|
|
135
138
|
}
|
|
136
139
|
start(onEvent, onError) {
|
|
@@ -388,6 +391,91 @@ export class LarkCli {
|
|
|
388
391
|
throw new Error('Invalid Lark feedback card.');
|
|
389
392
|
}
|
|
390
393
|
}
|
|
394
|
+
/** Create a CardKit card entity. Nothing reaches a chat yet, so a failure needs no delivery guard. */
|
|
395
|
+
async createCardEntity(card) {
|
|
396
|
+
const data = this.cardEntityData(card);
|
|
397
|
+
const result = await this.executeJson(['api', 'POST', '/open-apis/cardkit/v1/cards', '--data', '-', '--as', 'bot', '--format', 'json'], JSON.stringify({ type: 'card_json', data }), 'card', Math.min(this.commandTimeoutMs, 10_000));
|
|
398
|
+
this.confirmFeedback(result);
|
|
399
|
+
const receipt = result.data;
|
|
400
|
+
if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt) || !LarkCli.cardId(receipt.card_id)) {
|
|
401
|
+
throw new Error('Lark card entity creation was not confirmed.');
|
|
402
|
+
}
|
|
403
|
+
return receipt.card_id;
|
|
404
|
+
}
|
|
405
|
+
/** Reply with a card entity. An unconfirmed reply may still have been delivered; never resend it. */
|
|
406
|
+
async replyCardEntity(messageId, cardId, options) {
|
|
407
|
+
if (typeof messageId !== 'string' || !/^om_[A-Za-z0-9_-]{1,256}$/.test(messageId) || !LarkCli.cardId(cardId)
|
|
408
|
+
|| typeof options?.idempotencyKey !== 'string' || !/^[A-Za-z0-9._:-]{1,50}$/.test(options.idempotencyKey)
|
|
409
|
+
|| typeof options.inThread !== 'boolean')
|
|
410
|
+
throw new Error('Invalid Lark card reply.');
|
|
411
|
+
const body = JSON.stringify({ msg_type: 'interactive', content: JSON.stringify({ type: 'card', data: { card_id: cardId } }),
|
|
412
|
+
uuid: options.idempotencyKey, reply_in_thread: options.inThread });
|
|
413
|
+
const result = await this.executeJson(['api', 'POST', `/open-apis/im/v1/messages/${messageId}/reply`,
|
|
414
|
+
'--data', '-', '--as', 'bot', '--format', 'json'], body, 'reply', this.commandTimeoutMs);
|
|
415
|
+
this.confirmFeedback(result);
|
|
416
|
+
const receipt = result.data;
|
|
417
|
+
if (!receipt || typeof receipt !== 'object' || typeof receipt.message_id !== 'string' || !/^om_[A-Za-z0-9_-]{1,256}$/.test(receipt.message_id)) {
|
|
418
|
+
throw new Error('Lark card reply was not confirmed; delivery is unknown.');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** Stream one element's complete text. Any later update supersedes a failed one. */
|
|
422
|
+
async streamCardText(cardId, elementId, content, sequence) {
|
|
423
|
+
if (!LarkCli.cardId(cardId) || typeof elementId !== 'string' || !/^[a-z][a-z0-9_]{0,19}$/.test(elementId)
|
|
424
|
+
|| typeof content !== 'string' || !content || Buffer.byteLength(content) > 100_000 || !LarkCli.sequence(sequence)) {
|
|
425
|
+
throw new Error('Invalid Lark card stream update.');
|
|
426
|
+
}
|
|
427
|
+
const result = await this.executeJson(['api', 'PUT', `/open-apis/cardkit/v1/cards/${cardId}/elements/${elementId}/content`,
|
|
428
|
+
'--data', '-', '--as', 'bot', '--format', 'json'], JSON.stringify({ content, sequence, uuid: randomUUID() }), 'card', Math.min(this.commandTimeoutMs, 10_000));
|
|
429
|
+
this.confirmFeedback(result);
|
|
430
|
+
}
|
|
431
|
+
/** Replace a card entity. Applying the same state twice is harmless, so callers may retry with a newer sequence. */
|
|
432
|
+
async updateCardEntity(cardId, card, sequence) {
|
|
433
|
+
if (!LarkCli.cardId(cardId) || !LarkCli.sequence(sequence))
|
|
434
|
+
throw new Error('Invalid Lark card update.');
|
|
435
|
+
const data = this.cardEntityData(card);
|
|
436
|
+
const result = await this.executeJson(['api', 'PUT', `/open-apis/cardkit/v1/cards/${cardId}`, '--data', '-', '--as', 'bot', '--format', 'json'], JSON.stringify({ card: { type: 'card_json', data }, sequence, uuid: randomUUID() }), 'card', Math.min(this.commandTimeoutMs, 10_000));
|
|
437
|
+
this.confirmFeedback(result);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Reply with one local file the caller has already validated. Only a
|
|
441
|
+
* `./basename` crosses argv, resolved from the file's own directory, so it
|
|
442
|
+
* can never be read as an upload key, a URL or another flag.
|
|
443
|
+
*/
|
|
444
|
+
async replyFile(messageId, path, options) {
|
|
445
|
+
const name = typeof path === 'string' ? basename(path) : '';
|
|
446
|
+
if (typeof messageId !== 'string' || !/^om_[A-Za-z0-9_-]{1,256}$/.test(messageId) || typeof path !== 'string' || !isAbsolute(path)
|
|
447
|
+
|| /[\\/]$/.test(path) || !name || name.length > 255 || /[\u0000-\u001f\u007f/\\]/.test(name) || name === '.' || name === '..'
|
|
448
|
+
|| typeof options?.idempotencyKey !== 'string' || !/^[A-Za-z0-9._:-]{1,50}$/.test(options.idempotencyKey)
|
|
449
|
+
|| typeof options.inThread !== 'boolean' || typeof options.image !== 'boolean')
|
|
450
|
+
throw new Error('Invalid Lark file reply.');
|
|
451
|
+
const args = ['im', '+messages-reply', '--message-id', messageId, `--${options.image ? 'image' : 'file'}=./${name}`,
|
|
452
|
+
'--as', 'bot', '--format', 'json', '--idempotency-key', options.idempotencyKey];
|
|
453
|
+
if (options.inThread)
|
|
454
|
+
args.push('--reply-in-thread');
|
|
455
|
+
// Uploads take longer than text; the deadline still bounds an unknown result.
|
|
456
|
+
await this.executeJson(args, '', 'reply', Math.max(this.commandTimeoutMs, 120_000), dirname(path));
|
|
457
|
+
}
|
|
458
|
+
cardEntityData(card) {
|
|
459
|
+
let data;
|
|
460
|
+
try {
|
|
461
|
+
if (!card || typeof card !== 'object' || Array.isArray(card) || card.schema !== '2.0')
|
|
462
|
+
throw new Error();
|
|
463
|
+
data = JSON.stringify(card);
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
throw new Error('Invalid Lark card entity.');
|
|
467
|
+
}
|
|
468
|
+
// CardKit rejects cards over 30 KB; keep a margin for the request envelope.
|
|
469
|
+
if (Buffer.byteLength(data) > 28_500)
|
|
470
|
+
throw new Error('Lark card entity exceeds the size limit.');
|
|
471
|
+
return data;
|
|
472
|
+
}
|
|
473
|
+
static cardId(value) {
|
|
474
|
+
return typeof value === 'string' && /^[0-9A-Za-z_-]{1,64}$/.test(value);
|
|
475
|
+
}
|
|
476
|
+
static sequence(value) {
|
|
477
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 1 && value <= 2_147_483_647;
|
|
478
|
+
}
|
|
391
479
|
confirmFeedback(result) {
|
|
392
480
|
if (result.identity !== 'bot' || (result.dry_run !== undefined && result.dry_run !== false)) {
|
|
393
481
|
throw new Error('Lark feedback command did not confirm bot delivery; delivery is unknown.');
|
|
@@ -509,15 +597,15 @@ export class LarkCli {
|
|
|
509
597
|
args.push('--reply-in-thread');
|
|
510
598
|
return this.executeJson(args, input, 'reply', this.commandTimeoutMs).then(() => undefined);
|
|
511
599
|
}
|
|
512
|
-
executeJson(args, input, operation, deadlineMs) {
|
|
600
|
+
executeJson(args, input, operation, deadlineMs, cwd) {
|
|
513
601
|
if (this.stopped)
|
|
514
602
|
return Promise.reject(new Error('Lark transport is stopped.'));
|
|
515
603
|
const label = `Lark ${operation} command`;
|
|
516
|
-
const uncertain = operation === 'progress' ? 'result is unknown.' : 'delivery is unknown.';
|
|
604
|
+
const uncertain = operation === 'progress' || operation === 'card' ? 'result is unknown.' : 'delivery is unknown.';
|
|
517
605
|
return new Promise((resolve, reject) => {
|
|
518
606
|
let child;
|
|
519
607
|
try {
|
|
520
|
-
child = this.launch(args);
|
|
608
|
+
child = this.launch(args, cwd);
|
|
521
609
|
}
|
|
522
610
|
catch {
|
|
523
611
|
reject(new Error(`Could not start the Lark ${operation} command.`));
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { TurnStreamEvent } from './dsh-runtime.js';
|
|
2
|
+
export interface LiveCardTransport {
|
|
3
|
+
createCardEntity(card: Record<string, unknown>): Promise<string>;
|
|
4
|
+
replyCardEntity(messageId: string, cardId: string, options: {
|
|
5
|
+
idempotencyKey: string;
|
|
6
|
+
inThread: boolean;
|
|
7
|
+
}): Promise<void>;
|
|
8
|
+
streamCardText(cardId: string, elementId: string, content: string, sequence: number): Promise<void>;
|
|
9
|
+
updateCardEntity(cardId: string, card: Record<string, unknown>, sequence: number): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
export interface LiveReplyOptions {
|
|
12
|
+
now?: () => number;
|
|
13
|
+
sleep?: (ms: number) => Promise<void>;
|
|
14
|
+
audit?: (stage: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface LiveReplyFinal {
|
|
17
|
+
phase: 'done' | 'stopped' | 'failed';
|
|
18
|
+
/** Authoritative answer text; replaces whatever was streamed. */
|
|
19
|
+
answer?: string;
|
|
20
|
+
reasoning?: string;
|
|
21
|
+
/** Trusted application text shown under the answer. */
|
|
22
|
+
note?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* One streaming reply card: the answer, with reasoning and tool steps in a
|
|
26
|
+
* panel that stays folded unless the reader opens it. Only the initial reply
|
|
27
|
+
* is a send; every later operation updates the same card entity and is safe
|
|
28
|
+
* to supersede or repeat. A reply never continues in another message.
|
|
29
|
+
*/
|
|
30
|
+
export declare class LiveReply {
|
|
31
|
+
private readonly transport;
|
|
32
|
+
private readonly target;
|
|
33
|
+
private readonly options;
|
|
34
|
+
private phase;
|
|
35
|
+
private readonly steps;
|
|
36
|
+
private trimmed;
|
|
37
|
+
private answer;
|
|
38
|
+
private separator;
|
|
39
|
+
private mark;
|
|
40
|
+
private tools;
|
|
41
|
+
private note?;
|
|
42
|
+
private cardId?;
|
|
43
|
+
private sequence;
|
|
44
|
+
private readonly sent;
|
|
45
|
+
private dirty;
|
|
46
|
+
private layoutDirty;
|
|
47
|
+
private degraded;
|
|
48
|
+
private streamFailures;
|
|
49
|
+
private closed;
|
|
50
|
+
private loop?;
|
|
51
|
+
private wake?;
|
|
52
|
+
private streamingSince;
|
|
53
|
+
private readonly startedAt;
|
|
54
|
+
private answeredAt?;
|
|
55
|
+
private finishedAt?;
|
|
56
|
+
constructor(transport: LiveCardTransport, target: {
|
|
57
|
+
messageId: string;
|
|
58
|
+
inThread: boolean;
|
|
59
|
+
idempotencyKey: string;
|
|
60
|
+
}, options?: LiveReplyOptions);
|
|
61
|
+
/** Send the card. False means nothing was sent, so the caller keeps its ordinary reply. */
|
|
62
|
+
open(): Promise<boolean>;
|
|
63
|
+
push(event: TurnStreamEvent): void;
|
|
64
|
+
/** Stop streaming and write the final card. True only when Feishu confirmed it. */
|
|
65
|
+
finish(final: LiveReplyFinal): Promise<boolean>;
|
|
66
|
+
private now;
|
|
67
|
+
private sleep;
|
|
68
|
+
private audit;
|
|
69
|
+
/** Keep only what the card can still show; the turn result stays authoritative. */
|
|
70
|
+
private trim;
|
|
71
|
+
private schedule;
|
|
72
|
+
private run;
|
|
73
|
+
/** Pacing between updates; finishing the reply ends it early. */
|
|
74
|
+
private pause;
|
|
75
|
+
private flush;
|
|
76
|
+
private markSent;
|
|
77
|
+
private stepsMarkdown;
|
|
78
|
+
private thinkContent;
|
|
79
|
+
private answerContent;
|
|
80
|
+
private panelTitle;
|
|
81
|
+
/** The folded process panel. Readers open it when they want the reasoning and tool steps. */
|
|
82
|
+
private panel;
|
|
83
|
+
/** Live layout when steps is undefined; a final layout otherwise ('' leaves out the panel). */
|
|
84
|
+
private card;
|
|
85
|
+
/** The answer comes first. The folded process only gets the space that is left. */
|
|
86
|
+
private finalCard;
|
|
87
|
+
}
|