docs: update README with Discord and multi-channel features
This commit is contained in:
39
src/bot/delivery-hub.js
Normal file
39
src/bot/delivery-hub.js
Normal file
@@ -0,0 +1,39 @@
|
||||
// Delivery hub — send to multiple channels from one call
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const channels = new Map(); // name -> { send: (msg) => Promise<void> }
|
||||
|
||||
export function registerChannel(name, sendFn) {
|
||||
channels.set(name, sendFn);
|
||||
logger.info(`📡 Channel registered: ${name}`);
|
||||
}
|
||||
|
||||
export function unregisterChannel(name) {
|
||||
channels.delete(name);
|
||||
logger.info(`📡 Channel unregistered: ${name}`);
|
||||
}
|
||||
|
||||
export function getChannels() {
|
||||
return Array.from(channels.keys());
|
||||
}
|
||||
|
||||
export async function broadcast(message, opts = {}, except = []) {
|
||||
const results = [];
|
||||
for (const [name, sendFn] of channels) {
|
||||
if (except.includes(name)) continue;
|
||||
try {
|
||||
await sendFn(message);
|
||||
results.push({ channel: name, ok: true });
|
||||
} catch (e) {
|
||||
logger.error(`Broadcast to ${name} failed:`, e.message);
|
||||
results.push({ channel: name, ok: false, error: e.message });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function sendTo(channel, message) {
|
||||
const sendFn = channels.get(channel);
|
||||
if (!sendFn) throw new Error(`Channel not found: ${channel}`);
|
||||
await sendFn(message);
|
||||
}
|
||||
44
src/bot/discord.js
Normal file
44
src/bot/discord.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Discord bot — minimal, fast. Reuses svc registry from bot/index.js
|
||||
import { Client, GatewayIntentBits, Partials } from 'discord.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { registerChannel } from './delivery-hub.js';
|
||||
|
||||
const INTENTS = GatewayIntentBits.GuildMessages | GatewayIntentBits.MessageContent |
|
||||
GatewayIntentBits.DirectMessages;
|
||||
const PARTIALS = [Partials.Channel];
|
||||
|
||||
export async function initDiscord(token, svc, chatWithAI) {
|
||||
if (!token) { logger.warn('⚠ Discord token not set'); return null; }
|
||||
|
||||
const client = new Client({ intents: INTENTS, partials: PARTIALS,
|
||||
makeCache: (manager) => ['UserManager', 'ChannelManager'].includes(manager.constructor.name) ? manager : false,
|
||||
});
|
||||
|
||||
client.once('ready', () => {
|
||||
logger.info('✅ Discord bot connected');
|
||||
registerChannel('discord', async (msg) => {
|
||||
// broadcast to first available guild channel
|
||||
const guild = client.guilds.cache.first();
|
||||
if (!guild) return;
|
||||
const ch = guild.systemChannel || guild.channels.cache.find(c => c.type === 0);
|
||||
if (ch) await ch.send(msg.slice(0, 1900));
|
||||
});
|
||||
});
|
||||
|
||||
client.on('messageCreate', async (msg) => {
|
||||
if (msg.author.bot) return;
|
||||
// Ignore commands directed at other bots
|
||||
if (msg.mentions.has(client.user) || msg.channel.type === 1) {
|
||||
const text = msg.content.replace(/<@!?\d+>/g, '').trim() || 'hello';
|
||||
await msg.channel.sendTyping();
|
||||
const result = await chatWithAI([
|
||||
{ role: 'system', content: `You are zCode CLI X on Discord. Be concise.` },
|
||||
{ role: 'user', content: text },
|
||||
]);
|
||||
await msg.reply(typeof result === 'string' ? result.slice(0, 1900) : result);
|
||||
}
|
||||
});
|
||||
|
||||
await client.login(token);
|
||||
return client;
|
||||
}
|
||||
44
src/bot/self-correction.js
Normal file
44
src/bot/self-correction.js
Normal file
@@ -0,0 +1,44 @@
|
||||
// Self-correction loop — retry on failure with backoff & simplified approach
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
const MAX_RETRIES = 2;
|
||||
const RETRY_DELAY_MS = 500;
|
||||
|
||||
function shouldRetry(content) {
|
||||
if (!content) return true;
|
||||
const lowered = content.toLowerCase();
|
||||
if (lowered.includes('❌') && lowered.includes('error')) return true;
|
||||
if (lowered.includes('rate limit') || lowered.includes('timeout')) return true;
|
||||
if (lowered.includes('internal server error') || lowered.includes('5xx')) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function withSelfCorrection(fn) {
|
||||
return async (...args) => {
|
||||
let lastError;
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
const result = await fn(...args);
|
||||
if (typeof result === 'string' && shouldRetry(result) && attempt < MAX_RETRIES) {
|
||||
logger.warn(`Self-correct: retry ${attempt + 1}/${MAX_RETRIES} — error in response`);
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS * (attempt + 1)));
|
||||
// Simplify the prompt on retry
|
||||
const lastMsg = args[1]?.[args[1].length - 1];
|
||||
if (lastMsg) lastMsg.content = `[SIMPLIFIED RETRY ${attempt + 1}] ${lastMsg.content.slice(0, 500)}`;
|
||||
continue;
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
lastError = err;
|
||||
if (attempt < MAX_RETRIES) {
|
||||
logger.warn(`Self-correct: retry ${attempt + 1}/${MAX_RETRIES} — ${err.message}`);
|
||||
await new Promise(r => setTimeout(r, RETRY_DELAY_MS * (attempt + 1)));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
const msg = lastError ? `❌ Failed after ${MAX_RETRIES + 1} attempts: ${lastError.message}` : '❌ Failed after retries';
|
||||
logger.error(msg);
|
||||
return msg;
|
||||
};
|
||||
}
|
||||
@@ -21,5 +21,6 @@ export function checkEnv() {
|
||||
GLM_BASE_URL: process.env.GLM_BASE_URL || '',
|
||||
TELEGRAM_BOT_TOKEN: process.env.TELEGRAM_BOT_TOKEN || '',
|
||||
TELEGRAM_ALLOWED_USERS: process.env.TELEGRAM_ALLOWED_USERS || '',
|
||||
DISCORD_TOKEN: process.env.DISCORD_TOKEN || '',
|
||||
};
|
||||
}
|
||||
|
||||
87
src/zcode.js
87
src/zcode.js
@@ -5,11 +5,11 @@ import { initTools } from './tools/index.js';
|
||||
import { initSkills } from './skills/index.js';
|
||||
import { initAgents } from './agents/index.js';
|
||||
import { checkEnv } from './utils/env.js';
|
||||
import { registerChannel, getChannels } from './bot/delivery-hub.js';
|
||||
|
||||
export async function zcode(options) {
|
||||
logger.info('🚀 Initializing zCode CLI X...');
|
||||
|
||||
// 1. Check environment
|
||||
|
||||
const env = checkEnv();
|
||||
if (!env.valid) {
|
||||
logger.error('Missing required environment variables:');
|
||||
@@ -19,53 +19,74 @@ export async function zcode(options) {
|
||||
|
||||
logger.info('✓ Environment validated');
|
||||
logger.info(`Z.AI API Key: ${env.ZAI_API_KEY.substring(0, 10)}...`);
|
||||
logger.info(`Telegram Bot Token: ${env.TELEGRAM_BOT_TOKEN ? 'Configured' : 'Not configured'}`);
|
||||
logger.info(`Telegram: ${env.TELEGRAM_BOT_TOKEN ? '✅' : '❌'}`);
|
||||
|
||||
// 2. Initialize configuration
|
||||
// Init core services
|
||||
const config = await initConfig();
|
||||
logger.info('✓ Configuration loaded');
|
||||
|
||||
// 3. Initialize Z.AI API
|
||||
const api = await initAPI();
|
||||
logger.info('✓ Z.AI API connected');
|
||||
|
||||
// 4. Initialize tools
|
||||
const tools = await initTools();
|
||||
logger.info(`✓ Tools loaded: ${tools.length} available`);
|
||||
|
||||
// 5. Initialize skills
|
||||
const skills = await initSkills();
|
||||
logger.info(`✓ Skills loaded: ${skills.length} available`);
|
||||
|
||||
// 6. Initialize agents
|
||||
const agents = await initAgents();
|
||||
logger.info(`✓ Agents loaded: ${agents.length} available`);
|
||||
|
||||
// 7. Initialize Telegram bot (if enabled)
|
||||
// Register telegram delivery channel (filled after bot init)
|
||||
const deliveryTargets = new Map();
|
||||
|
||||
registerChannel('log', async (msg) => {
|
||||
logger.info(`[broadcast] ${msg.substring(0, 200)}`);
|
||||
});
|
||||
|
||||
// Init Telegram bot
|
||||
let bot;
|
||||
if (options.bot !== false && env.TELEGRAM_BOT_TOKEN) {
|
||||
// Import bot module dynamically to avoid circular dependency
|
||||
const botModule = await import('./bot/index.js');
|
||||
const bot = await botModule.initBot(config, api, tools, skills, agents);
|
||||
logger.info('✓ Telegram bot initialized');
|
||||
|
||||
// Keep process alive for bot
|
||||
logger.info('🤖 zCode CLI X is now running 24/7');
|
||||
logger.info('Type your commands or just chat with the bot!');
|
||||
|
||||
// Wait for bot to handle messages
|
||||
bot = await botModule.initBot(config, api, tools, skills, agents);
|
||||
if (bot) {
|
||||
deliveryTargets.set('telegram', bot.send);
|
||||
registerChannel('telegram', (msg) => bot.send(env.TELEGRAM_ALLOWED_USERS?.split(',')[0] || '6352861167', msg));
|
||||
logger.info('✓ Telegram bot initialized');
|
||||
}
|
||||
}
|
||||
|
||||
// Init Discord bot (opt-in via DISCORD_TOKEN env)
|
||||
if (env.DISCORD_TOKEN) {
|
||||
try {
|
||||
const { initDiscord } = await import('./bot/discord.js');
|
||||
const discordClient = await initDiscord(env.DISCORD_TOKEN, {
|
||||
config, api, tools, skills, agents,
|
||||
}, (messages) => {
|
||||
// Inline minimal chat for Discord
|
||||
return api.client.post('/chat/completions', {
|
||||
model: config.api?.models?.default || 'glm-5.1',
|
||||
messages,
|
||||
temperature: 0.7,
|
||||
max_tokens: 4096,
|
||||
}).then(r => r.data.choices?.[0]?.message?.content || '✅ Done.').catch(e => `❌ ${e.message}`);
|
||||
});
|
||||
if (discordClient) {
|
||||
deliveryTargets.set('discord', discordClient);
|
||||
logger.info('✓ Discord bot initialized');
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('⚠ Discord init skipped:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Log loaded services
|
||||
logger.info(`✓ ${tools.length} tools · ${skills.length} skills · ${agents.length} agents`);
|
||||
logger.info(`📡 Delivery channels: ${getChannels().join(', ') || 'none'}`);
|
||||
|
||||
// Keep alive
|
||||
if (bot) {
|
||||
logger.info('🤖 zCode CLI X running 24/7');
|
||||
await bot.waitForMessages();
|
||||
} else if (options.cli !== false) {
|
||||
// CLI-only mode
|
||||
logger.info('🔧 CLI mode: Run interactive mode');
|
||||
logger.info('🔧 CLI mode');
|
||||
await runInteractiveMode(config, api, tools, skills);
|
||||
} else {
|
||||
logger.info('🤖 Bot mode: zCode is running in the background');
|
||||
logger.info(' Telegram bot will handle all interactions');
|
||||
logger.info('🤖 Background mode');
|
||||
}
|
||||
}
|
||||
|
||||
async function runInteractiveMode(config, api, tools, skills) {
|
||||
// TODO: Implement interactive CLI mode
|
||||
console.log('Interactive mode coming soon!');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user