export type ChannelName = "feishu" | "wechat" | "dashboard"; export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh"; export type AgentMode = "mock" | "pi"; export interface BotMessage { // A channel package must normalize platform-specific payloads into this shape // before handing control to the app runtime. channel: ChannelName; conversationId: string; senderId: string; messageId: string; text: string; isDirectMessage: boolean; mentions: string[]; threadId?: string; raw?: unknown; } export interface BotReply { text: string; } export type ChannelReply = string | BotReply | null | void; export interface ChannelStreamHandlers { onMeta?(meta: { sessionKey: string }): void; onDelta?(delta: string): void; onError?(error: string): void; } export interface ChannelHandler { onMessage?(message: BotMessage): Promise | ChannelReply; onStreamMessage?( message: BotMessage, handlers: ChannelStreamHandlers ): Promise | ChannelReply; } export interface ChannelInstance { name: ChannelName; start(): Promise; stop(): Promise; } export interface ChannelTransport { start?(): Promise | void; stop?(): Promise | void; send?(message: TOutgoingMessage): Promise | void; } export interface RoutingGateConfig { requireMention?: boolean; } export interface SessionStore { get(key: string): TValue | undefined; set(key: string, value: TValue): TValue; has(key: string): boolean; delete(key: string): boolean; clear(): void; entries(): Array<[string, TValue]>; } export interface AgentRuntime { // App projects own the agent runtime on purpose. We keep it out of channel/core // so future work can use raw pi-coding-agent APIs directly. run(message: BotMessage): Promise; dispose(): Promise; } export interface AgentRuntimeConfig { mode: AgentMode; provider?: string; model?: string; configPath?: string; thinkingLevel?: ThinkingLevel; baseUrl?: string; cwd?: string; agentDir?: string; } export interface BotChannelConfig { enabled: boolean; } export interface FeishuBotChannelConfig extends BotChannelConfig { appId?: string; appSecret?: string; domain?: string; encryptKey?: string; verificationToken?: string; thinkingReaction?: { enabled?: boolean; emojiType?: string; }; } export interface WechatBotChannelConfig extends BotChannelConfig { implementation?: "mock"; } export interface BotRoutingConfig { feishuGroupRequireMention: boolean; wechatGroupRequireMention: boolean; } export interface BotAppConfig { appName: string; configRoot?: Record; agent: AgentRuntimeConfig; routing: BotRoutingConfig; channels: { feishu?: FeishuBotChannelConfig; wechat?: WechatBotChannelConfig; }; } export function normalizeReply(reply: ChannelReply): BotReply | null { if (reply == null) return null; if (typeof reply === "string") return { text: reply }; if (typeof reply === "object" && typeof reply.text === "string") { return { text: reply.text }; } throw new TypeError("Reply must be a string, an object with a text field, or null."); } export function assertBotMessage(message: unknown): asserts message is BotMessage { if (!message || typeof message !== "object") { throw new TypeError("BotMessage must be an object."); } const requiredFields: Array = [ "channel", "conversationId", "senderId", "messageId", "text", "isDirectMessage", "mentions" ]; for (const field of requiredFields) { if (!(field in message)) { throw new TypeError(`BotMessage is missing required field: ${field}`); } } } export function getSessionKey(message: BotMessage): string { assertBotMessage(message); // Session routing is deterministic and host-controlled. // The model never decides where a message should be threaded. if (message.isDirectMessage) { return `${message.channel}:dm:${message.senderId}`; } if (message.threadId) { return `${message.channel}:thread:${message.conversationId}:${message.threadId}`; } return `${message.channel}:group:${message.conversationId}`; } export function shouldHandleGroupMessage( message: BotMessage, routing: RoutingGateConfig = {} ): boolean { assertBotMessage(message); // Mention gating lives in the host/channel layer, not in the model prompt. if (message.isDirectMessage) return true; if (!routing.requireMention) return true; return Array.isArray(message.mentions) && message.mentions.length > 0; } export function createInMemorySessionStore(): SessionStore { const sessions = new Map(); return { get(key) { return sessions.get(key); }, set(key, value) { sessions.set(key, value); return value; }, has(key) { return sessions.has(key); }, delete(key) { return sessions.delete(key); }, clear() { sessions.clear(); }, entries() { return Array.from(sessions.entries()); } }; } export function createBotMessage(overrides: Partial = {}): BotMessage { return { channel: overrides.channel ?? "feishu", conversationId: overrides.conversationId ?? "conversation-1", senderId: overrides.senderId ?? "user-1", messageId: overrides.messageId ?? `msg-${Date.now()}`, text: overrides.text ?? "", isDirectMessage: overrides.isDirectMessage ?? true, mentions: overrides.mentions ?? [], threadId: overrides.threadId, raw: overrides.raw }; }