From 8c805c110fcec75c0a87ffe63ab78887398dd666 Mon Sep 17 00:00:00 2001 From: Fei Guo Date: Sun, 1 Mar 2026 09:39:07 +0100 Subject: [PATCH 1/5] feat:add discord as an additional connector --- package.json | 1 + src/connectors/discord/discord-plugin.ts | 152 +++++++++++++++++++++++ src/connectors/discord/index.ts | 1 + src/main.ts | 40 ++++++ 4 files changed, 194 insertions(+) create mode 100644 src/connectors/discord/discord-plugin.ts create mode 100644 src/connectors/discord/index.ts diff --git a/package.json b/package.json index d18f049..6c67412 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "ccxt": "^4.5.38", "chalk": "^5.6.2", "decimal.js": "^10.6.0", + "discord.js": "^14.25.1", "express": "^5.2.1", "file-type": "^21.3.0", "grammy": "^1.40.0", diff --git a/src/connectors/discord/discord-plugin.ts b/src/connectors/discord/discord-plugin.ts new file mode 100644 index 0000000..15091b4 --- /dev/null +++ b/src/connectors/discord/discord-plugin.ts @@ -0,0 +1,152 @@ +import { Client, GatewayIntentBits, TextChannel, AttachmentBuilder } from 'discord.js'; +import { readFile } from 'node:fs/promises'; +import type { Plugin, EngineContext } from '../../core/types.js'; +import type { Connector } from '../../core/connector-center.js'; +import { SessionStore } from '../../core/session.js'; + +export interface DiscordConfig { + enabled: boolean; + botToken?: string; + channelId?: string; +} + +export class DiscordPlugin implements Plugin { + name = 'discord'; + private config: DiscordConfig; + private client: Client; + private connectorCenter: EngineContext['connectorCenter'] | null = null; + private sessions = new Map(); + private unregisterConnector?: () => void; + + constructor(config: DiscordConfig) { + this.config = config; + this.client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildMembers, + ] + }); + } + + async start(engineCtx: EngineContext): Promise { + if (!this.config.enabled || !this.config.botToken) { + return; + } + + this.connectorCenter = engineCtx.connectorCenter; + + this.client.on('ready', () => { + console.log(`discord plugin: connected as @${this.client.user?.tag}`); + }); + + this.client.on('messageCreate', async (message) => { + if (message.author.bot) return; + if (message.channel.id !== this.config.channelId) return; + + const channel = await this.client.channels.fetch(this.config.channelId) as TextChannel; + if (!channel) return; + + const stopTyping = this.startTypingIndicator(channel); + + try { + const session = await this.getSession(message.author.id); + const result = await engineCtx.engine.askWithSession(message.content, session, { + historyPreamble: 'The following is the recent conversation from this Discord chat. Use it as context if the user references earlier messages.', + }); + stopTyping(); + + // Send media + if (result.media && result.media.length > 0) { + for (const attachment of result.media) { + try { + const buf = await readFile(attachment.path); + const discordAttachment = new AttachmentBuilder(buf, { name: 'image.png' }); + await channel.send({ files: [discordAttachment] }); + } catch (err) { + console.error('discord: failed to send photo:', err); + } + } + } + + if (result.text) { + await channel.send(result.text); + } + } catch (err) { + stopTyping(); + console.error('discord message handling error:', err); + await channel.send('Sorry, something went wrong processing your message.'); + } + }); + + if (this.config.channelId) { + this.unregisterConnector = this.connectorCenter!.register(this.createConnector(this.client, this.config.channelId)); + } + + await this.client.login(this.config.botToken); + } + + async stop(): Promise { + await this.client.destroy(); + this.unregisterConnector?.(); + } + + private createConnector(client: Client, channelId: string): Connector { + return { + channel: 'discord', + to: channelId, + capabilities: { push: true, media: true }, + send: async (payload) => { + const channel = await client.channels.fetch(channelId) as TextChannel; + if (!channel) { + console.error(`discord: channel not found: ${channelId}`); + return { delivered: false }; + } + + // Send media first + if (payload.media && payload.media.length > 0) { + for (const attachment of payload.media) { + try { + const buf = await readFile(attachment.path); + const discordAttachment = new AttachmentBuilder(buf, { name: 'image.png' }); + await channel.send({ files: [discordAttachment] }); + } catch (err) { + console.error('discord: failed to send photo:', err); + } + } + } + + // Send text + if (payload.text) { + // split message into chunks of 2000 characters + const chunks = payload.text.match(/[\s\S]{1,2000}/g) || []; + for (const chunk of chunks) { + await channel.send(chunk); + } + } + + return { delivered: true }; + }, + }; + } + + private startTypingIndicator(channel: TextChannel): () => void { + channel.sendTyping(); + const interval = setInterval(() => { + channel.sendTyping(); + }, 9000); // Discord's typing indicator lasts for 10 seconds + return () => clearInterval(interval); + } + + private async getSession(userId: string): Promise { + let session = this.sessions.get(userId); + if (!session) { + session = new SessionStore(`discord/${userId}`); + await session.restore(); + this.sessions.set(userId, session); + console.log(`discord: session discord/${userId} ready`); + } + return session; + } +} diff --git a/src/connectors/discord/index.ts b/src/connectors/discord/index.ts new file mode 100644 index 0000000..9194abe --- /dev/null +++ b/src/connectors/discord/index.ts @@ -0,0 +1 @@ +export { DiscordPlugin } from './discord-plugin.js'; diff --git a/src/main.ts b/src/main.ts index 69a63a3..dcb3db4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { McpPlugin } from './plugins/mcp.js' import { TelegramPlugin } from './connectors/telegram/index.js' import { WebPlugin } from './connectors/web/index.js' import { McpAskPlugin } from './connectors/mcp-ask/index.js' +import { DiscordPlugin } from './connectors/discord/index.js' import { createThinkingTools } from './extension/thinking-kit/index.js' import type { WalletExportState } from './extension/crypto-trading/index.js' import { @@ -414,6 +415,14 @@ async function main() { })) } + if (config.connectors.discord.enabled && config.connectors.discord.botToken) { + optionalPlugins.set('discord', new DiscordPlugin({ + enabled: true, + botToken: config.connectors.discord.botToken, + channelId: config.connectors.discord.channelId, + })) + } + // ==================== Connector Reconnect ==================== let connectorsReconnecting = false @@ -468,6 +477,37 @@ async function main() { } } + // --- Discord --- + const discordWanted = fresh.connectors.discord.enabled && !!fresh.connectors.discord.botToken + const discordRunning = optionalPlugins.has('discord') + if (discordRunning && !discordWanted) { + await optionalPlugins.get('discord')!.stop() + optionalPlugins.delete('discord') + changes.push('discord stopped') + } else if (!discordRunning && discordWanted) { + const p = new DiscordPlugin({ + enabled: true, + botToken: fresh.connectors.discord.botToken!, + channelId: fresh.connectors.discord.channelId, + }) + await p.start(ctx) + optionalPlugins.set('discord', p) + changes.push('discord started') + } + + if (changes.length > 0) { + console.log(`reconnect: connectors — ${changes.join(', ')}`) + } + return { success: true, message: changes.length > 0 ? changes.join(', ') : 'no changes' } +} catch (err) { + const msg = err instanceof Error ? err.message : String(err) + console.error('reconnect: connectors failed:', msg) + return { success: false, error: msg } +} finally { + connectorsReconnecting = false +} + } + const ctx: EngineContext = { config, connectorCenter, engine, cryptoEngine: null, eventLog, heartbeat, cronEngine, reconnectCrypto, reconnectSecurities, reconnectConnectors, From 55682a51c5fe8435fdee533b737a7d4f31f7ceda Mon Sep 17 00:00:00 2001 From: Fei Guo Date: Sun, 1 Mar 2026 10:08:36 +0100 Subject: [PATCH 2/5] feat(connectors): add discord connector UI and config Adds the necessary frontend components, configuration, and backend logic to support the Discord connector. - Defines the discord connector schema in the core configuration. - Implements the plugin loading and reconnect logic for the Discord connector in the main application entry point. - Adds 'discord.js' and its related dependencies to the project. - Updates the frontend API types to include the Discord connector configuration. - Adds the Discord option to the connector selection component in the UI. - Implements the UI section for displaying and configuring the Discord bot token and channel ID on the Connectors page. --- pnpm-lock.yaml | 159 +++++++++++++++++++++++++++++- src/core/config.ts | 5 + src/main.ts | 51 ++++------ ui/src/api/types.ts | 5 + ui/src/components/SDKSelector.tsx | 7 ++ ui/src/pages/ConnectorsPage.tsx | 37 +++++++ 6 files changed, 230 insertions(+), 34 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc431bb..f2a154e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ importers: decimal.js: specifier: ^10.6.0 version: 10.6.0 + discord.js: + specifier: ^14.25.1 + version: 14.25.1 express: specifier: ^5.2.1 version: 5.2.1 @@ -152,6 +155,34 @@ packages: '@borewit/text-codec@0.2.1': resolution: {integrity: sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==} + '@discordjs/builders@1.13.1': + resolution: {integrity: sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.6.2': + resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.6.0': + resolution: {integrity: sha512-RDYrhmpB7mTvmCKcpj+pc5k7POKszS4E2O9TYc+U+Y4iaCP+r910QdO43qmpOja8LRr1RJ0b3U+CqVsnPqzf4w==} + engines: {node: '>=18'} + + '@discordjs/util@1.2.0': + resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} + engines: {node: '>=18'} + + '@discordjs/ws@1.2.3': + resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} + engines: {node: '>=16.11.0'} + '@emnapi/runtime@1.8.1': resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} @@ -690,6 +721,18 @@ packages: cpu: [x64] os: [win32] + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.3': + resolution: {integrity: sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@sinclair/typebox@0.34.48': resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} @@ -781,6 +824,10 @@ packages: '@vitest/utils@4.0.18': resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + abort-controller@3.0.0: resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} engines: {node: '>=6.5'} @@ -977,6 +1024,13 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + discord-api-types@0.38.40: + resolution: {integrity: sha512-P/His8cotqZgQqrt+hzrocp9L8RhQQz1GkrCnC9TMJ8Uw2q0tg8YyqJyGULxhXn/8kxHETN4IppmOv+P2m82lQ==} + + discord.js@14.25.1: + resolution: {integrity: sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==} + engines: {node: '>=18'} + doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} @@ -1343,9 +1397,15 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + magic-bytes.js@1.13.0: + resolution: {integrity: sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -1769,6 +1829,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + ts-nkeys@1.0.16: resolution: {integrity: sha512-1qrhAlavbm36wtW+7NtKOgxpzl+70NTF8xlz9mEhiA5zHMlMxjj3sEVKWm3pGZhHXE0Q3ykjrj+OSRVaYw+Dqg==} @@ -1833,6 +1896,10 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + undici@6.21.3: + resolution: {integrity: sha512-gBLkYIlEnSp8pFbT64yFgGE6UIB9tAkhukC23PmMDCe5Nd+cRqKxSjw5y54MK2AZMgZfJWMaNE4nYUHgi1XEOw==} + engines: {node: '>=18.17'} + undici@7.22.0: resolution: {integrity: sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg==} engines: {node: '>=20.18.1'} @@ -2046,6 +2113,55 @@ snapshots: '@borewit/text-codec@0.2.1': {} + '@discordjs/builders@1.13.1': + dependencies: + '@discordjs/formatters': 0.6.2 + '@discordjs/util': 1.2.0 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.40 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.6.2': + dependencies: + discord-api-types: 0.38.40 + + '@discordjs/rest@2.6.0': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.3 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.40 + magic-bytes.js: 1.13.0 + tslib: 2.8.1 + undici: 6.21.3 + + '@discordjs/util@1.2.0': + dependencies: + discord-api-types: 0.38.40 + + '@discordjs/ws@1.2.3': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.6.0 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.40 + tslib: 2.8.1 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@emnapi/runtime@1.8.1': dependencies: tslib: 2.8.1 @@ -2400,6 +2516,15 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.57.1': optional: true + '@sapphire/async-queue@1.5.5': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.17.23 + + '@sapphire/snowflake@3.5.3': {} + '@sinclair/typebox@0.34.48': {} '@standard-schema/spec@1.1.0': {} @@ -2510,6 +2635,8 @@ snapshots: '@vitest/pretty-format': 4.0.18 tinyrainbow: 3.0.3 + '@vladfrangu/async_event_emitter@2.4.7': {} + abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 @@ -2691,6 +2818,27 @@ snapshots: detect-libc@2.1.2: {} + discord-api-types@0.38.40: {} + + discord.js@14.25.1: + dependencies: + '@discordjs/builders': 1.13.1 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.2 + '@discordjs/rest': 2.6.0 + '@discordjs/util': 1.2.0 + '@discordjs/ws': 1.2.3 + '@sapphire/snowflake': 3.5.3 + discord-api-types: 0.38.40 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.13.0 + tslib: 2.8.1 + undici: 6.21.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + doctrine@3.0.0: dependencies: esutils: 2.0.3 @@ -3102,8 +3250,12 @@ snapshots: lodash.merge@4.6.2: {} + lodash.snakecase@4.1.1: {} + lodash@4.17.23: {} + magic-bytes.js@1.13.0: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -3547,12 +3699,13 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-mixer@6.0.4: {} + ts-nkeys@1.0.16: dependencies: tweetnacl: 1.0.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tslog@4.10.2: {} @@ -3613,6 +3766,8 @@ snapshots: undici-types@7.16.0: {} + undici@6.21.3: {} + undici@7.22.0: {} unpipe@1.0.0: {} diff --git a/src/core/config.ts b/src/core/config.ts index 0e8717e..b236863 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -158,6 +158,11 @@ const connectorsSchema = z.object({ botUsername: z.string().optional(), chatIds: z.array(z.number()).default([]), }).default({ enabled: false, chatIds: [] }), + discord: z.object({ + enabled: z.boolean().default(false), + botToken: z.string().optional(), + channelId: z.string().optional(), + }).default({ enabled: false }), }) const heartbeatSchema = z.object({ diff --git a/src/main.ts b/src/main.ts index dcb3db4..5622855 100644 --- a/src/main.ts +++ b/src/main.ts @@ -464,6 +464,24 @@ async function main() { changes.push('telegram started') } + // --- Discord --- + const discordWanted = fresh.connectors.discord.enabled && !!fresh.connectors.discord.botToken + const discordRunning = optionalPlugins.has('discord') + if (discordRunning && !discordWanted) { + await optionalPlugins.get('discord')!.stop() + optionalPlugins.delete('discord') + changes.push('discord stopped') + } else if (!discordRunning && discordWanted) { + const p = new DiscordPlugin({ + enabled: true, + botToken: fresh.connectors.discord.botToken!, + channelId: fresh.connectors.discord.channelId, + }) + await p.start(ctx) + optionalPlugins.set('discord', p) + changes.push('discord started') + } + if (changes.length > 0) { console.log(`reconnect: connectors — ${changes.join(', ')}`) } @@ -474,38 +492,7 @@ async function main() { return { success: false, error: msg } } finally { connectorsReconnecting = false - } - } - - // --- Discord --- - const discordWanted = fresh.connectors.discord.enabled && !!fresh.connectors.discord.botToken - const discordRunning = optionalPlugins.has('discord') - if (discordRunning && !discordWanted) { - await optionalPlugins.get('discord')!.stop() - optionalPlugins.delete('discord') - changes.push('discord stopped') - } else if (!discordRunning && discordWanted) { - const p = new DiscordPlugin({ - enabled: true, - botToken: fresh.connectors.discord.botToken!, - channelId: fresh.connectors.discord.channelId, - }) - await p.start(ctx) - optionalPlugins.set('discord', p) - changes.push('discord started') - } - - if (changes.length > 0) { - console.log(`reconnect: connectors — ${changes.join(', ')}`) - } - return { success: true, message: changes.length > 0 ? changes.join(', ') : 'no changes' } -} catch (err) { - const msg = err instanceof Error ? err.message : String(err) - console.error('reconnect: connectors failed:', msg) - return { success: false, error: msg } -} finally { - connectorsReconnecting = false -} + } } const ctx: EngineContext = { diff --git a/ui/src/api/types.ts b/ui/src/api/types.ts index 162d750..84380d8 100644 --- a/ui/src/api/types.ts +++ b/ui/src/api/types.ts @@ -51,6 +51,11 @@ export interface ConnectorsConfig { botUsername?: string chatIds: number[] } + discord: { + enabled: boolean + botToken?: string + channelId?: string + } } // ==================== News Collector ==================== diff --git a/ui/src/components/SDKSelector.tsx b/ui/src/components/SDKSelector.tsx index 0475ba7..e8a8bec 100644 --- a/ui/src/components/SDKSelector.tsx +++ b/ui/src/components/SDKSelector.tsx @@ -237,4 +237,11 @@ export const CONNECTOR_OPTIONS: SDKOption[] = [ badge: 'TG', badgeColor: 'text-cyan', }, + { + id: 'discord', + name: 'Discord', + description: 'Two-way chat via Discord bot in a specified channel.', + badge: 'DC', + badgeColor: 'text-indigo', + }, ] diff --git a/ui/src/pages/ConnectorsPage.tsx b/ui/src/pages/ConnectorsPage.tsx index 80a159f..e88c1d4 100644 --- a/ui/src/pages/ConnectorsPage.tsx +++ b/ui/src/pages/ConnectorsPage.tsx @@ -18,6 +18,7 @@ export function ConnectorsPage() { 'mcp', ...(config.mcpAsk.enabled ? ['mcpAsk'] : []), ...(config.telegram.enabled ? ['telegram'] : []), + ...(config.discord.enabled ? ['discord'] : []), ] : ['web', 'mcp'] @@ -27,6 +28,8 @@ export function ConnectorsPage() { updateConfigImmediate({ mcpAsk: { ...config.mcpAsk, enabled: !config.mcpAsk.enabled } }) } else if (id === 'telegram') { updateConfigImmediate({ telegram: { ...config.telegram, enabled: !config.telegram.enabled } }) + } else if (id === 'discord') { + updateConfigImmediate({ discord: { ...config.discord, enabled: !config.discord.enabled } }) } } @@ -165,6 +168,40 @@ export function ConnectorsPage() { )} + + {/* Discord config */} + {config.discord.enabled && ( +
+ + + updateConfig({ + discord: { ...config.discord, botToken: e.target.value || undefined }, + }) + } + placeholder="Super-secret-token" + /> + + + + updateConfig({ + discord: { ...config.discord, channelId: e.target.value || undefined }, + }) + } + placeholder="123456789012345678" + /> + +
+ )} )} {loadError &&

Failed to load configuration.

} From e21f53b0fae81aaff9264bcb0559c1acdb74b7a9 Mon Sep 17 00:00:00 2001 From: Fei Guo Date: Sun, 1 Mar 2026 10:22:22 +0100 Subject: [PATCH 3/5] fix(config): correct API key save logic on provider page Fixes a race condition where the auto-save functionality for model/provider settings could overwrite newly saved API keys. The 'Save Keys' handler now uses the most up-to-date form state ('modelData') when building the configuration object, ensuring that manual key updates are not lost. --- ui/src/pages/AIProviderPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/pages/AIProviderPage.tsx b/ui/src/pages/AIProviderPage.tsx index 216803f..fb8bb46 100644 --- a/ui/src/pages/AIProviderPage.tsx +++ b/ui/src/pages/AIProviderPage.tsx @@ -200,7 +200,7 @@ function ModelForm({ aiProvider }: { aiProvider: AIProviderConfig }) { if (keys.anthropic) updatedKeys.anthropic = keys.anthropic if (keys.openai) updatedKeys.openai = keys.openai if (keys.google) updatedKeys.google = keys.google - await api.config.updateSection('aiProvider', { ...aiProvider, apiKeys: updatedKeys }) + await api.config.updateSection('aiProvider', { ...modelData, apiKeys: updatedKeys }) setLiveKeyStatus({ anthropic: !!updatedKeys.anthropic, openai: !!updatedKeys.openai, From 56969f70aa9e58d9a855dbc34ff53df207fb21b0 Mon Sep 17 00:00:00 2001 From: Fei Guo Date: Sun, 1 Mar 2026 10:54:02 +0100 Subject: [PATCH 4/5] fix(discord): handle messages exceeding 2000 characters Adds message chunking to the 'messageCreate' handler in the Discord plugin. This prevents crashes when the AI generates a response longer than Discord's 2000-character limit by splitting the message into multiple, smaller messages. --- src/connectors/discord/discord-plugin.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/connectors/discord/discord-plugin.ts b/src/connectors/discord/discord-plugin.ts index 15091b4..455ed76 100644 --- a/src/connectors/discord/discord-plugin.ts +++ b/src/connectors/discord/discord-plugin.ts @@ -71,7 +71,11 @@ export class DiscordPlugin implements Plugin { } if (result.text) { - await channel.send(result.text); + // split message into chunks of 2000 characters + const chunks = result.text.match(/[\s\S]{1,2000}/g) || []; + for (const chunk of chunks) { + await channel.send(chunk); + } } } catch (err) { stopTyping(); From cb812187d1b1f7ba65c70053952fe9c71e0b8b19 Mon Sep 17 00:00:00 2001 From: Fei Guo Date: Sun, 1 Mar 2026 13:37:38 +0100 Subject: [PATCH 5/5] feat(tools): add readFile tool for local file access Adds a new 'readFile' tool to the thinking-kit. This provides the AI with a secure, dedicated tool to read the contents of local files, addressing a previous limitation where it had to rely on other, less suitable tools. --- src/extension/thinking-kit/adapter.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/extension/thinking-kit/adapter.ts b/src/extension/thinking-kit/adapter.ts index b921ce7..139cf09 100644 --- a/src/extension/thinking-kit/adapter.ts +++ b/src/extension/thinking-kit/adapter.ts @@ -1,6 +1,8 @@ import { tool } from 'ai'; import { z } from 'zod'; import { calculate } from './tools/calculate.tool'; +import { readFile as fsReadFile } from 'fs/promises'; +import { resolve } from 'path'; /** * Create thinking AI tools (cognition + utility, no data dependency) @@ -9,6 +11,7 @@ import { calculate } from './tools/calculate.tool'; * - think: Record observations and analysis * - plan: Record action plans * - calculate: Safe mathematical expression evaluation + * - readFile: Read the content of a local file * - reportWarning: Report anomalies or unexpected situations * - getConfirm: Request user confirmation before actions */ @@ -96,6 +99,30 @@ This commits you to a specific action plan before execution. }, }), + readFile: tool({ + description: 'Reads the entire content of a file at the given local path and returns it as a string. Use this to inspect local files like configuration or scripts.', + inputSchema: z.object({ + path: z.string().describe('The relative or absolute path to the file.'), + }), + execute: async ({ path }) => { + try { + // Resolve path to be relative to the project root for security + const safePath = resolve(process.cwd(), path); + // Basic jailbreak prevention + if (!safePath.startsWith(process.cwd())) { + return { error: 'Access denied. Path is outside the project directory.' }; + } + const content = await fsReadFile(safePath, 'utf-8'); + return { content }; + } catch (error: any) { + if (error.code === 'ENOENT') { + return { error: `File not found at ${path}` }; + } + return { error: `Failed to read file: ${error.message}` }; + } + }, + }), + reportWarning: tool({ description: 'Report a warning when you detect anomalies or unexpected situations in the sandbox. Use this to alert about suspicious data, unexpected PnL, zero prices, or any other concerning conditions.',