mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-12 15:30:45 +08:00
fix(feishu): conform card form submission and model select to upstream
- read form data from action.form_value (Card 2.0) instead of formValue
- wrap custom steer input and submit in a form container with submit button
- reuse runModelCommand for dropdown model picks so IDs with multiple / keep working
- pass key when sending the help card so back-to-menu stays valid
- compute initial_index from {value} option arrays
- add in-flight tests for menu stop, steer dropdown, and steer form
This commit is contained in:
parent
7d2c93da86
commit
0241a32ec5
4 changed files with 297 additions and 59 deletions
|
|
@ -2,8 +2,10 @@ import assert from 'node:assert/strict';
|
|||
import test from 'node:test';
|
||||
import {
|
||||
cardActionProbeCard,
|
||||
customSteerCard,
|
||||
menuCard,
|
||||
menuHelpText,
|
||||
modelCard,
|
||||
} from '../../../src/channels/feishu/feishu-cards.mjs';
|
||||
|
||||
function buttons(value, result = []) {
|
||||
|
|
@ -17,6 +19,28 @@ function buttons(value, result = []) {
|
|||
return result;
|
||||
}
|
||||
|
||||
function selects(value, result = []) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) selects(item, result);
|
||||
return result;
|
||||
}
|
||||
if (!value || typeof value !== 'object') return result;
|
||||
if (value.tag === 'select_static') result.push(value);
|
||||
for (const child of Object.values(value)) selects(child, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function forms(value, result = []) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) forms(item, result);
|
||||
return result;
|
||||
}
|
||||
if (!value || typeof value !== 'object') return result;
|
||||
if (value.tag === 'form') result.push(value);
|
||||
for (const child of Object.values(value)) forms(child, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
test('menu exposes the increased command set and keeps repair number-only', () => {
|
||||
const card = JSON.parse(menuCard());
|
||||
assert.match(JSON.stringify(card), /6 · 修复卡片按钮/);
|
||||
|
|
@ -49,3 +73,57 @@ test('card-action probe carries only its action and opaque nonce', () => {
|
|||
}]);
|
||||
assert.throws(() => cardActionProbeCard('{{client_id}}'), /safe card-action probe nonce/);
|
||||
});
|
||||
|
||||
test('custom steer card wraps input and submit in a form container', () => {
|
||||
const card = JSON.parse(customSteerCard());
|
||||
const form = forms(card)[0];
|
||||
assert.ok(form, 'custom steer card must contain a form container');
|
||||
assert.equal(form.name, 'steer_form');
|
||||
const inputs = buttons(form).filter((element) => element.tag === 'input');
|
||||
// inputs are not buttons; scan form.elements directly
|
||||
const input = form.elements.find((element) => element.tag === 'input');
|
||||
assert.equal(input?.name, 'steer_text');
|
||||
const submit = form.elements.find((element) => element.tag === 'button');
|
||||
assert.equal(submit?.form_action_type, 'submit');
|
||||
assert.deepEqual(submit?.behaviors, [{
|
||||
type: 'callback',
|
||||
value: { action: 'steer', source: 'form' },
|
||||
}]);
|
||||
// 表单外的返回菜单按钮仍保留
|
||||
const back = buttons(card).find((b) => b.behaviors?.[0]?.value?.action === 'back_to_menu');
|
||||
assert.ok(back);
|
||||
});
|
||||
|
||||
test('menu session dropdown highlights the currently bound session via initial_index', () => {
|
||||
const sessions = [
|
||||
{ id: 'session-1', title: 'First' },
|
||||
{ id: 'session-2', title: 'Second' },
|
||||
{ id: 'session-3', title: 'Third' },
|
||||
];
|
||||
const card = JSON.parse(menuCard({
|
||||
currentSession: { id: 'session-2', title: 'Second' },
|
||||
sessions,
|
||||
}));
|
||||
const pick = selects(card).find((s) => s.name === 'session_pick');
|
||||
assert.ok(pick, 'menu must render a session dropdown');
|
||||
// initial_index is 1-based; the currently bound session sits at index 2.
|
||||
assert.equal(pick.initial_index, 2);
|
||||
});
|
||||
|
||||
test('model card dropdown highlights the current model via initial_index', () => {
|
||||
const catalog = {
|
||||
groups: [
|
||||
{ id: 'openrouter', name: 'OpenRouter', models: [
|
||||
{ id: 'anthropic/claude-sonnet-4', name: 'Claude Sonnet 4' },
|
||||
{ id: 'deepseek/deepseek-chat', name: 'DeepSeek Chat' },
|
||||
] },
|
||||
],
|
||||
current: { provider: 'openrouter', model: 'deepseek/deepseek-chat' },
|
||||
};
|
||||
const card = JSON.parse(modelCard(catalog));
|
||||
const pick = selects(card).find((s) => s.name === 'model_pick');
|
||||
assert.ok(pick, 'model card must render a dropdown');
|
||||
// deepseek/deepseek-chat is the second option (1-based index 2), even
|
||||
// though the id itself contains a `/`.
|
||||
assert.equal(pick.initial_index, 2);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue