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 c7b7ebfd..f67827d6 100644 --- a/src/messaging/outbound/actions.ts +++ b/src/messaging/outbound/actions.ts @@ -18,9 +18,10 @@ 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 { Type } from '@sinclair/typebox'; import { jsonResult, readReactionParams } from '../../core/sdk-compat'; import { LarkClient } from '../../core/lark-client'; @@ -32,6 +33,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.Optional(Type.String({ description: FEISHU_SEND_TEXT_DESCRIPTION })), + text: Type.Optional(Type.String({ description: FEISHU_SEND_TEXT_DESCRIPTION })), + }, + visibility: 'current-channel' as const, +} satisfies ChannelMessageToolSchemaContribution; + // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- @@ -170,7 +182,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..6e515880 --- /dev/null +++ b/tests/message-actions-schema.test.ts @@ -0,0 +1,59 @@ +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 { + 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'), + }, + }, + }); + }); + + 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'); + }); +});