v2: make v2 the main entry point, move v1 to src/v1/
- Move all v1 files (index, router, container-runner, db, ipc, types, logger, channels/registry, and all utilities) to src/v1/ as a fully self-contained archive with no shared dependencies - Rename v2 files to remove -v2 suffix (index-v2.ts → index.ts, etc.) - Update all imports across v2 source, tests, and setup files - Migrate shared utilities (config, env, container-runtime, mount-security, timezone, group-folder) from pino logger to v2 log module - Migrate setup/ files from logger to log with argument order swap - Container agent-runner: move v1 entry to v1/, rename v2 to index.ts - Update setup skill to offer all 13 v2 channels - Install all Chat SDK adapter packages - dist/index.js now runs v2; dist/v1/index.js runs v1 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
136
src/router.ts
136
src/router.ts
@@ -1,43 +1,111 @@
|
||||
import { Channel, NewMessage } from './types.js';
|
||||
import { formatLocalTime } from './timezone.js';
|
||||
/**
|
||||
* Inbound message routing for v2.
|
||||
*
|
||||
* Channel adapter event → resolve messaging group → resolve agent group
|
||||
* → resolve/create session → write messages_in → wake container
|
||||
*/
|
||||
import { getMessagingGroupByPlatform, createMessagingGroup, getMessagingGroupAgents } from './db/messaging-groups.js';
|
||||
import { log } from './log.js';
|
||||
import { resolveSession, writeSessionMessage } from './session-manager.js';
|
||||
import { wakeContainer } from './container-runner.js';
|
||||
import { getSession } from './db/sessions.js';
|
||||
import type { MessagingGroupAgent } from './types.js';
|
||||
|
||||
export function escapeXml(s: string): string {
|
||||
if (!s) return '';
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
function generateId(): string {
|
||||
return `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
export function formatMessages(messages: NewMessage[], timezone: string): string {
|
||||
const lines = messages.map((m) => {
|
||||
const displayTime = formatLocalTime(m.timestamp, timezone);
|
||||
const replyAttr = m.reply_to_message_id ? ` reply_to="${escapeXml(m.reply_to_message_id)}"` : '';
|
||||
const replySnippet =
|
||||
m.reply_to_message_content && m.reply_to_sender_name
|
||||
? `\n <quoted_message from="${escapeXml(m.reply_to_sender_name)}">${escapeXml(m.reply_to_message_content)}</quoted_message>`
|
||||
: '';
|
||||
return `<message sender="${escapeXml(m.sender_name)}" time="${escapeXml(displayTime)}"${replyAttr}>${replySnippet}${escapeXml(m.content)}</message>`;
|
||||
export interface InboundEvent {
|
||||
channelType: string;
|
||||
platformId: string;
|
||||
threadId: string | null;
|
||||
message: {
|
||||
id: string;
|
||||
kind: 'chat' | 'chat-sdk';
|
||||
content: string; // JSON blob
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Route an inbound message from a channel adapter to the correct session.
|
||||
* Creates messaging group + session if they don't exist yet.
|
||||
*/
|
||||
export async function routeInbound(event: InboundEvent): Promise<void> {
|
||||
// 1. Resolve messaging group
|
||||
let mg = getMessagingGroupByPlatform(event.channelType, event.platformId);
|
||||
|
||||
if (!mg) {
|
||||
// Auto-create messaging group (adapter already decided to forward this)
|
||||
const mgId = `mg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
mg = {
|
||||
id: mgId,
|
||||
channel_type: event.channelType,
|
||||
platform_id: event.platformId,
|
||||
name: null,
|
||||
is_group: 0,
|
||||
admin_user_id: null,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
createMessagingGroup(mg);
|
||||
log.info('Auto-created messaging group', {
|
||||
id: mgId,
|
||||
channelType: event.channelType,
|
||||
platformId: event.platformId,
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Resolve agent group via messaging_group_agents
|
||||
const agents = getMessagingGroupAgents(mg.id);
|
||||
if (agents.length === 0) {
|
||||
log.warn('No agent groups configured for messaging group', {
|
||||
messagingGroupId: mg.id,
|
||||
platformId: event.platformId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Pick the best matching agent (highest priority, trigger matching in future)
|
||||
const match = pickAgent(agents, event);
|
||||
if (!match) {
|
||||
log.debug('No agent matched for message', { messagingGroupId: mg.id });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Resolve or create session
|
||||
const { session, created } = resolveSession(match.agent_group_id, mg.id, event.threadId, match.session_mode);
|
||||
|
||||
// 4. Write message to session DB
|
||||
writeSessionMessage(session.agent_group_id, session.id, {
|
||||
id: event.message.id || generateId(),
|
||||
kind: event.message.kind,
|
||||
timestamp: event.message.timestamp,
|
||||
platformId: event.platformId,
|
||||
channelType: event.channelType,
|
||||
threadId: event.threadId,
|
||||
content: event.message.content,
|
||||
});
|
||||
|
||||
const header = `<context timezone="${escapeXml(timezone)}" />\n`;
|
||||
log.info('Message routed', {
|
||||
sessionId: session.id,
|
||||
agentGroup: match.agent_group_id,
|
||||
kind: event.message.kind,
|
||||
created,
|
||||
});
|
||||
|
||||
return `${header}<messages>\n${lines.join('\n')}\n</messages>`;
|
||||
// 5. Wake container
|
||||
const freshSession = getSession(session.id);
|
||||
if (freshSession) {
|
||||
await wakeContainer(freshSession);
|
||||
}
|
||||
}
|
||||
|
||||
export function stripInternalTags(text: string): string {
|
||||
return text.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
|
||||
}
|
||||
|
||||
export function formatOutbound(rawText: string): string {
|
||||
const text = stripInternalTags(rawText);
|
||||
if (!text) return '';
|
||||
return text;
|
||||
}
|
||||
|
||||
export function routeOutbound(channels: Channel[], jid: string, text: string): Promise<void> {
|
||||
const channel = channels.find((c) => c.ownsJid(jid) && c.isConnected());
|
||||
if (!channel) throw new Error(`No channel for JID: ${jid}`);
|
||||
return channel.sendMessage(jid, text);
|
||||
}
|
||||
|
||||
export function findChannel(channels: Channel[], jid: string): Channel | undefined {
|
||||
return channels.find((c) => c.ownsJid(jid));
|
||||
/**
|
||||
* Pick the matching agent for an inbound event.
|
||||
* Currently: highest priority agent. Future: trigger rule matching.
|
||||
*/
|
||||
function pickAgent(agents: MessagingGroupAgent[], _event: InboundEvent): MessagingGroupAgent | null {
|
||||
// Agents are already ordered by priority DESC from the DB query
|
||||
// TODO: apply trigger_rules matching (pattern, mentionOnly, etc.)
|
||||
return agents[0] ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user