mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-10 18:03:18 +08:00
feat(feishu): interactive approval and question cards with buttons
Render Harness approval and single-choice question interactions as interactive Feishu cards with approve/reject and answer buttons, matching the already-available interactionCards behaviour. Cards are now the default; set DSH_IM_INTERACTION_CARDS=0 (or interactionCards=false) to keep the plain-text reply flow. - bridge.mjs: approve:/reject:/answer: card actions, interactionCards option, gated approval render hook, interactive question presentation, and operationArguments/printableText helpers; refactor the inline question answer into #submitQuestionAnswer so both text replies and card buttons share one path. - feishu-cards.mjs: approvalCard (approve/reject) and questionCard (option buttons) templates. - feishu-runtime.mjs: default interactionCards from env (on). - harness-approval.mjs: optional channel render hook + submitByApprovalId. - i18n-en: add the new card t() keys. - Tests: pin the plain-text reply flow in state-machine suites that drive it, and add dedicated card tests for the default approve/reject, answer buttons, and the text fallback. Full suite shows no new failures versus upstream.
This commit is contained in:
parent
c2be2389b0
commit
0b42ea9889
8 changed files with 778 additions and 212 deletions
|
|
@ -245,6 +245,10 @@ export class HarnessApprovalQueue {
|
|||
await this.#rejectInteraction(interaction, payload);
|
||||
return true;
|
||||
}
|
||||
// Optional channel-provided renderer. When present, the approval is shown
|
||||
// as an interactive card (e.g. Feishu approve/reject buttons) instead of
|
||||
// plain text. Channels that don't provide one keep the text-reply path.
|
||||
const render = typeof context?.render === 'function' ? context.render : null;
|
||||
|
||||
const text = harnessApprovalText(payload, {
|
||||
toolCall: interaction.toolCall,
|
||||
|
|
@ -267,6 +271,7 @@ export class HarnessApprovalQueue {
|
|||
actor,
|
||||
requiresMention: context.requiresMention === true,
|
||||
send,
|
||||
render,
|
||||
text,
|
||||
presented: false,
|
||||
presentationTask: null,
|
||||
|
|
@ -287,6 +292,20 @@ export class HarnessApprovalQueue {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit an approval decision by id, as triggered by a channel card button
|
||||
* (e.g. Feishu approve/reject). Returns false when no matching pending
|
||||
* approval is found. Callers may pass the acting user to enforce that only
|
||||
* the originating actor can decide.
|
||||
*/
|
||||
async submitByApprovalId(approvalId, outcome, { actor } = {}) {
|
||||
const pending = this.#byId.get(cleanText(approvalId));
|
||||
if (!pending || pending.inactive || pending.resolving || pending.submitting) return false;
|
||||
if (actor !== undefined && pending.actor !== actor) return false;
|
||||
await this.#submit(pending, outcome);
|
||||
return true;
|
||||
}
|
||||
|
||||
async handleResolved(resolution) {
|
||||
if (resolution?.kind !== 'approval') return false;
|
||||
const pending = this.#byId.get(cleanText(resolution.interactionId));
|
||||
|
|
@ -353,7 +372,11 @@ export class HarnessApprovalQueue {
|
|||
if (this.#routes.get(pending.key)?.items[0] !== pending
|
||||
|| pending.inactive || pending.resolving || pending.presented) return;
|
||||
if (pending.presentationTask) return pending.presentationTask;
|
||||
const task = Promise.resolve().then(() => pending.send(pending.text));
|
||||
// A channel-provided renderer shows the approval as an interactive card
|
||||
// (e.g. approve/reject buttons); otherwise fall back to plain text.
|
||||
const task = pending.render
|
||||
? Promise.resolve().then(() => pending.render(pending, pending.send))
|
||||
: Promise.resolve().then(() => pending.send(pending.text));
|
||||
pending.presentationTask = task;
|
||||
try {
|
||||
await task;
|
||||
|
|
|
|||
|
|
@ -356,4 +356,15 @@ export default {
|
|||
'⚠️ Repair verification failed: the dedicated test card could not be sent, so card.action.trigger cannot be confirmed restored. Do not authorize again; check the bot message permission and connection status first.',
|
||||
'⚠️ 修复验证中断:Runtime 已停止,未完成 card.action.trigger 实测,不能确认修复成功。请不要重复授权;先等待机器人恢复连接。':
|
||||
'⚠️ Repair verification interrupted: the Runtime stopped before the card.action.trigger test completed, so the repair cannot be confirmed. Do not authorize again; wait for the bot to reconnect.',
|
||||
|
||||
// feishu/bridge.mjs — interaction cards (approve/reject / answer buttons)
|
||||
'该审批已处理或不存在,无需重复操作。':
|
||||
'This approval has already been processed or does not exist; no need to repeat the action.',
|
||||
// feishu/feishu-cards.mjs — approval card
|
||||
'操作参数:\n{operation}': 'Operation parameters:\n{operation}',
|
||||
'✅ 批准': '✅ Approve',
|
||||
'❌ 拒绝': '❌ Reject',
|
||||
'🔐 工具审批': '🔐 Tool approval',
|
||||
// feishu/feishu-cards.mjs — question card
|
||||
'❓ 请补充信息{progress}': '❓ Please provide more information{progress}',
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue