From 34e755f0214ed3d119f131b3c02a066d46bf503d Mon Sep 17 00:00:00 2001 From: zhulijin1991 <167979819+zhulijin1991@users.noreply.github.com> Date: Wed, 13 May 2026 12:55:07 +0800 Subject: [PATCH 1/2] Guide Feishu sends during card replies --- src/messaging/outbound/actions.ts | 15 +++++++++-- tests/message-actions-schema.test.ts | 38 ++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 tests/message-actions-schema.test.ts diff --git a/src/messaging/outbound/actions.ts b/src/messaging/outbound/actions.ts index c7b7ebfd..6c973fbf 100644 --- a/src/messaging/outbound/actions.ts +++ b/src/messaging/outbound/actions.ts @@ -18,7 +18,7 @@ import type { ChannelMessageActionName, OpenClawConfig, } from 'openclaw/plugin-sdk'; -import type { ChannelThreadingToolContext } from 'openclaw/plugin-sdk/channel-contract'; +import type { ChannelMessageToolSchemaContribution, ChannelThreadingToolContext } from 'openclaw/plugin-sdk/channel-contract'; import { extractToolSend } from 'openclaw/plugin-sdk/tool-send'; import { readStringParam } from 'openclaw/plugin-sdk/param-readers'; import { jsonResult, readReactionParams } from '../../core/sdk-compat'; @@ -32,6 +32,17 @@ import { uploadAndSendMediaLark } from './media'; const log = larkLogger('outbound/actions'); +const FEISHU_SEND_TEXT_DESCRIPTION = + 'Text to send as a separate Feishu message. During a normal Feishu streaming-card reply, do not call send just to repeat or finalize the same answer; return the final answer normally so the active card can be completed by the reply dispatcher. Use send only when the user explicitly needs an additional separate message.'; + +const FEISHU_MESSAGE_TOOL_SCHEMA = { + properties: { + message: { type: 'string', description: FEISHU_SEND_TEXT_DESCRIPTION }, + text: { type: 'string', description: FEISHU_SEND_TEXT_DESCRIPTION }, + }, + visibility: 'current-channel' as const, +} as unknown as ChannelMessageToolSchemaContribution; + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -170,7 +181,7 @@ export const feishuMessageActions: ChannelMessageActionAdapter = { return { actions: Array.from(SUPPORTED_ACTIONS), capabilities: ['cards'], - schema: null, + schema: FEISHU_MESSAGE_TOOL_SCHEMA, }; }, diff --git a/tests/message-actions-schema.test.ts b/tests/message-actions-schema.test.ts new file mode 100644 index 00000000..bb9337cb --- /dev/null +++ b/tests/message-actions-schema.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from 'vitest'; +import type { ClawdbotConfig } from 'openclaw/plugin-sdk'; +import { feishuMessageActions } from '../src/messaging/outbound/actions'; + +function configuredFeishuConfig(): ClawdbotConfig { + return { + channels: { + feishu: { + appId: 'cli_a', + appSecret: 'secret', + }, + }, + } as unknown as ClawdbotConfig; +} + +describe('Feishu message action discovery', () => { + it('guides send text away from duplicate streaming-card final replies', () => { + const discovery = feishuMessageActions.describeMessageTool({ + cfg: configuredFeishuConfig(), + currentChannelProvider: 'feishu', + }); + + expect(discovery?.actions).toContain('send'); + expect(discovery?.schema).toMatchObject({ + visibility: 'current-channel', + properties: { + message: { + type: 'string', + description: expect.stringContaining('do not call send'), + }, + text: { + type: 'string', + description: expect.stringContaining('active card'), + }, + }, + }); + }); +}); From e81fb6243c2f056cf64d6b7573f04bb1eab27c34 Mon Sep 17 00:00:00 2001 From: zhulijin1991 <167979819+zhulijin1991@users.noreply.github.com> Date: Fri, 15 May 2026 22:18:15 +0800 Subject: [PATCH 2/2] Keep Feishu send text schema optional --- package.json | 2 +- pnpm-lock.yaml | 11 +++-------- src/messaging/outbound/actions.ts | 7 ++++--- tests/message-actions-schema.test.ts | 21 +++++++++++++++++++++ 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 940f39db..c821deaa 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ }, "dependencies": { "@larksuiteoapi/node-sdk": "^1.60.0", - "@sinclair/typebox": "0.34.48", + "@sinclair/typebox": "0.34.49", "image-size": "^2.0.2", "undici-types": "^8.1.0", "zod": "^4.3.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc7bcb6e..6e97149d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^1.60.0 version: 1.60.0 '@sinclair/typebox': - specifier: 0.34.48 - version: 0.34.48 + specifier: 0.34.49 + version: 0.34.49 image-size: specifier: ^2.0.2 version: 2.0.2 @@ -1302,9 +1302,6 @@ packages: '@silvia-odwyer/photon-node@0.3.4': resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} - '@sinclair/typebox@0.34.48': - resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} - '@sinclair/typebox@0.34.49': resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} @@ -5824,7 +5821,7 @@ snapshots: '@aws-sdk/client-bedrock-runtime': 3.1024.0 '@google/genai': 1.49.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6)) '@mistralai/mistralai': 1.14.1 - '@sinclair/typebox': 0.34.48 + '@sinclair/typebox': 0.34.49 ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) chalk: 5.6.2 @@ -6145,8 +6142,6 @@ snapshots: '@silvia-odwyer/photon-node@0.3.4': {} - '@sinclair/typebox@0.34.48': {} - '@sinclair/typebox@0.34.49': {} '@sindresorhus/merge-streams@4.0.0': {} diff --git a/src/messaging/outbound/actions.ts b/src/messaging/outbound/actions.ts index 6c973fbf..f67827d6 100644 --- a/src/messaging/outbound/actions.ts +++ b/src/messaging/outbound/actions.ts @@ -21,6 +21,7 @@ import type { import type { ChannelMessageToolSchemaContribution, ChannelThreadingToolContext } from 'openclaw/plugin-sdk/channel-contract'; import { extractToolSend } from 'openclaw/plugin-sdk/tool-send'; import { readStringParam } from 'openclaw/plugin-sdk/param-readers'; +import { Type } from '@sinclair/typebox'; import { jsonResult, readReactionParams } from '../../core/sdk-compat'; import { LarkClient } from '../../core/lark-client'; @@ -37,11 +38,11 @@ const FEISHU_SEND_TEXT_DESCRIPTION = const FEISHU_MESSAGE_TOOL_SCHEMA = { properties: { - message: { type: 'string', description: FEISHU_SEND_TEXT_DESCRIPTION }, - text: { type: 'string', description: FEISHU_SEND_TEXT_DESCRIPTION }, + message: Type.Optional(Type.String({ description: FEISHU_SEND_TEXT_DESCRIPTION })), + text: Type.Optional(Type.String({ description: FEISHU_SEND_TEXT_DESCRIPTION })), }, visibility: 'current-channel' as const, -} as unknown as ChannelMessageToolSchemaContribution; +} satisfies ChannelMessageToolSchemaContribution; // --------------------------------------------------------------------------- // Helpers diff --git a/tests/message-actions-schema.test.ts b/tests/message-actions-schema.test.ts index bb9337cb..6e515880 100644 --- a/tests/message-actions-schema.test.ts +++ b/tests/message-actions-schema.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { ClawdbotConfig } from 'openclaw/plugin-sdk'; +import { Type } from '@sinclair/typebox'; import { feishuMessageActions } from '../src/messaging/outbound/actions'; function configuredFeishuConfig(): ClawdbotConfig { @@ -35,4 +36,24 @@ describe('Feishu message action discovery', () => { }, }); }); + + it('keeps send text fields optional in the composed TypeBox schema', () => { + const discovery = feishuMessageActions.describeMessageTool({ + cfg: configuredFeishuConfig(), + currentChannelProvider: 'feishu', + }); + const schema = discovery?.schema; + if (!schema || Array.isArray(schema)) { + throw new Error('expected a single Feishu message tool schema contribution'); + } + + const composedSchema = Type.Object({ + action: Type.String(), + ...schema.properties, + }); + + expect(composedSchema.required).toContain('action'); + expect(composedSchema.required ?? []).not.toContain('message'); + expect(composedSchema.required ?? []).not.toContain('text'); + }); });