feat(chat-ws): add versioned Eventa protocols (#2308)

Signed-off-by: RainbowBird <git@luoling.moe>
This commit is contained in:
RainbowBird
2026-08-20 12:24:47 +08:00
committed by GitHub
parent 702b4c862d
commit d22daf6b4c
29 changed files with 510 additions and 252 deletions
@@ -0,0 +1,26 @@
# @proj-airi/server-sdk-shared
Eventa contracts for the hosted chat WebSocket.
## Usage
```shell
ni @proj-airi/server-sdk-shared -D
pnpm i @proj-airi/server-sdk-shared -D
```
```typescript
import type { WireMessage } from '@proj-airi/server-sdk-shared'
import { newMessages, pullMessages, sendMessages } from '@proj-airi/server-sdk-shared'
```
The package uses Eventa `1.0.0-beta.15`. Its WebSocket adapter accepts beta.13
`id/type/payload` envelopes and sends these fields with current envelopes.
`/ws/chat` keeps query-token authentication for deployed clients. `/ws/v2/chat`
authenticates after the WebSocket opens.
## License
[MIT](../../../LICENSE)
@@ -0,0 +1,44 @@
{
"name": "@proj-airi/server-sdk-shared",
"type": "module",
"version": "0.11.3",
"private": true,
"description": "Eventa contracts for the hosted chat WebSocket",
"author": {
"name": "Moeru AI Project AIRI Team",
"email": "airi@moeru.ai",
"url": "https://github.com/moeru-ai"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/moeru-ai/airi.git",
"directory": "server/packages/server-sdk-shared"
},
"exports": {
".": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"./v2": {
"types": "./dist/v2.d.mts",
"default": "./dist/v2.mjs"
}
},
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"files": [
"README.md",
"dist",
"package.json"
],
"scripts": {
"dev": "pnpm run build",
"build": "tsdown",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@moeru/eventa": "catalog:",
"valibot": "catalog:"
}
}
@@ -0,0 +1,63 @@
import * as v from 'valibot'
const NonEmptyStringSchema = v.pipe(v.string(), v.minLength(1))
const SendMessageSchema = v.object({
id: NonEmptyStringSchema,
role: v.string(),
content: v.string(),
})
export const SendMessagesRequestSchema = v.object({
chatId: NonEmptyStringSchema,
messages: v.array(SendMessageSchema),
})
export const PullMessagesRequestSchema = v.object({
chatId: NonEmptyStringSchema,
afterSeq: v.pipe(v.number(), v.integer(), v.minValue(0)),
limit: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))),
})
export interface WireMessage {
id: string
chatId: string
senderId: string | null
role: 'system' | 'user' | 'assistant' | 'tool' | 'error'
content: string
seq: number
createdAt: number
updatedAt: number
}
export type MessageRole = WireMessage['role']
export type SendMessagesRequest = v.InferOutput<typeof SendMessagesRequestSchema>
export interface SendMessagesResponse {
seq: number
}
export type PullMessagesRequest = v.InferOutput<typeof PullMessagesRequestSchema>
export interface PullMessagesResponse {
messages: WireMessage[]
seq: number
}
export interface NewMessagesPayload {
chatId: string
messages: WireMessage[]
fromSeq: number
toSeq: number
}
/** Parses a `chat:send-messages` payload at the WebSocket boundary. */
export function parseSendMessagesRequest(request: unknown): SendMessagesRequest {
return v.parse(SendMessagesRequestSchema, request)
}
/** Parses a `chat:pull-messages` payload at the WebSocket boundary. */
export function parsePullMessagesRequest(request: unknown): PullMessagesRequest {
return v.parse(PullMessagesRequestSchema, request)
}
@@ -0,0 +1 @@
export * from './v2'
@@ -0,0 +1,23 @@
import type { NewMessagesPayload, PullMessagesRequest, PullMessagesResponse, SendMessagesRequest, SendMessagesResponse } from './chat'
import { defineInvokeEventa, defineOutboundEventa } from '@moeru/eventa'
export type {
MessageRole,
NewMessagesPayload,
PullMessagesRequest,
PullMessagesResponse,
SendMessagesRequest,
SendMessagesResponse,
WireMessage,
} from './chat'
export {
parsePullMessagesRequest,
parseSendMessagesRequest,
PullMessagesRequestSchema,
SendMessagesRequestSchema,
} from './chat'
export const sendMessages = defineInvokeEventa<SendMessagesResponse, SendMessagesRequest>('chat:send-messages')
export const pullMessages = defineInvokeEventa<PullMessagesResponse, PullMessagesRequest>('chat:pull-messages')
export const newMessages = defineOutboundEventa<NewMessagesPayload>('chat:new-messages')
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": [
"ESNext"
],
"module": "ESNext",
"moduleResolution": "bundler",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts"
]
}
@@ -0,0 +1,11 @@
import { defineConfig } from 'tsdown'
export default defineConfig({
entry: {
index: 'src/index.ts',
v2: 'src/v2.ts',
},
sourcemap: true,
unused: true,
inlineOnly: false,
})