style: lint

This commit is contained in:
Neko Ayaka
2026-08-26 19:49:58 +08:00
parent e60a04a4ec
commit 98f40d7d0b
1625 changed files with 75216 additions and 75203 deletions
@@ -10,9 +10,9 @@ const log = useLogg('SatoriAPI')
export interface SatoriAPIConfig {
baseUrl: string
token?: string
platform: string
selfId: string
token?: string
}
export class SatoriAPI {
@@ -22,6 +22,43 @@ export class SatoriAPI {
this.config = config
}
async deleteMessage(channelId: string, messageId: string): Promise<void> {
await this.request('/message.delete', {
channel_id: channelId,
message_id: messageId,
})
}
async getMessage(channelId: string, messageId: string): Promise<SatoriMessage> {
const response = await this.request<unknown>('/message.get', {
channel_id: channelId,
message_id: messageId,
})
return v.parse(SatoriMessageSchema, response)
}
async sendMessage(
channelId: string,
content: string,
): Promise<SatoriMessageCreateResponse[]> {
const body: SatoriMessageCreateRequest = {
channel_id: channelId,
content,
}
log.log(`Sending message to channel ${channelId}: ${content}`)
const response = await this.request<unknown[]>('/message.create', body)
return v.parse(v.array(SatoriMessageCreateResponseSchema), response)
}
async updateMessage(channelId: string, messageId: string, content: string): Promise<void> {
await this.request('/message.update', {
channel_id: channelId,
content,
message_id: messageId,
})
}
private getHeaders(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
@@ -44,9 +81,9 @@ export class SatoriAPI {
try {
const response = await fetch(url, {
method: 'POST',
headers: this.getHeaders(),
body: body ? JSON.stringify(body) : undefined,
headers: this.getHeaders(),
method: 'POST',
signal: AbortSignal.timeout(10000),
})
@@ -62,41 +99,4 @@ export class SatoriAPI {
throw error
}
}
async sendMessage(
channelId: string,
content: string,
): Promise<SatoriMessageCreateResponse[]> {
const body: SatoriMessageCreateRequest = {
channel_id: channelId,
content,
}
log.log(`Sending message to channel ${channelId}: ${content}`)
const response = await this.request<unknown[]>('/message.create', body)
return v.parse(v.array(SatoriMessageCreateResponseSchema), response)
}
async getMessage(channelId: string, messageId: string): Promise<SatoriMessage> {
const response = await this.request<unknown>('/message.get', {
channel_id: channelId,
message_id: messageId,
})
return v.parse(SatoriMessageSchema, response)
}
async deleteMessage(channelId: string, messageId: string): Promise<void> {
await this.request('/message.delete', {
channel_id: channelId,
message_id: messageId,
})
}
async updateMessage(channelId: string, messageId: string, content: string): Promise<void> {
await this.request('/message.update', {
channel_id: channelId,
message_id: messageId,
content,
})
}
}
@@ -13,24 +13,24 @@ import { SatoriOpcode } from './types'
const log = useLogg('SatoriClient')
export interface SatoriClientConfig {
url: string
token?: string
apiBaseUrl?: string
token?: string
url: string
}
export class SatoriClient {
private ws?: WebSocket
private apiClients = new Map<string, SatoriAPI>()
private config: SatoriClientConfig
private connected = false
private lastSequenceNumber = 0
private heartbeatInterval?: NodeJS.Timeout
private reconnectTimeout?: NodeJS.Timeout
private shouldReconnect = true
private apiClients = new Map<string, SatoriAPI>()
// Event handlers
private eventHandlers = new Map<string, Set<(event: SatoriEvent) => void | Promise<void>>>()
private readyHandler?: (logins: SatoriReadyBody) => void | Promise<void>
private eventHandlers = new Map<string, Set<(event: SatoriEvent) => Promise<void> | void>>()
private heartbeatInterval?: NodeJS.Timeout
private lastSequenceNumber = 0
private readyHandler?: (logins: SatoriReadyBody) => Promise<void> | void
private reconnectTimeout?: NodeJS.Timeout
private shouldReconnect = true
private ws?: WebSocket
constructor(config: SatoriClientConfig) {
this.config = config
@@ -84,86 +84,95 @@ export class SatoriClient {
})
}
private sendIdentify(): void {
const body: SatoriIdentifyBody = {
token: this.config.token,
sn: this.lastSequenceNumber,
disconnect(): void {
this.shouldReconnect = false
this.stopHeartbeat()
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout)
this.reconnectTimeout = undefined
}
this.sendSignal({
op: SatoriOpcode.IDENTIFY,
body,
})
if (this.ws) {
this.ws.removeAllListeners()
this.ws.close()
this.ws = undefined
}
log.log('Sent IDENTIFY signal')
this.connected = false
log.log('Disconnected from Satori server')
}
private startHeartbeat(): void {
// Send PING every 10 seconds
this.heartbeatInterval = setInterval(() => {
if (this.connected) {
this.sendSignal({ op: SatoriOpcode.PING })
isConnected(): boolean {
return this.connected
}
off(eventType: string, handler?: (event: SatoriEvent) => Promise<void> | void): void {
const handlers = this.eventHandlers.get(eventType)
if (!handlers) {
return
}
if (handler) {
handlers.delete(handler)
if (handlers.size === 0) {
this.eventHandlers.delete(eventType)
}
}, 10000)
}
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = undefined
}
else {
this.eventHandlers.delete(eventType)
}
}
private async handleMessage(data: WebSocket.Data): Promise<void> {
// Event subscription
on(eventType: string, handler: (event: SatoriEvent) => Promise<void> | void): void {
let handlers = this.eventHandlers.get(eventType)
if (!handlers) {
handlers = new Set()
this.eventHandlers.set(eventType, handlers)
}
handlers.add(handler)
}
onReady(handler: (logins: SatoriReadyBody) => Promise<void> | void): void {
this.readyHandler = handler
}
// Public API for sending messages
async sendMessage(platform: string, selfId: string, channelId: string, content: string): Promise<void> {
const key = `${platform}:${selfId}`
log.debug(`sendMessage called - platform: "${platform}", selfId: "${selfId}", key: "${key}"`)
log.debug(`Available API clients: ${Array.from(this.apiClients.keys()).join(', ')}`)
const api = this.apiClients.get(key)
if (!api) {
log.error(`No API client found for ${key}`)
return
}
try {
const rawData = JSON.parse(data.toString())
const signal = v.parse(SatoriSignalSchema, rawData)
switch (signal.op) {
case SatoriOpcode.READY: {
const readyBody = v.parse(SatoriReadyBodySchema, signal.body)
log.log('Received READY signal')
// Initialize API clients for each login
this.initializeAPIClients(readyBody)
if (this.readyHandler) {
await this.readyHandler(readyBody)
}
break
}
case SatoriOpcode.EVENT: {
const event = v.parse(SatoriEventSchema, signal.body)
this.lastSequenceNumber = event.id
await this.handleEvent(event)
break
}
case SatoriOpcode.PONG: {
// Heartbeat response received
break
}
case SatoriOpcode.META: {
log.log('Received META signal:', signal.body)
break
}
default:
log.warn('Unknown opcode received:', signal.op)
}
await api.sendMessage(channelId, content)
log.log(`Message sent to channel ${channelId}`)
}
catch (error) {
if (v.isValiError(error)) {
log.error('Satori protocol validation failed:')
for (const issue of error.issues) {
log.error(` - ${issue.path?.map(p => p.key).join('.')}: ${issue.message}`)
}
}
else {
log.withError(error as Error).error('Failed to handle message')
}
log.withError(error as Error).error('Failed to send message')
}
}
private handleDisconnect(): void {
this.connected = false
this.stopHeartbeat()
if (this.ws) {
this.ws.removeAllListeners()
this.ws = undefined
}
if (this.shouldReconnect) {
log.log('Attempting to reconnect in 5 seconds...')
this.reconnectTimeout = setTimeout(() => {
void this.connect()
}, 5000)
}
}
@@ -190,29 +199,56 @@ export class SatoriClient {
}
}
private handleDisconnect(): void {
this.connected = false
this.stopHeartbeat()
private async handleMessage(data: WebSocket.Data): Promise<void> {
try {
const rawData = JSON.parse(data.toString())
const signal = v.parse(SatoriSignalSchema, rawData)
if (this.ws) {
this.ws.removeAllListeners()
this.ws = undefined
}
switch (signal.op) {
case SatoriOpcode.EVENT: {
const event = v.parse(SatoriEventSchema, signal.body)
this.lastSequenceNumber = event.id
await this.handleEvent(event)
break
}
if (this.shouldReconnect) {
log.log('Attempting to reconnect in 5 seconds...')
this.reconnectTimeout = setTimeout(() => {
void this.connect()
}, 5000)
}
}
case SatoriOpcode.META: {
log.log('Received META signal:', signal.body)
break
}
private sendSignal(signal: SatoriSignal): void {
if (this.ws && this.connected) {
this.ws.send(JSON.stringify(signal))
case SatoriOpcode.PONG: {
// Heartbeat response received
break
}
case SatoriOpcode.READY: {
const readyBody = v.parse(SatoriReadyBodySchema, signal.body)
log.log('Received READY signal')
// Initialize API clients for each login
this.initializeAPIClients(readyBody)
if (this.readyHandler) {
await this.readyHandler(readyBody)
}
break
}
default:
log.warn('Unknown opcode received:', signal.op)
}
}
else {
log.warn('Cannot send signal: not connected')
catch (error) {
if (v.isValiError(error)) {
log.error('Satori protocol validation failed:')
for (const issue of error.issues) {
log.error(` - ${issue.path?.map(p => p.key).join('.')}: ${issue.message}`)
}
}
else {
log.withError(error as Error).error('Failed to handle message')
}
}
}
@@ -224,87 +260,51 @@ export class SatoriClient {
const key = `${login.platform}:${login.self_id}`
this.apiClients.set(key, new SatoriAPI({
baseUrl: apiBaseUrl,
token: this.config.token,
platform: login.platform,
selfId: login.self_id,
token: this.config.token,
}))
log.log(`Initialized API client for ${key}`)
}
}
}
// Public API for sending messages
async sendMessage(platform: string, selfId: string, channelId: string, content: string): Promise<void> {
const key = `${platform}:${selfId}`
log.debug(`sendMessage called - platform: "${platform}", selfId: "${selfId}", key: "${key}"`)
log.debug(`Available API clients: ${Array.from(this.apiClients.keys()).join(', ')}`)
const api = this.apiClients.get(key)
if (!api) {
log.error(`No API client found for ${key}`)
return
private sendIdentify(): void {
const body: SatoriIdentifyBody = {
sn: this.lastSequenceNumber,
token: this.config.token,
}
try {
await api.sendMessage(channelId, content)
log.log(`Message sent to channel ${channelId}`)
}
catch (error) {
log.withError(error as Error).error('Failed to send message')
}
this.sendSignal({
body,
op: SatoriOpcode.IDENTIFY,
})
log.log('Sent IDENTIFY signal')
}
// Event subscription
on(eventType: string, handler: (event: SatoriEvent) => void | Promise<void>): void {
let handlers = this.eventHandlers.get(eventType)
if (!handlers) {
handlers = new Set()
this.eventHandlers.set(eventType, handlers)
}
handlers.add(handler)
}
off(eventType: string, handler?: (event: SatoriEvent) => void | Promise<void>): void {
const handlers = this.eventHandlers.get(eventType)
if (!handlers) {
return
}
if (handler) {
handlers.delete(handler)
if (handlers.size === 0) {
this.eventHandlers.delete(eventType)
}
private sendSignal(signal: SatoriSignal): void {
if (this.ws && this.connected) {
this.ws.send(JSON.stringify(signal))
}
else {
this.eventHandlers.delete(eventType)
log.warn('Cannot send signal: not connected')
}
}
onReady(handler: (logins: SatoriReadyBody) => void | Promise<void>): void {
this.readyHandler = handler
private startHeartbeat(): void {
// Send PING every 10 seconds
this.heartbeatInterval = setInterval(() => {
if (this.connected) {
this.sendSignal({ op: SatoriOpcode.PING })
}
}, 10000)
}
disconnect(): void {
this.shouldReconnect = false
this.stopHeartbeat()
if (this.reconnectTimeout) {
clearTimeout(this.reconnectTimeout)
this.reconnectTimeout = undefined
private stopHeartbeat(): void {
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval)
this.heartbeatInterval = undefined
}
if (this.ws) {
this.ws.removeAllListeners()
this.ws.close()
this.ws = undefined
}
this.connected = false
log.log('Disconnected from Satori server')
}
isConnected(): boolean {
return this.connected
}
}
@@ -1,89 +1,89 @@
import * as v from 'valibot'
export const SatoriUserSchema = v.object({
avatar: v.optional(v.string()),
id: v.string(),
is_bot: v.optional(v.boolean()),
name: v.optional(v.string()),
nick: v.optional(v.string()),
avatar: v.optional(v.string()),
is_bot: v.optional(v.boolean()),
})
export const SatoriChannelSchema = v.object({
id: v.string(),
type: v.number(),
name: v.optional(v.string()),
parent_id: v.optional(v.string()),
type: v.number(),
})
export const SatoriGuildSchema = v.object({
avatar: v.optional(v.string()),
id: v.string(),
name: v.optional(v.string()),
avatar: v.optional(v.string()),
})
export const SatoriGuildMemberSchema = v.object({
user: v.optional(SatoriUserSchema),
nick: v.optional(v.string()),
avatar: v.optional(v.string()),
joined_at: v.optional(v.number()),
nick: v.optional(v.string()),
user: v.optional(SatoriUserSchema),
})
export const SatoriMessageSchema = v.object({
id: v.string(),
content: v.string(),
platform: v.optional(v.string()),
channel: v.optional(SatoriChannelSchema),
guild: v.optional(SatoriGuildSchema),
member: v.optional(SatoriGuildMemberSchema),
user: v.optional(SatoriUserSchema),
content: v.string(),
created_at: v.optional(v.number()),
guild: v.optional(SatoriGuildSchema),
id: v.string(),
member: v.optional(SatoriGuildMemberSchema),
platform: v.optional(v.string()),
updated_at: v.optional(v.number()),
user: v.optional(SatoriUserSchema),
})
export const SatoriLoginSchema = v.object({
user: v.optional(SatoriUserSchema),
self_id: v.optional(v.string()),
platform: v.optional(v.string()),
status: v.number(),
features: v.optional(v.array(v.string())),
platform: v.optional(v.string()),
proxy_urls: v.optional(v.array(v.string())),
self_id: v.optional(v.string()),
status: v.number(),
user: v.optional(SatoriUserSchema),
})
export const SatoriArgvSchema = v.object({
name: v.string(),
arguments: v.array(v.unknown()),
name: v.string(),
options: v.record(v.string(), v.unknown()),
})
export const SatoriEventSchema = v.object({
id: v.number(),
type: v.string(),
platform: v.string(),
self_id: v.string(),
timestamp: v.number(),
_data: v.optional(v.record(v.string(), v.unknown())),
_type: v.optional(v.string()),
argv: v.optional(SatoriArgvSchema),
button: v.optional(v.object({ id: v.string() })),
channel: v.optional(SatoriChannelSchema),
guild: v.optional(SatoriGuildSchema),
id: v.number(),
login: v.optional(SatoriLoginSchema),
member: v.optional(SatoriGuildMemberSchema),
message: v.optional(SatoriMessageSchema),
operator: v.optional(SatoriUserSchema),
platform: v.string(),
role: v.optional(v.object({ id: v.string(), name: v.optional(v.string()) })),
self_id: v.string(),
timestamp: v.number(),
type: v.string(),
user: v.optional(SatoriUserSchema),
_type: v.optional(v.string()),
_data: v.optional(v.record(v.string(), v.unknown())),
})
export const SatoriMessageCreateResponseSchema = v.object({
id: v.string(),
content: v.optional(v.string()),
channel: v.optional(SatoriChannelSchema),
guild: v.optional(SatoriGuildSchema),
member: v.optional(SatoriGuildMemberSchema),
user: v.optional(SatoriUserSchema),
content: v.optional(v.string()),
created_at: v.optional(v.number()),
guild: v.optional(SatoriGuildSchema),
id: v.string(),
member: v.optional(SatoriGuildMemberSchema),
updated_at: v.optional(v.number()),
user: v.optional(SatoriUserSchema),
})
export const SatoriReadyBodySchema = v.object({
@@ -92,8 +92,8 @@ export const SatoriReadyBodySchema = v.object({
})
export const SatoriSignalSchema = v.object({
op: v.number(),
body: v.optional(v.unknown()),
op: v.number(),
})
export function SatoriListSchema<T extends v.BaseSchema<any, any, any>>(itemSchema: T) {
@@ -13,59 +13,67 @@ export enum SatoriOpcode {
META = 5, // 接收元信息更新
}
// WebSocket Signal Structure
export interface SatoriSignal<T = unknown> {
op: SatoriOpcode
body?: T
// Interaction Argv
export interface SatoriArgv {
arguments: unknown[]
name: string
options: Record<string, unknown>
}
// IDENTIFY signal body
export interface SatoriIdentifyBody {
token?: string
sn?: number
// Bidirectional paginated list
export interface SatoriBidiList<T> {
data: T[]
next?: string
prev?: string
}
// READY signal body
export interface SatoriReadyBody {
logins: SatoriLogin[]
proxy_urls?: string[]
}
// META signal body
export interface SatoriMetaBody {
proxy_urls?: string[]
}
// User resource
export interface SatoriUser {
// Interaction Button
export interface SatoriButton {
id: string
name?: string
nick?: string
avatar?: string
is_bot?: boolean
}
// Channel resource
export interface SatoriChannel {
id: string
type: number
name?: string
parent_id?: string
type: number
}
// Event structure
export interface SatoriEvent {
_data?: Record<string, unknown>
_type?: string
argv?: SatoriArgv
button?: SatoriButton
channel?: SatoriChannel
guild?: SatoriGuild
id: number
login?: SatoriLogin
member?: SatoriGuildMember
message?: SatoriMessage
operator?: SatoriUser
platform: string
role?: SatoriGuildRole
self_id: string
timestamp: number
type: string
user?: SatoriUser
}
// Guild resource
export interface SatoriGuild {
avatar?: string
id: string
name?: string
avatar?: string
}
// Guild Member resource
export interface SatoriGuildMember {
user?: SatoriUser
nick?: string
avatar?: string
joined_at?: number
nick?: string
user?: SatoriUser
}
// Guild Role resource
@@ -74,60 +82,39 @@ export interface SatoriGuildRole {
name?: string
}
// Message resource
export interface SatoriMessage {
id: string
content: string
platform?: string
channel?: SatoriChannel
guild?: SatoriGuild
member?: SatoriGuildMember
user?: SatoriUser
created_at?: number
updated_at?: number
// IDENTIFY signal body
export interface SatoriIdentifyBody {
sn?: number
token?: string
}
// Paginated list
export interface SatoriList<T> {
data: T[]
next?: string
}
// Login resource
export interface SatoriLogin {
user?: SatoriUser
self_id?: string
platform?: string
status: number
features?: string[]
platform?: string
proxy_urls?: string[]
self_id?: string
status: number
user?: SatoriUser
}
// Interaction Argv
export interface SatoriArgv {
name: string
arguments: unknown[]
options: Record<string, unknown>
}
// Interaction Button
export interface SatoriButton {
id: string
}
// Event structure
export interface SatoriEvent {
id: number
type: string
platform: string
self_id: string
timestamp: number
argv?: SatoriArgv
button?: SatoriButton
// Message resource
export interface SatoriMessage {
channel?: SatoriChannel
content: string
created_at?: number
guild?: SatoriGuild
login?: SatoriLogin
id: string
member?: SatoriGuildMember
message?: SatoriMessage
operator?: SatoriUser
role?: SatoriGuildRole
platform?: string
updated_at?: number
user?: SatoriUser
_type?: string
_data?: Record<string, unknown>
}
// API Request/Response types
@@ -137,25 +124,38 @@ export interface SatoriMessageCreateRequest {
}
export interface SatoriMessageCreateResponse {
id: string
content?: string
channel?: SatoriChannel
guild?: SatoriGuild
member?: SatoriGuildMember
user?: SatoriUser
content?: string
created_at?: number
guild?: SatoriGuild
id: string
member?: SatoriGuildMember
updated_at?: number
user?: SatoriUser
}
// Paginated list
export interface SatoriList<T> {
data: T[]
next?: string
// META signal body
export interface SatoriMetaBody {
proxy_urls?: string[]
}
// Bidirectional paginated list
export interface SatoriBidiList<T> {
data: T[]
prev?: string
next?: string
// READY signal body
export interface SatoriReadyBody {
logins: SatoriLogin[]
proxy_urls?: string[]
}
// WebSocket Signal Structure
export interface SatoriSignal<T = unknown> {
body?: T
op: SatoriOpcode
}
// User resource
export interface SatoriUser {
avatar?: string
id: string
is_bot?: boolean
name?: string
nick?: string
}
@@ -5,14 +5,13 @@ import { useLogg } from '@guiiai/logg'
import { deleteUnreadEventsByIds } from '../../lib/db'
export const readMessagesAction: ActionHandler = {
name: 'read_unread_messages',
description: 'Read unread messages from a specific channel',
execute: async (botContext, chatCtx, args): Promise<ActionResult> => {
if (args.action !== 'read_unread_messages') {
return {
success: false,
shouldContinue: true,
result: 'System Error: Action mismatch for read_unread_messages.',
shouldContinue: true,
success: false,
}
}
const logger = useLogg('readMessagesAction').useGlobalConfig()
@@ -20,9 +19,9 @@ export const readMessagesAction: ActionHandler = {
if (!channelId) {
return {
success: false,
shouldContinue: true,
result: 'System Error: No channelId provided for read_unread_messages.',
shouldContinue: true,
success: false,
}
}
@@ -31,9 +30,9 @@ export const readMessagesAction: ActionHandler = {
if (!unreadEventsForThisChannel || unreadEventsForThisChannel.length === 0) {
delete botContext.unreadEvents[channelId]
return {
success: true,
shouldContinue: true,
result: 'AIRI System: No unread messages found.',
shouldContinue: true,
success: true,
}
}
@@ -60,9 +59,10 @@ export const readMessagesAction: ActionHandler = {
logger.log(`Read ${unreadEventsForThisChannel.length} unread events from channel ${channelId}`)
return {
success: true,
shouldContinue: true,
result: `AIRI System: Read ${unreadEventsForThisChannel.length} unread events from channel ${channelId}:\n${formattedMessages}`,
shouldContinue: true,
success: true,
}
},
name: 'read_unread_messages',
}
@@ -7,13 +7,12 @@ import { recordMessage } from '../../lib/db'
export function createSendMessageAction(client: SatoriClient): ActionHandler {
return {
name: 'send_message',
execute: async (ctx, chatCtx, args) => {
if (args.action !== 'send_message') {
return {
success: false,
shouldContinue: true,
result: 'System Error: Action mismatch for send_message.',
shouldContinue: true,
success: false,
}
}
const logger = useLogg('Action:send_message').useGlobalConfig()
@@ -24,9 +23,9 @@ export function createSendMessageAction(client: SatoriClient): ActionHandler {
logger.withField('channelId', channelId).warn('Aborting message send due to new incoming events')
return {
success: false,
shouldContinue: true,
result: 'AIRI System: [INTERRUPT] Message sending ABORTED. New unread messages were detected from the user. Please [read_unread_messages] first to understand the new context.',
shouldContinue: true,
success: false,
}
}
@@ -38,19 +37,20 @@ export function createSendMessageAction(client: SatoriClient): ActionHandler {
await recordMessage(channelId, chatCtx.selfId, 'AIRI', content)
return {
success: true,
shouldContinue: true,
result: `AIRI System: Message sent to ${channelId}: ${content}`,
shouldContinue: true,
success: true,
}
}
catch (error) {
logger.withError(error as Error).error('Failed to send message')
return {
success: false,
shouldContinue: true,
result: `AIRI System: Error sending message: ${(error as Error).message}`,
shouldContinue: true,
success: false,
}
}
},
name: 'send_message',
}
}
@@ -5,60 +5,60 @@ import { listChannels } from '../../lib/db'
// 1. Continue Action
export const continueAction: ActionHandler = {
name: 'continue',
execute: async (): Promise<ActionResult> => {
return {
success: true,
shouldContinue: false,
result: 'AIRI System: Acknowledged, will now wait for user input.',
shouldContinue: false,
success: true,
}
},
name: 'continue',
}
// 2. Break Action
export const breakAction: ActionHandler = {
name: 'break',
execute: async (_ctx, chatCtx): Promise<ActionResult> => {
chatCtx.actions = []
return {
success: true,
shouldContinue: false,
result: 'AIRI System: Memory cleared. Loop broken.',
shouldContinue: false,
success: true,
}
},
name: 'break',
}
// 3. Sleep Action
export const sleepAction: ActionHandler = {
name: 'sleep',
execute: async (_ctx, _chatCtx, args): Promise<ActionResult> => {
if (args.action !== 'sleep') {
return {
success: false,
shouldContinue: true,
result: 'System Error: Action mismatch for sleep.',
shouldContinue: true,
success: false,
}
}
const duration = args.duration || SLEEP_DURATION_MS
await new Promise(resolve => setTimeout(resolve, duration))
return {
success: true,
shouldContinue: true,
result: `AIRI System: Slept for ${duration / 1000} seconds.`,
shouldContinue: true,
success: true,
}
},
name: 'sleep',
}
// 4. List Channels Action
export const listChannelsAction: ActionHandler = {
name: 'list_channels',
execute: async (): Promise<ActionResult> => {
const channels = await listChannels()
const list = channels.map(c => `ID:${c.id}, Name:${c.name}, Platform:${c.platform}`).join('\n')
return {
success: true,
shouldContinue: true,
result: `AIRI System: Channel List:\n${list}`,
shouldContinue: true,
success: true,
}
},
name: 'list_channels',
}
@@ -1,13 +1,6 @@
import type { Action, BotContext, ChatContext } from '../core/types'
export interface ActionResult {
success: boolean
shouldContinue: boolean
result: unknown
}
export interface ActionHandler {
name: string
description?: string
execute: (
ctx: BotContext,
@@ -15,4 +8,11 @@ export interface ActionHandler {
args: Action,
abortSignal?: AbortSignal,
) => Promise<ActionResult>
name: string
}
export interface ActionResult {
result: unknown
shouldContinue: boolean
success: boolean
}
@@ -9,10 +9,6 @@ import { breakAction, continueAction, listChannelsAction, sleepAction } from './
export class ActionRegistry {
private actions = new Map<string, ActionHandler>()
register(handler: ActionHandler) {
this.actions.set(handler.name, handler)
}
get(name: string): ActionHandler | undefined {
return this.actions.get(name)
}
@@ -29,6 +25,10 @@ export class ActionRegistry {
this.register(createSendMessageAction(client))
this.register(readMessagesAction)
}
register(handler: ActionHandler) {
this.actions.set(handler.name, handler)
}
}
export const globalRegistry = new ActionRegistry()
+18 -18
View File
@@ -3,10 +3,8 @@ import { env, exit } from 'node:process'
import * as v from 'valibot'
const ConfigSchema = v.object({
satori: v.object({
wsUrl: v.string(),
token: v.optional(v.string()),
apiBaseUrl: v.optional(v.string()),
db: v.object({
path: v.optional(v.string(), '../../data/pglite-db'),
}),
llm: v.object({
apiKey: v.string(),
@@ -14,25 +12,19 @@ const ConfigSchema = v.object({
model: v.string(),
ollamaDisableThink: v.optional(v.boolean(), false),
}),
db: v.object({
path: v.optional(v.string(), '../../data/pglite-db'),
satori: v.object({
apiBaseUrl: v.optional(v.string()),
token: v.optional(v.string()),
wsUrl: v.string(),
}),
})
export type Config = v.InferOutput<typeof ConfigSchema>
function parseBoolean(value: string | undefined): boolean | undefined {
if (value === undefined)
return undefined
return value.toLowerCase() === 'true' || value === '1'
}
export function loadConfig(): Config {
const rawConfig = {
satori: {
wsUrl: env.SATORI_WS_URL || 'ws://localhost:5140/satori/v1/events',
token: env.SATORI_TOKEN,
apiBaseUrl: env.SATORI_API_BASE_URL,
db: {
path: env.DB_PATH,
},
llm: {
apiKey: env.LLM_API_KEY,
@@ -40,8 +32,10 @@ export function loadConfig(): Config {
model: env.LLM_MODEL,
ollamaDisableThink: parseBoolean(env.LLM_OLLAMA_DISABLE_THINK),
},
db: {
path: env.DB_PATH,
satori: {
apiBaseUrl: env.SATORI_API_BASE_URL,
token: env.SATORI_TOKEN,
wsUrl: env.SATORI_WS_URL || 'ws://localhost:5140/satori/v1/events',
},
}
@@ -62,4 +56,10 @@ export function loadConfig(): Config {
}
}
function parseBoolean(value: string | undefined): boolean | undefined {
if (value === undefined)
return undefined
return value.toLowerCase() === 'true' || value === '1'
}
export const config = loadConfig()
@@ -18,9 +18,9 @@ export async function dispatchAction(
if (!parseResult.success) {
return {
success: false,
shouldContinue: true,
result: `System Error: Invalid action payload: ${parseResult.issues.map(i => i.message).join(', ')}`,
shouldContinue: true,
success: false,
}
}
@@ -29,9 +29,9 @@ export async function dispatchAction(
if (!handler) {
return {
success: false,
shouldContinue: true,
result: `System Error: Action "${validatedAction.action}" is not implemented.`,
shouldContinue: true,
success: false,
}
}
@@ -50,9 +50,9 @@ export async function dispatchAction(
catch (error) {
log.withError(error as Error).error('Action execution failed')
return {
success: false,
shouldContinue: true,
result: `System Error: Execution failed: ${(error as Error).message}`,
shouldContinue: true,
success: false,
}
}
}
+18 -18
View File
@@ -7,24 +7,6 @@ import type { BotContext } from '../types'
import { pushToEventQueue } from '../../lib/db'
import { onMessageArrival } from './scheduler'
/**
* Set up the ready event handler
* Logs connection information when Satori client is ready
*/
export function setupReadyEventHandler(
satoriClient: SatoriClient,
logger: Logg,
): void {
satoriClient.onReady((ready: SatoriReadyBody) => {
logger.log('Satori client ready:', ready)
logger.log(`Connected to ${ready.logins.length} platform(s)`)
for (const login of ready.logins) {
logger.log(`- ${login.platform} (${login.self_id}): ${login.status}`)
}
})
}
/**
* Set up the message-created event handler
* Processes incoming messages, filters bot's own messages, and triggers bot responses
@@ -68,3 +50,21 @@ export function setupMessageEventHandler(
await onMessageArrival(botContext, satoriClient)
})
}
/**
* Set up the ready event handler
* Logs connection information when Satori client is ready
*/
export function setupReadyEventHandler(
satoriClient: SatoriClient,
logger: Logg,
): void {
satoriClient.onReady((ready: SatoriReadyBody) => {
logger.log('Satori client ready:', ready)
logger.log(`Connected to ${ready.logins.length} platform(s)`)
for (const login of ready.logins) {
logger.log(`- ${login.platform} (${login.self_id}): ${login.status}`)
}
})
}
@@ -70,8 +70,8 @@ export async function handleLoopStep(
// Dynamic history injection: Fetch last 10 messages from DB
const dbMessages = await getRecentMessages(chatCtx.channelId, 10)
const llmMessages: LLMMessage[] = dbMessages.map(m => ({
role: m.userId === chatCtx.selfId ? 'assistant' : 'user',
content: m.content,
role: m.userId === chatCtx.selfId ? 'assistant' : 'user',
}))
const actionPayload = await imagineAnAction(
@@ -79,8 +79,8 @@ export async function handleLoopStep(
llmMessages,
chatCtx?.actions || [],
{
unreadEvents: ctx.unreadEvents,
incomingEvents: currentIncoming ? [currentIncoming] : [],
unreadEvents: ctx.unreadEvents,
},
)
@@ -129,6 +129,14 @@ export async function loopIterationForChannel(
await handleLoopStep(bot, satoriClient, chatContext, incomingEvent)
}
/**
* Start the periodic loop
* Begins the recursive periodic processing of channels with unread messages
*/
export function startPeriodicLoop(botCtx: BotContext, satoriClient: SatoriClient) {
loopPeriodic(botCtx, satoriClient)
}
/**
* Process periodic loop iteration for existing channels with unread messages
* Only processes channels that have unread messages to avoid unnecessary LLM calls
@@ -200,14 +208,6 @@ function loopPeriodic(botCtx: BotContext, satoriClient: SatoriClient) {
}, PERIODIC_LOOP_INTERVAL_MS)
}
/**
* Start the periodic loop
* Begins the recursive periodic processing of channels with unread messages
*/
export function startPeriodicLoop(botCtx: BotContext, satoriClient: SatoriClient) {
loopPeriodic(botCtx, satoriClient)
}
let isQueueConsumerRunning = false
/**
@@ -267,9 +267,9 @@ export async function onMessageArrival(
botContext.logger
.withFields({
channelId: chatCtx.channelId,
sourceUserId: currMsg.event.user?.id || currMsg.event.member?.user?.id,
selfId: chatCtx.selfId,
messageId: currMsg.event.id,
selfId: chatCtx.selfId,
sourceUserId: currMsg.event.user?.id || currMsg.event.member?.user?.id,
})
.debug('[DEBUG] Skipping bot\'s own event in unreadEvents - filtered out')
botContext.eventQueue.shift()
@@ -294,7 +294,7 @@ export async function onMessageArrival(
}
const unreadEventId = await pushToUnreadEvents(chatCtx.channelId, currMsg.event)
unreadEventsForThisChannel.push({ id: unreadEventId, event: currMsg.event })
unreadEventsForThisChannel.push({ event: currMsg.event, id: unreadEventId })
if (unreadEventsForThisChannel.length > MAX_UNREAD_EVENTS) {
unreadEventsForThisChannel = unreadEventsForThisChannel.slice(-MAX_UNREAD_EVENTS)
@@ -20,8 +20,8 @@ export async function imagineAnAction(
messages: LLMMessage[],
actions: { action: Action, result: unknown }[],
globalStates: {
unreadEvents: Record<string, StoredUnreadEvent[]>
incomingEvents?: SatoriEvent[]
unreadEvents: Record<string, StoredUnreadEvent[]>
},
): Promise<Action | undefined> {
const logger = useLogg('imagineAnAction').useGlobalConfig()
@@ -57,11 +57,11 @@ export async function imagineAnAction(
try {
const req = {
abortSignal: currentAbortController?.signal,
apiKey: config.llm.apiKey,
baseURL: config.llm.baseUrl,
model: config.llm.model,
messages: requestMessages,
abortSignal: currentAbortController?.signal,
model: config.llm.model,
} satisfies GenerateTextOptions
if (config.llm.ollamaDisableThink) {
@@ -76,12 +76,12 @@ export async function imagineAnAction(
}
logger.withFields({
response: res.text,
unreadEvents: Object.fromEntries(Object.entries(globalStates.unreadEvents).map(([key, value]) => [key, value.length])),
now: new Date().toLocaleString(),
totalTokens: res.usage.totalTokens,
promptTokens: res.usage.inputTokens,
completion_tokens: res.usage.outputTokens,
now: new Date().toLocaleString(),
promptTokens: res.usage.inputTokens,
response: res.text,
totalTokens: res.usage.totalTokens,
unreadEvents: Object.fromEntries(Object.entries(globalStates.unreadEvents).map(([key, value]) => [key, value.length])),
}).log('Generated action')
responseText = res.text
@@ -15,12 +15,12 @@ export async function createBotContext(logger: Logg): Promise<BotContext> {
])
const botSelf: BotContext = {
eventQueue,
unreadEvents,
processedIds: new Set(),
logger,
lastInteractedChannelIds: [],
chats: new Map<string, ChatContext>(),
eventQueue,
lastInteractedChannelIds: [],
logger,
processedIds: new Set(),
unreadEvents,
}
return botSelf
@@ -48,13 +48,13 @@ export async function ensureChatContext(botCtx: BotContext, channelId: string):
const channelInfo = channels.find(c => c.id === channelId)
const newChatContext: ChatContext = {
actions: [],
channelId,
currentAbortController: undefined,
currentTask: undefined,
isProcessing: false,
platform: channelInfo?.platform || '',
selfId: channelInfo?.selfId || '',
isProcessing: false,
currentTask: undefined,
currentAbortController: undefined,
actions: [],
}
log
+36 -36
View File
@@ -24,8 +24,8 @@ export const ListChannelsActionSchema = v.object({
export const SendMessageActionSchema = v.object({
action: v.literal('send_message'),
content: v.string(),
channelId: v.string(),
content: v.string(),
})
export const ReadUnreadMessagesActionSchema = v.object({
@@ -44,9 +44,42 @@ export const ActionSchema = v.union([
export type Action = v.InferOutput<typeof ActionSchema>
export interface BotContext {
chats: Map<string, ChatContext>
currentProcessingStartTime?: number
eventQueue: PendingEvent[]
lastInteractedChannelIds: string[]
logger: Logg
processedIds: Set<string>
unreadEvents: Record<string, StoredUnreadEvent[]> // channelId -> events
}
export interface CancellablePromise<T> {
promise: Promise<T>
cancel: () => void
promise: Promise<T>
}
export interface ChatContext {
actions: { action: Action, result: unknown }[]
channelId: string
currentAbortController?: AbortController
currentTask?: CancellablePromise<void>
isProcessing: boolean
platform: string
selfId: string
}
export interface PendingEvent {
event: SatoriEvent
id: string
status: 'pending' | 'ready'
}
export interface StoredUnreadEvent {
event: SatoriEvent
id: string
}
export function cancellable<T>(promise: Promise<T>): CancellablePromise<T> {
@@ -58,40 +91,7 @@ export function cancellable<T>(promise: Promise<T>): CancellablePromise<T> {
})
return {
promise: wrappedPromise,
cancel: () => cancel?.(),
promise: wrappedPromise,
}
}
export interface PendingEvent {
id: string
event: SatoriEvent
status: 'pending' | 'ready'
}
export interface StoredUnreadEvent {
id: string
event: SatoriEvent
}
export interface BotContext {
logger: Logg
eventQueue: PendingEvent[]
unreadEvents: Record<string, StoredUnreadEvent[]> // channelId -> events
processedIds: Set<string>
lastInteractedChannelIds: string[]
currentProcessingStartTime?: number
chats: Map<string, ChatContext>
}
export interface ChatContext {
channelId: string
platform: string
selfId: string
isProcessing: boolean
currentTask?: CancellablePromise<void>
currentAbortController?: AbortController
actions: { action: Action, result: unknown }[]
}
+44 -44
View File
@@ -1,33 +1,35 @@
import type { SatoriEvent, SatoriMessage } from '../adapter/satori/types'
import type { Action, BotContext, ChatContext } from './types'
/**
* Intelligently truncate action history while preserving logical chains.
* If the first action in the kept list is a 'continue', it backtracks to include
* the action that triggered it, ensuring the LLM has full context of its sequence.
* Format debug context for logging
* Creates a summary of bot state for debugging
*/
export function trimActions(
actions: { action: Action, result: unknown }[],
max: number,
keep: number,
): { action: Action, result: unknown }[] {
if (actions.length <= max) {
return actions
export function formatDebugContext(
ctx: BotContext,
chatCtx?: ChatContext,
): Record<string, unknown> {
const unreadEventsSummary = Object.fromEntries(
Object.entries(ctx.unreadEvents).map(([key, value]) => [key, value.length]),
)
const context: Record<string, unknown> = {
messageQueueLength: ctx.eventQueue.length,
totalUnreadCount: Object.values(ctx.unreadEvents).reduce((acc, cur) => acc + cur.length, 0),
unreadEvents: unreadEventsSummary,
}
let startIndex = actions.length - keep
if (chatCtx) {
context.channelId = chatCtx.channelId
context.totalActionsInContext = chatCtx.actions.length
// Backtrack to avoid starting with a 'continue' action which lacks its previous context
while (startIndex > 0) {
const currentAction = actions[startIndex].action
if (currentAction.action === 'continue') {
startIndex--
}
else {
break
}
const lastActions = chatCtx.actions.slice(-3).map(action => ({
action: action.action.action,
result: typeof action.result === 'string' ? action.result.substring(0, 100) : String(action.result).substring(0, 100),
}))
context.lastActions = lastActions
}
return actions.slice(startIndex)
return context
}
/**
@@ -67,33 +69,31 @@ export function isBotOwnMessage(
}
/**
* Format debug context for logging
* Creates a summary of bot state for debugging
* Intelligently truncate action history while preserving logical chains.
* If the first action in the kept list is a 'continue', it backtracks to include
* the action that triggered it, ensuring the LLM has full context of its sequence.
*/
export function formatDebugContext(
ctx: BotContext,
chatCtx?: ChatContext,
): Record<string, unknown> {
const unreadEventsSummary = Object.fromEntries(
Object.entries(ctx.unreadEvents).map(([key, value]) => [key, value.length]),
)
const context: Record<string, unknown> = {
messageQueueLength: ctx.eventQueue.length,
unreadEvents: unreadEventsSummary,
totalUnreadCount: Object.values(ctx.unreadEvents).reduce((acc, cur) => acc + cur.length, 0),
export function trimActions(
actions: { action: Action, result: unknown }[],
max: number,
keep: number,
): { action: Action, result: unknown }[] {
if (actions.length <= max) {
return actions
}
if (chatCtx) {
context.channelId = chatCtx.channelId
context.totalActionsInContext = chatCtx.actions.length
let startIndex = actions.length - keep
const lastActions = chatCtx.actions.slice(-3).map(action => ({
action: action.action.action,
result: typeof action.result === 'string' ? action.result.substring(0, 100) : String(action.result).substring(0, 100),
}))
context.lastActions = lastActions
// Backtrack to avoid starting with a 'continue' action which lacks its previous context
while (startIndex > 0) {
const currentAction = actions[startIndex].action
if (currentAction.action === 'continue') {
startIndex--
}
else {
break
}
}
return context
return actions.slice(startIndex)
}
+2 -2
View File
@@ -20,9 +20,9 @@ async function main() {
// Create Satori client
const satoriClient = new SatoriClient({
url: config.satori.wsUrl,
token: config.satori.token,
apiBaseUrl: config.satori.apiBaseUrl,
token: config.satori.token,
url: config.satori.wsUrl,
})
// Create bot context
+97 -97
View File
@@ -27,33 +27,20 @@ export async function initDb() {
await migrate(db, { migrationsFolder: migrationsPath })
}
export const { channels, messages, eventQueue, unreadEvents } = schema
export const { channels, eventQueue, messages, unreadEvents } = schema
export async function recordChannel(id: string, name: string, platform: string, selfId: string) {
await db.insert(channels)
.values({ id, name, platform, selfId })
.onConflictDoUpdate({
target: channels.id,
set: { name, platform, selfId },
})
export async function clearEventQueue() {
await db.delete(eventQueue)
}
export async function listChannels() {
return await db.select().from(channels)
export async function clearUnreadEventsForChannel(channelId: string) {
await db.delete(unreadEvents).where(eq(unreadEvents.channelId, channelId))
}
export async function recordMessage(channelId: string, userId: string, userName: string, content: string, timestamp?: number) {
const ts = timestamp || Date.now()
const id = nanoid()
await db.insert(messages).values({
id,
channelId,
userId,
userName,
content,
timestamp: ts,
})
export async function deleteUnreadEventsByIds(channelId: string, ids: string[]) {
if (ids.length === 0)
return
await db.delete(unreadEvents).where(inArray(unreadEvents.id, ids))
}
/**
@@ -70,90 +57,19 @@ export async function getRecentMessages(channelId: string, limit: number = 10) {
// Event Queue Persistence
export async function pushToEventQueue(item: { event: SatoriEvent, status: 'pending' | 'ready' }) {
const id = nanoid()
await db.insert(eventQueue).values({
id,
event: item.event,
status: item.status,
createdAt: Date.now(),
})
return id
}
export async function removeFromEventQueue(id: string) {
await db.delete(eventQueue).where(eq(eventQueue.id, id))
}
export async function clearEventQueue() {
await db.delete(eventQueue)
}
export async function saveEventQueue(queue: { id?: string, event: SatoriEvent, status: 'pending' | 'ready' }[]) {
// If we have IDs, we might be able to do something smarter, but for now let's just keep it as is
// but optimized for the common case where we might want to just replace all.
// Actually, the best way to handle this is to NOT use saveEventQueue for single items.
await db.delete(eventQueue)
if (queue.length > 0) {
await db.insert(eventQueue).values(queue.map(item => ({
id: item.id || nanoid(),
event: item.event,
status: item.status,
createdAt: Date.now(),
})))
}
export async function listChannels() {
return await db.select().from(channels)
}
export async function loadEventQueue() {
const result = await db.select().from(eventQueue).orderBy(schema.eventQueue.createdAt)
return result.map(r => ({
id: r.id,
event: r.event as SatoriEvent,
id: r.id,
status: r.status as 'pending' | 'ready',
}))
}
// Unread Events Persistence
export async function pushToUnreadEvents(channelId: string, event: SatoriEvent) {
const id = nanoid()
await db.insert(unreadEvents).values({
id,
channelId,
event,
createdAt: Date.now(),
})
return id
}
export async function deleteUnreadEventsByIds(channelId: string, ids: string[]) {
if (ids.length === 0)
return
await db.delete(unreadEvents).where(inArray(unreadEvents.id, ids))
}
export async function clearUnreadEventsForChannel(channelId: string) {
await db.delete(unreadEvents).where(eq(unreadEvents.channelId, channelId))
}
export async function saveUnreadEvents(allUnread: Record<string, StoredUnreadEvent[]>) {
await db.delete(unreadEvents)
const values = []
for (const [channelId, events] of Object.entries(allUnread)) {
for (const item of events) {
values.push({
id: item.id || nanoid(),
channelId,
event: item.event,
createdAt: Date.now(),
})
}
}
if (values.length > 0) {
await db.insert(unreadEvents).values(values)
}
}
export async function loadUnreadEvents() {
const result = await db.select().from(unreadEvents).orderBy(schema.unreadEvents.createdAt)
const allUnread: Record<string, StoredUnreadEvent[]> = {}
@@ -162,9 +78,93 @@ export async function loadUnreadEvents() {
allUnread[r.channelId] = []
}
allUnread[r.channelId].push({
id: r.id,
event: r.event as SatoriEvent,
id: r.id,
})
}
return allUnread
}
export async function pushToEventQueue(item: { event: SatoriEvent, status: 'pending' | 'ready' }) {
const id = nanoid()
await db.insert(eventQueue).values({
createdAt: Date.now(),
event: item.event,
id,
status: item.status,
})
return id
}
export async function pushToUnreadEvents(channelId: string, event: SatoriEvent) {
const id = nanoid()
await db.insert(unreadEvents).values({
channelId,
createdAt: Date.now(),
event,
id,
})
return id
}
// Unread Events Persistence
export async function recordChannel(id: string, name: string, platform: string, selfId: string) {
await db.insert(channels)
.values({ id, name, platform, selfId })
.onConflictDoUpdate({
set: { name, platform, selfId },
target: channels.id,
})
}
export async function recordMessage(channelId: string, userId: string, userName: string, content: string, timestamp?: number) {
const ts = timestamp || Date.now()
const id = nanoid()
await db.insert(messages).values({
channelId,
content,
id,
timestamp: ts,
userId,
userName,
})
}
export async function removeFromEventQueue(id: string) {
await db.delete(eventQueue).where(eq(eventQueue.id, id))
}
export async function saveEventQueue(queue: { event: SatoriEvent, id?: string, status: 'pending' | 'ready' }[]) {
// If we have IDs, we might be able to do something smarter, but for now let's just keep it as is
// but optimized for the common case where we might want to just replace all.
// Actually, the best way to handle this is to NOT use saveEventQueue for single items.
await db.delete(eventQueue)
if (queue.length > 0) {
await db.insert(eventQueue).values(queue.map(item => ({
createdAt: Date.now(),
event: item.event,
id: item.id || nanoid(),
status: item.status,
})))
}
}
export async function saveUnreadEvents(allUnread: Record<string, StoredUnreadEvent[]>) {
await db.delete(unreadEvents)
const values = []
for (const [channelId, events] of Object.entries(allUnread)) {
for (const item of events) {
values.push({
channelId,
createdAt: Date.now(),
event: item.event,
id: item.id || nanoid(),
})
}
}
if (values.length > 0) {
await db.insert(unreadEvents).values(values)
}
}
+8 -8
View File
@@ -8,12 +8,12 @@ export const channels = pgTable('channels', {
})
export const messages = pgTable('messages', {
id: text('id').primaryKey(),
channelId: text('channel_id').notNull(),
content: text('content').notNull(),
id: text('id').primaryKey(),
timestamp: bigint('timestamp', { mode: 'number' }).notNull(),
userId: text('user_id').notNull(),
userName: text('user_name').notNull(),
content: text('content').notNull(),
timestamp: bigint('timestamp', { mode: 'number' }).notNull(),
}, (table) => {
return [
index('channel_timestamp_idx').on(table.channelId, table.timestamp),
@@ -21,15 +21,15 @@ export const messages = pgTable('messages', {
})
export const eventQueue = pgTable('event_queue', {
id: text('id').primaryKey(),
event: json('event').notNull(),
status: text('status').notNull(), // 'pending' | 'ready'
createdAt: bigint('created_at', { mode: 'number' }).notNull(),
event: json('event').notNull(),
id: text('id').primaryKey(),
status: text('status').notNull(), // 'pending' | 'ready'
})
export const unreadEvents = pgTable('unread_events', {
id: text('id').primaryKey(),
channelId: text('channel_id').notNull(),
event: json('event').notNull(),
createdAt: bigint('created_at', { mode: 'number' }).notNull(),
event: json('event').notNull(),
id: text('id').primaryKey(),
})