mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: basic api server
This commit is contained in:
@@ -4,10 +4,24 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "bun run --env-file=../../.env --watch src/index.ts",
|
"dev": "bun run --env-file=../../.env --watch src/index.ts",
|
||||||
"build": "bun build src/index.ts --outfile dist/index.js --target bun --minify",
|
"build": "bun build src/index.ts --outfile dist/index.js --target bun --minify",
|
||||||
"start": "bun run dist/index.js"
|
"start": "bun run dist/index.js",
|
||||||
|
"test": "vitest"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./client": "./src/client.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"elysia": "latest"
|
"@elysiajs/cors": "^1.4.1",
|
||||||
|
"@elysiajs/cron": "^1.4.1",
|
||||||
|
"@elysiajs/eden": "^1.4.6",
|
||||||
|
"@memohome/agent": "workspace:*",
|
||||||
|
"@memohome/db": "workspace:*",
|
||||||
|
"@memohome/memory": "workspace:*",
|
||||||
|
"@memohome/shared": "workspace:*",
|
||||||
|
"drizzle-orm": "^0.45.1",
|
||||||
|
"elysia": "latest",
|
||||||
|
"node-cron": "^4.2.1",
|
||||||
|
"zod": "^4.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"bun-types": "latest"
|
"bun-types": "latest"
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { app } from './index'
|
||||||
|
import { treaty } from '@elysiajs/eden'
|
||||||
|
|
||||||
|
export type ApiClient = typeof app
|
||||||
|
|
||||||
|
export const createClient = (
|
||||||
|
baseUrl: string = process.env.API_BASE_URL ?? 'http://localhost:7002'
|
||||||
|
) => {
|
||||||
|
return treaty<ApiClient>(baseUrl)
|
||||||
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
import { Elysia } from 'elysia'
|
import { Elysia } from 'elysia'
|
||||||
|
import { corsMiddleware } from './middlewares'
|
||||||
|
import { agentModule, modelModule, settingsModule } from './modules'
|
||||||
|
import { memoryModule } from './modules/memory'
|
||||||
|
|
||||||
const port = process.env.API_SERVER_PORT || 7002
|
const port = process.env.API_SERVER_PORT || 7002
|
||||||
|
|
||||||
const app = new Elysia().get('/', () => 'Hello Elysia').listen(port)
|
export const app = new Elysia()
|
||||||
|
.use(corsMiddleware)
|
||||||
|
.use(agentModule)
|
||||||
|
.use(memoryModule)
|
||||||
|
.use(modelModule)
|
||||||
|
.use(settingsModule)
|
||||||
|
.listen(port)
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import cors from '@elysiajs/cors'
|
||||||
|
|
||||||
|
export const corsMiddleware = cors({
|
||||||
|
origin: '*',
|
||||||
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
||||||
|
allowedHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
exposeHeaders: ['Content-Type', 'Authorization'],
|
||||||
|
credentials: true,
|
||||||
|
})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './cors'
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import Elysia from 'elysia'
|
||||||
|
|
||||||
|
export const agentModule = new Elysia({
|
||||||
|
prefix: '/agent',
|
||||||
|
})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from './agent'
|
||||||
|
export * from './model'
|
||||||
|
export * from './settings'
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import Elysia from 'elysia'
|
||||||
|
import { messageModule } from './message'
|
||||||
|
import { AddMemoryModel, SearchMemoryModel } from './model'
|
||||||
|
import { addMemory, searchMemory } from './service'
|
||||||
|
import { MemoryUnit } from '@memohome/memory'
|
||||||
|
|
||||||
|
export const memoryModule = new Elysia({
|
||||||
|
prefix: '/memory',
|
||||||
|
})
|
||||||
|
.use(messageModule)
|
||||||
|
// Add memory
|
||||||
|
.post('/', async ({ body }) => {
|
||||||
|
try {
|
||||||
|
const result = await addMemory(body as unknown as MemoryUnit)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to add memory',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, AddMemoryModel)
|
||||||
|
// Search memory
|
||||||
|
.get('/search', async ({ query }) => {
|
||||||
|
try {
|
||||||
|
const results = await searchMemory(query.query, query.userId)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: results,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to search memory',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, SearchMemoryModel)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import Elysia from 'elysia'
|
||||||
|
import { GetMemoryMessageFilterModel, GetMemoryMessageModel } from './model'
|
||||||
|
import { getMemoryMessages, getMemoryMessagesFilter } from './service'
|
||||||
|
|
||||||
|
export const messageModule = new Elysia({
|
||||||
|
prefix: '/message',
|
||||||
|
})
|
||||||
|
.get('/', async ({ query }) => {
|
||||||
|
const units = await getMemoryMessages(query)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
units,
|
||||||
|
}
|
||||||
|
}, GetMemoryMessageModel)
|
||||||
|
.get('/filter', async ({ query }) => {
|
||||||
|
const units = await getMemoryMessagesFilter(query)
|
||||||
|
|
||||||
|
return {
|
||||||
|
units,
|
||||||
|
success: true,
|
||||||
|
}
|
||||||
|
}, GetMemoryMessageFilterModel)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
export const GetMemoryMessageModel = {
|
||||||
|
query: z.object({
|
||||||
|
limit: z.string().transform(Number).default(10),
|
||||||
|
page: z.string().transform(Number).default(1),
|
||||||
|
userId: z.string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetMemoryMessageFilterModel = {
|
||||||
|
query: z.object({
|
||||||
|
from: z.date(),
|
||||||
|
to: z.date(),
|
||||||
|
userId: z.string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { db } from '@memohome/db'
|
||||||
|
import { history } from '@memohome/db/schema'
|
||||||
|
import { eq, desc, and, gte, lte, asc } from 'drizzle-orm'
|
||||||
|
|
||||||
|
export const getMemoryMessages = async (
|
||||||
|
query: {
|
||||||
|
limit: number
|
||||||
|
page: number
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { limit, page, userId } = query
|
||||||
|
const results = await db
|
||||||
|
.select()
|
||||||
|
.from(history)
|
||||||
|
.where(eq(history.user, userId))
|
||||||
|
.orderBy(desc(history.timestamp))
|
||||||
|
.limit(limit)
|
||||||
|
.offset((page - 1) * limit)
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMemoryMessagesFilter = async (
|
||||||
|
query: {
|
||||||
|
from: Date
|
||||||
|
to: Date
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { from, to, userId } = query
|
||||||
|
const results = await db
|
||||||
|
.select()
|
||||||
|
.from(history)
|
||||||
|
.where(and(
|
||||||
|
gte(history.timestamp, from),
|
||||||
|
lte(history.timestamp, to),
|
||||||
|
eq(history.user, userId),
|
||||||
|
))
|
||||||
|
.orderBy(asc(history.timestamp))
|
||||||
|
|
||||||
|
return results
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
// MemoryUnit schema
|
||||||
|
const MemoryUnitSchema = z.object({
|
||||||
|
messages: z.array(z.object()),
|
||||||
|
timestamp: z.coerce.date(),
|
||||||
|
user: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AddMemoryModel = {
|
||||||
|
body: MemoryUnitSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchMemoryModel = {
|
||||||
|
query: z.object({
|
||||||
|
query: z.string().min(1, 'Search query is required'),
|
||||||
|
userId: z.string().min(1, 'User ID is required'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { createMemory, MemoryUnit } from '@memohome/memory'
|
||||||
|
import { getEmbeddingModel, getSummaryModel } from '@/modules/model/service'
|
||||||
|
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||||
|
|
||||||
|
export const addMemory = async (memoryUnit: MemoryUnit) => {
|
||||||
|
const [embeddingModel, summaryModel] = await Promise.all([
|
||||||
|
getEmbeddingModel(memoryUnit.user),
|
||||||
|
getSummaryModel(memoryUnit.user),
|
||||||
|
])
|
||||||
|
if (!embeddingModel || !summaryModel) {
|
||||||
|
throw new Error('Embedding or summary model not found')
|
||||||
|
}
|
||||||
|
const { addMemory } = createMemory({
|
||||||
|
summaryModel: summaryModel.model as ChatModel,
|
||||||
|
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||||
|
})
|
||||||
|
await addMemory(memoryUnit)
|
||||||
|
return memoryUnit
|
||||||
|
}
|
||||||
|
|
||||||
|
export const searchMemory = async (query: string, userId: string) => {
|
||||||
|
const [embeddingModel, summaryModel] = await Promise.all([
|
||||||
|
getEmbeddingModel(userId),
|
||||||
|
getSummaryModel(userId),
|
||||||
|
])
|
||||||
|
if (!embeddingModel || !summaryModel) {
|
||||||
|
throw new Error('Embedding or summary model not found')
|
||||||
|
}
|
||||||
|
const { searchMemory } = createMemory({
|
||||||
|
summaryModel: summaryModel.model as ChatModel,
|
||||||
|
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||||
|
})
|
||||||
|
const results = await searchMemory(query, userId)
|
||||||
|
return results
|
||||||
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
import Elysia from 'elysia'
|
||||||
|
import {
|
||||||
|
CreateModelModel,
|
||||||
|
UpdateModelModel,
|
||||||
|
GetModelByIdModel,
|
||||||
|
DeleteModelModel,
|
||||||
|
GetDefaultModelModel,
|
||||||
|
} from './model'
|
||||||
|
import {
|
||||||
|
getModels,
|
||||||
|
getModelById,
|
||||||
|
createModel,
|
||||||
|
updateModel,
|
||||||
|
deleteModel,
|
||||||
|
getChatModel,
|
||||||
|
getSummaryModel,
|
||||||
|
getEmbeddingModel,
|
||||||
|
} from './service'
|
||||||
|
import { Model } from '@memohome/shared'
|
||||||
|
|
||||||
|
export const modelModule = new Elysia({
|
||||||
|
prefix: '/model',
|
||||||
|
})
|
||||||
|
// Get all models
|
||||||
|
.get('/', async () => {
|
||||||
|
try {
|
||||||
|
const models = await getModels()
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: models,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch models',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// Get model by ID
|
||||||
|
.get('/:id', async ({ params }) => {
|
||||||
|
try {
|
||||||
|
const { id } = params
|
||||||
|
const model = await getModelById(id)
|
||||||
|
if (!model) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Model not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: model,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch model',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, GetModelByIdModel)
|
||||||
|
// Create new model
|
||||||
|
.post('/', async ({ body }) => {
|
||||||
|
try {
|
||||||
|
const newModel = await createModel(body as Model)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: newModel,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to create model',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, CreateModelModel)
|
||||||
|
// Update model
|
||||||
|
.put('/:id', async ({ params, body }) => {
|
||||||
|
try {
|
||||||
|
const { id } = params
|
||||||
|
const updatedModel = await updateModel(id, body as Model)
|
||||||
|
if (!updatedModel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Model not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: updatedModel,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to update model',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, UpdateModelModel)
|
||||||
|
// Delete model
|
||||||
|
.delete('/:id', async ({ params }) => {
|
||||||
|
try {
|
||||||
|
const { id } = params
|
||||||
|
const deletedModel = await deleteModel(id)
|
||||||
|
if (!deletedModel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Model not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: deletedModel,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to delete model',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, DeleteModelModel)
|
||||||
|
// Get default chat model
|
||||||
|
.get('/chat/default', async ({ query }) => {
|
||||||
|
try {
|
||||||
|
const { userId } = query
|
||||||
|
const chatModel = await getChatModel(userId)
|
||||||
|
if (!chatModel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Default chat model not found or not set',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: chatModel,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch default chat model',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, GetDefaultModelModel)
|
||||||
|
// Get default summary model
|
||||||
|
.get('/summary/default', async ({ query }) => {
|
||||||
|
try {
|
||||||
|
const { userId } = query
|
||||||
|
const summaryModel = await getSummaryModel(userId)
|
||||||
|
if (!summaryModel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Default summary model not found or not set',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: summaryModel,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch default summary model',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, GetDefaultModelModel)
|
||||||
|
// Get default embedding model
|
||||||
|
.get('/embedding/default', async ({ query }) => {
|
||||||
|
try {
|
||||||
|
const { userId } = query
|
||||||
|
const embeddingModel = await getEmbeddingModel(userId)
|
||||||
|
if (!embeddingModel) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Default embedding model not found or not set',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: embeddingModel,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch default embedding model',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, GetDefaultModelModel)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const BaseModelSchema = z.object({
|
||||||
|
modelId: z.string().min(1, 'Model ID is required'),
|
||||||
|
baseUrl: z.string(),
|
||||||
|
apiKey: z.string().min(1, 'API key is required'),
|
||||||
|
clientType: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Chat model schema (type is optional and defaults to 'chat')
|
||||||
|
const ChatModelSchema = BaseModelSchema.extend({
|
||||||
|
type: z.enum(['chat']).optional().default('chat'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Embedding model schema (type must be 'embedding' and dimensions is required)
|
||||||
|
const EmbeddingModelSchema = BaseModelSchema.extend({
|
||||||
|
type: z.literal('embedding'),
|
||||||
|
dimensions: z.number().int().positive('Dimensions must be a positive integer'),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Union of both model types
|
||||||
|
const ModelSchema = z.union([ChatModelSchema, EmbeddingModelSchema])
|
||||||
|
|
||||||
|
// Export the inferred type from the schema
|
||||||
|
export type ModelInput = z.infer<typeof ModelSchema>
|
||||||
|
|
||||||
|
export const CreateModelModel = {
|
||||||
|
body: ModelSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateModelModel = {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
body: ModelSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetModelByIdModel = {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DeleteModelModel = {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GetDefaultModelModel = {
|
||||||
|
query: z.object({
|
||||||
|
userId: z.string().min(1, 'User ID is required'),
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { db } from '@memohome/db'
|
||||||
|
import { model } from '@memohome/db/schema'
|
||||||
|
import { Model } from '@memohome/shared'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import { getSettings } from '@/modules/settings/service'
|
||||||
|
|
||||||
|
export const getModels = async () => {
|
||||||
|
const models = await db.select().from(model)
|
||||||
|
return models
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getModelById = async (id: string) => {
|
||||||
|
const [result] = await db.select().from(model).where(eq(model.id, id))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createModel = async (data: Model) => {
|
||||||
|
const [newModel] = await db
|
||||||
|
.insert(model)
|
||||||
|
.values({ model: data })
|
||||||
|
.returning()
|
||||||
|
return newModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateModel = async (id: string, data: Model) => {
|
||||||
|
const [updatedModel] = await db
|
||||||
|
.update(model)
|
||||||
|
.set({ model: data })
|
||||||
|
.where(eq(model.id, id))
|
||||||
|
.returning()
|
||||||
|
return updatedModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteModel = async (id: string) => {
|
||||||
|
const [deletedModel] = await db
|
||||||
|
.delete(model)
|
||||||
|
.where(eq(model.id, id))
|
||||||
|
.returning()
|
||||||
|
return deletedModel
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getChatModel = async (userId: string) => {
|
||||||
|
const userSettings = await getSettings(userId)
|
||||||
|
if (!userSettings?.defaultChatModel) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return await getModelById(userSettings.defaultChatModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getSummaryModel = async (userId: string) => {
|
||||||
|
const userSettings = await getSettings(userId)
|
||||||
|
if (!userSettings?.defaultSummaryModel) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return await getModelById(userSettings.defaultSummaryModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getEmbeddingModel = async (userId: string) => {
|
||||||
|
const userSettings = await getSettings(userId)
|
||||||
|
if (!userSettings?.defaultEmbeddingModel) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return await getModelById(userSettings.defaultEmbeddingModel)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import Elysia from 'elysia'
|
||||||
|
import {
|
||||||
|
GetSettingsModel,
|
||||||
|
CreateSettingsModel,
|
||||||
|
UpdateSettingsModel,
|
||||||
|
} from './model'
|
||||||
|
import {
|
||||||
|
getSettings,
|
||||||
|
createSettings,
|
||||||
|
updateSettings,
|
||||||
|
upsertSettings,
|
||||||
|
} from './service'
|
||||||
|
|
||||||
|
export const settingsModule = new Elysia({
|
||||||
|
prefix: '/settings',
|
||||||
|
})
|
||||||
|
// Get user settings
|
||||||
|
.get('/:userId', async ({ params }) => {
|
||||||
|
try {
|
||||||
|
const { userId } = params
|
||||||
|
const userSettings = await getSettings(userId)
|
||||||
|
if (!userSettings) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Settings not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: userSettings,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to fetch settings',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, GetSettingsModel)
|
||||||
|
// Create new settings
|
||||||
|
.post('/', async ({ body }) => {
|
||||||
|
try {
|
||||||
|
const newSettings = await createSettings(body)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: newSettings,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to create settings',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, CreateSettingsModel)
|
||||||
|
// Update settings
|
||||||
|
.put('/:userId', async ({ params, body }) => {
|
||||||
|
try {
|
||||||
|
const { userId } = params
|
||||||
|
const updatedSettings = await updateSettings(userId, body)
|
||||||
|
if (!updatedSettings) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Settings not found',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: updatedSettings,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to update settings',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, UpdateSettingsModel)
|
||||||
|
// Upsert settings (create or update)
|
||||||
|
.patch('/', async ({ body }) => {
|
||||||
|
try {
|
||||||
|
const result = await upsertSettings(body)
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error.message : 'Failed to upsert settings',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, CreateSettingsModel)
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const SettingsSchema = z.object({
|
||||||
|
userId: z.string().min(1, 'User ID is required'),
|
||||||
|
defaultChatModel: z.string().uuid().nullable().optional(),
|
||||||
|
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
|
||||||
|
defaultSummaryModel: z.string().uuid().nullable().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SettingsInput = z.infer<typeof SettingsSchema>
|
||||||
|
|
||||||
|
export const GetSettingsModel = {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreateSettingsModel = {
|
||||||
|
body: SettingsSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UpdateSettingsModel = {
|
||||||
|
params: z.object({
|
||||||
|
userId: z.string(),
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
defaultChatModel: z.string().uuid().nullable().optional(),
|
||||||
|
defaultEmbeddingModel: z.string().uuid().nullable().optional(),
|
||||||
|
defaultSummaryModel: z.string().uuid().nullable().optional(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { db } from '@memohome/db'
|
||||||
|
import { settings } from '@memohome/db/schema'
|
||||||
|
import { eq } from 'drizzle-orm'
|
||||||
|
import type { SettingsInput } from './model'
|
||||||
|
|
||||||
|
export const getSettings = async (userId: string) => {
|
||||||
|
const [result] = await db
|
||||||
|
.select()
|
||||||
|
.from(settings)
|
||||||
|
.where(eq(settings.userId, userId))
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSettings = async (data: SettingsInput) => {
|
||||||
|
const [newSettings] = await db
|
||||||
|
.insert(settings)
|
||||||
|
.values({
|
||||||
|
userId: data.userId,
|
||||||
|
defaultChatModel: data.defaultChatModel || null,
|
||||||
|
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
|
||||||
|
defaultSummaryModel: data.defaultSummaryModel || null,
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
return newSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSettings = async (
|
||||||
|
userId: string,
|
||||||
|
data: Partial<Omit<SettingsInput, 'userId'>>
|
||||||
|
) => {
|
||||||
|
const [updatedSettings] = await db
|
||||||
|
.update(settings)
|
||||||
|
.set({
|
||||||
|
defaultChatModel: data.defaultChatModel,
|
||||||
|
defaultEmbeddingModel: data.defaultEmbeddingModel,
|
||||||
|
defaultSummaryModel: data.defaultSummaryModel,
|
||||||
|
})
|
||||||
|
.where(eq(settings.userId, userId))
|
||||||
|
.returning()
|
||||||
|
return updatedSettings
|
||||||
|
}
|
||||||
|
|
||||||
|
export const upsertSettings = async (data: SettingsInput) => {
|
||||||
|
const [result] = await db
|
||||||
|
.insert(settings)
|
||||||
|
.values({
|
||||||
|
userId: data.userId,
|
||||||
|
defaultChatModel: data.defaultChatModel || null,
|
||||||
|
defaultEmbeddingModel: data.defaultEmbeddingModel || null,
|
||||||
|
defaultSummaryModel: data.defaultSummaryModel || null,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: settings.userId,
|
||||||
|
set: {
|
||||||
|
defaultChatModel: data.defaultChatModel,
|
||||||
|
defaultEmbeddingModel: data.defaultEmbeddingModel,
|
||||||
|
defaultSummaryModel: data.defaultSummaryModel,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.returning()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# API Tests
|
||||||
|
|
||||||
|
这个目录包含了使用 Elysia Eden Client 编写的 API 测试。
|
||||||
|
|
||||||
|
## 测试文件
|
||||||
|
|
||||||
|
- `setup.ts` - 测试设置文件,配置测试服务器和 eden client
|
||||||
|
- `memory.test.ts` - Memory API 测试
|
||||||
|
- `memory-message.test.ts` - Memory Message API 测试
|
||||||
|
- `model.test.ts` - Model API 测试
|
||||||
|
- `settings.test.ts` - Settings API 测试
|
||||||
|
|
||||||
|
## 运行测试
|
||||||
|
|
||||||
|
从项目根目录运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
或者只运行 API 包的测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd packages/api
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试说明
|
||||||
|
|
||||||
|
测试使用 vitest 作为测试框架,并使用 Elysia Eden Client (treaty) 来测试 API 端点。
|
||||||
|
|
||||||
|
测试服务器会在测试开始前启动(端口 7003),测试结束后自动关闭。
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 确保数据库已配置并运行(某些测试可能需要数据库连接)
|
||||||
|
- 测试使用独立的测试端口(7003)以避免与开发服务器冲突
|
||||||
|
- 某些测试可能需要先创建数据才能测试查询功能
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { getTestClient } from './setup'
|
||||||
|
|
||||||
|
describe('Memory Message API', () => {
|
||||||
|
const client = getTestClient()
|
||||||
|
|
||||||
|
describe('GET /memory/message', () => {
|
||||||
|
it('should get memory messages successfully', async () => {
|
||||||
|
const response = await client.memory.message.get({
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.units).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should use default limit and page when not provided', async () => {
|
||||||
|
const response = await client.memory.message.get({
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for missing userId', async () => {
|
||||||
|
const response = await client.memory.message.get({
|
||||||
|
// @ts-expect-error - Testing invalid input
|
||||||
|
query: {
|
||||||
|
limit: 10,
|
||||||
|
page: 1,
|
||||||
|
// missing userId
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /memory/message/filter', () => {
|
||||||
|
it('should filter memory messages successfully', async () => {
|
||||||
|
const response = await client.memory.message.filter.get({
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
from: new Date('2024-01-01') as unknown as string,
|
||||||
|
to: new Date('2024-12-31') as unknown as string,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.units).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for missing required fields', async () => {
|
||||||
|
const response = await client.memory.message.filter.get({
|
||||||
|
// @ts-expect-error - Testing invalid input
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
// missing from and to
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { getTestClient } from './setup'
|
||||||
|
|
||||||
|
describe('Memory API', () => {
|
||||||
|
const client = getTestClient()
|
||||||
|
|
||||||
|
describe('POST /memory', () => {
|
||||||
|
it('should add memory successfully', async () => {
|
||||||
|
const memoryData = {
|
||||||
|
messages: [
|
||||||
|
{ role: 'user', content: 'Hello, this is a test message' },
|
||||||
|
{ role: 'assistant', content: 'Hello! How can I help you?' },
|
||||||
|
],
|
||||||
|
timestamp: new Date(),
|
||||||
|
user: 'test-user-123',
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.memory.post(memoryData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
console.log(response.data?.error)
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for invalid memory data', async () => {
|
||||||
|
const invalidData = {
|
||||||
|
messages: [],
|
||||||
|
// missing timestamp and user
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const response = await client.memory.post(invalidData as any)
|
||||||
|
|
||||||
|
// Elysia 会返回 400 或 422 对于验证错误
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /memory/search', () => {
|
||||||
|
it('should search memory successfully', async () => {
|
||||||
|
const response = await client.memory.search.get({
|
||||||
|
query: {
|
||||||
|
query: 'test search',
|
||||||
|
userId: 'test-user-123',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(Array.isArray(response.data?.data)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for missing query', async () => {
|
||||||
|
const response = await client.memory.search.get({
|
||||||
|
// @ts-expect-error - Testing invalid input
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
// missing query
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for missing userId', async () => {
|
||||||
|
const response = await client.memory.search.get({
|
||||||
|
// @ts-expect-error - Testing invalid input
|
||||||
|
query: {
|
||||||
|
query: 'test search',
|
||||||
|
// missing userId
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { getTestClient } from './setup'
|
||||||
|
|
||||||
|
describe('Model API', () => {
|
||||||
|
const client = getTestClient()
|
||||||
|
let createdModelId: string | null = null
|
||||||
|
|
||||||
|
describe('GET /model', () => {
|
||||||
|
it('should get all models successfully', async () => {
|
||||||
|
const response = await client.model.get()
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(Array.isArray(response.data?.data)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('POST /model', () => {
|
||||||
|
it('should create a chat model successfully', async () => {
|
||||||
|
const modelData = {
|
||||||
|
modelId: 'test-chat-model',
|
||||||
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
clientType: 'openai',
|
||||||
|
name: 'Test Chat Model',
|
||||||
|
type: 'chat' as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.model.post(modelData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
|
||||||
|
if (response.data?.data?.id) {
|
||||||
|
createdModelId = response.data.data.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create an embedding model successfully', async () => {
|
||||||
|
const modelData = {
|
||||||
|
modelId: 'test-embedding-model',
|
||||||
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
clientType: 'openai',
|
||||||
|
name: 'Test Embedding Model',
|
||||||
|
type: 'embedding' as const,
|
||||||
|
dimensions: 1536,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.model.post(modelData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for invalid model data', async () => {
|
||||||
|
const invalidData = {
|
||||||
|
// missing required fields
|
||||||
|
name: 'Invalid Model',
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const response = await client.model.post(invalidData as any)
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for embedding model without dimensions', async () => {
|
||||||
|
const invalidData = {
|
||||||
|
modelId: 'test-embedding-model',
|
||||||
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
clientType: 'openai',
|
||||||
|
type: 'embedding' as const,
|
||||||
|
// missing dimensions
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const response = await client.model.post(invalidData as any)
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /model/:id', () => {
|
||||||
|
it('should get model by id successfully', async () => {
|
||||||
|
if (!createdModelId) {
|
||||||
|
// 先创建一个模型
|
||||||
|
const createResponse = await client.model.post({
|
||||||
|
modelId: 'test-get-model',
|
||||||
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
clientType: 'openai',
|
||||||
|
type: 'chat' as const,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (createResponse.data?.data?.id) {
|
||||||
|
createdModelId = createResponse.data.data.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createdModelId) {
|
||||||
|
const response = await client.model({
|
||||||
|
id: createdModelId,
|
||||||
|
}).get()
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for non-existent model', async () => {
|
||||||
|
const response = await client.model({
|
||||||
|
id: 'non-existent-id',
|
||||||
|
}).get()
|
||||||
|
|
||||||
|
expect(response.status).toBe(200) // API 返回 200 但 success: false
|
||||||
|
expect(response.data?.success).toBe(false)
|
||||||
|
expect(response.data?.error).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('PUT /model/:id', () => {
|
||||||
|
it('should update model successfully', async () => {
|
||||||
|
if (!createdModelId) {
|
||||||
|
const createResponse = await client.model.post({
|
||||||
|
modelId: 'test-update-model',
|
||||||
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
clientType: 'openai',
|
||||||
|
type: 'chat' as const,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (createResponse.data?.data?.id) {
|
||||||
|
createdModelId = createResponse.data.data.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createdModelId) {
|
||||||
|
const updateData = {
|
||||||
|
modelId: 'test-updated-model',
|
||||||
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'updated-api-key',
|
||||||
|
clientType: 'openai',
|
||||||
|
name: 'Updated Model',
|
||||||
|
type: 'chat' as const,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.model[createdModelId].put(updateData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DELETE /model/:id', () => {
|
||||||
|
it('should delete model successfully', async () => {
|
||||||
|
// 先创建一个模型用于删除
|
||||||
|
const createResponse = await client.model.post({
|
||||||
|
modelId: 'test-delete-model',
|
||||||
|
baseUrl: 'https://api.openai.com/v1',
|
||||||
|
apiKey: 'test-api-key',
|
||||||
|
clientType: 'openai',
|
||||||
|
type: 'chat' as const,
|
||||||
|
})
|
||||||
|
|
||||||
|
const modelId = createResponse.data?.data?.id
|
||||||
|
if (modelId) {
|
||||||
|
const response = await client.model[modelId].delete()
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /model/chat/default', () => {
|
||||||
|
it('should get default chat model successfully', async () => {
|
||||||
|
const response = await client.model.chat.default.get({
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
// 可能返回 success: false 如果没有设置默认模型
|
||||||
|
expect(response.data?.success !== undefined).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for missing userId', async () => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const response = await client.model.chat.default.get({ query: {} } as any)
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /model/summary/default', () => {
|
||||||
|
it('should get default summary model successfully', async () => {
|
||||||
|
const response = await client.model.summary.default.get({
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success !== undefined).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('GET /model/embedding/default', () => {
|
||||||
|
it('should get default embedding model successfully', async () => {
|
||||||
|
const response = await client.model.embedding.default.get({
|
||||||
|
query: {
|
||||||
|
userId: 'test-user-123',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success !== undefined).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import { getTestClient } from './setup'
|
||||||
|
|
||||||
|
describe('Settings API', () => {
|
||||||
|
const client = getTestClient()
|
||||||
|
const testUserId = 'test-user-settings-123'
|
||||||
|
|
||||||
|
describe('GET /settings/:userId', () => {
|
||||||
|
it('should get settings successfully', async () => {
|
||||||
|
const response = await client.settings({
|
||||||
|
userId: testUserId,
|
||||||
|
}).get()
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
// 可能返回 success: false 如果设置不存在
|
||||||
|
expect(response.data?.success !== undefined).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('POST /settings', () => {
|
||||||
|
it('should create settings successfully', async () => {
|
||||||
|
const settingsData = {
|
||||||
|
userId: testUserId,
|
||||||
|
defaultChatModel: null,
|
||||||
|
defaultEmbeddingModel: null,
|
||||||
|
defaultSummaryModel: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.settings.post(settingsData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should create settings with model IDs', async () => {
|
||||||
|
const settingsData = {
|
||||||
|
userId: `${testUserId}-with-models`,
|
||||||
|
defaultChatModel: '00000000-0000-0000-0000-000000000001',
|
||||||
|
defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002',
|
||||||
|
defaultSummaryModel: '00000000-0000-0000-0000-000000000003',
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.settings.post(settingsData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for invalid UUID format', async () => {
|
||||||
|
const invalidData = {
|
||||||
|
userId: testUserId,
|
||||||
|
defaultChatModel: 'invalid-uuid',
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const response = await client.settings.post(invalidData as any)
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for missing userId', async () => {
|
||||||
|
const invalidData = {
|
||||||
|
defaultChatModel: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const response = await client.settings.post(invalidData as any)
|
||||||
|
|
||||||
|
expect([400, 422]).toContain(response.status)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('PUT /settings/:userId', () => {
|
||||||
|
it('should update settings successfully', async () => {
|
||||||
|
// 先创建设置
|
||||||
|
await client.settings.post({
|
||||||
|
userId: `${testUserId}-update`,
|
||||||
|
defaultChatModel: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
defaultChatModel: '00000000-0000-0000-0000-000000000001',
|
||||||
|
defaultEmbeddingModel: '00000000-0000-0000-0000-000000000002',
|
||||||
|
defaultSummaryModel: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.settings({
|
||||||
|
userId: `${testUserId}-update`,
|
||||||
|
}).put(updateData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return error for non-existent settings', async () => {
|
||||||
|
const updateData = {
|
||||||
|
defaultChatModel: '00000000-0000-0000-0000-000000000001',
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.settings({
|
||||||
|
userId: 'non-existent-user',
|
||||||
|
}).put(updateData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200) // API 返回 200 但 success: false
|
||||||
|
expect(response.data?.success).toBe(false)
|
||||||
|
expect(response.data?.error).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('PATCH /settings', () => {
|
||||||
|
it('should upsert settings successfully', async () => {
|
||||||
|
const settingsData = {
|
||||||
|
userId: `${testUserId}-upsert`,
|
||||||
|
defaultChatModel: '00000000-0000-0000-0000-000000000001',
|
||||||
|
defaultEmbeddingModel: null,
|
||||||
|
defaultSummaryModel: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.settings.patch(settingsData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data).toBeDefined()
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
expect(response.data?.data).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update existing settings on upsert', async () => {
|
||||||
|
const userId = `${testUserId}-upsert-update`
|
||||||
|
|
||||||
|
// 先创建
|
||||||
|
await client.settings.post({
|
||||||
|
userId,
|
||||||
|
defaultChatModel: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 然后 upsert
|
||||||
|
const upsertData = {
|
||||||
|
userId,
|
||||||
|
defaultChatModel: '00000000-0000-0000-0000-000000000001',
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await client.settings.patch(upsertData)
|
||||||
|
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
expect(response.data?.success).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createClient } from '../src/client'
|
||||||
|
|
||||||
|
export const getTestClient = () => {
|
||||||
|
return createClient()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
/* Modules */
|
/* Modules */
|
||||||
"module": "ES2022", /* Specify what module code is generated. */
|
"module": "ES2022", /* Specify what module code is generated. */
|
||||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
"moduleResolution": "Bundler", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||||
@@ -98,6 +98,10 @@
|
|||||||
|
|
||||||
/* Completeness */
|
/* Completeness */
|
||||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||||
}
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "src/**/**/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"studio": "drizzle-kit studio"
|
"studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@memohome/shared": "workspace:*",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"drizzle-orm": "^0.45.1",
|
"drizzle-orm": "^0.45.1",
|
||||||
"pg": "^8.16.3"
|
"pg": "^8.16.3"
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Model } from '@memohome/shared'
|
||||||
|
import { jsonb, pgTable, uuid } from 'drizzle-orm/pg-core'
|
||||||
|
|
||||||
|
export const model = pgTable('model', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
model: jsonb('model').notNull().$type<Model>(),
|
||||||
|
})
|
||||||
@@ -1 +1,3 @@
|
|||||||
export * from './history'
|
export * from './history'
|
||||||
|
export * from './model'
|
||||||
|
export * from './settings'
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { pgTable, text, uuid } from 'drizzle-orm/pg-core'
|
||||||
|
import { model } from './model'
|
||||||
|
|
||||||
|
export const settings = pgTable('settings', {
|
||||||
|
userId: text('user_id').primaryKey(),
|
||||||
|
defaultChatModel: uuid('default_chat_model').references(() => model.id),
|
||||||
|
defaultEmbeddingModel: uuid('default_embedding_model').references(() => model.id),
|
||||||
|
defaultSummaryModel: uuid('default_summary_model').references(() => model.id),
|
||||||
|
})
|
||||||
Generated
+73
@@ -90,9 +90,39 @@ importers:
|
|||||||
|
|
||||||
packages/api:
|
packages/api:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@elysiajs/cors':
|
||||||
|
specifier: ^1.4.1
|
||||||
|
version: 1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||||
|
'@elysiajs/cron':
|
||||||
|
specifier: ^1.4.1
|
||||||
|
version: 1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||||
|
'@elysiajs/eden':
|
||||||
|
specifier: ^1.4.6
|
||||||
|
version: 1.4.6(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))
|
||||||
|
'@memohome/agent':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../agent
|
||||||
|
'@memohome/db':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../db
|
||||||
|
'@memohome/memory':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../memory
|
||||||
|
'@memohome/shared':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../shared
|
||||||
|
drizzle-orm:
|
||||||
|
specifier: ^0.45.1
|
||||||
|
version: 0.45.1(@cloudflare/workers-types@4.20260109.0)(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(bun-types@1.3.5)(pg@8.16.3)(sqlite3@5.1.7)
|
||||||
elysia:
|
elysia:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
version: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||||
|
node-cron:
|
||||||
|
specifier: ^4.2.1
|
||||||
|
version: 4.2.1
|
||||||
|
zod:
|
||||||
|
specifier: ^4.3.5
|
||||||
|
version: 4.3.5
|
||||||
devDependencies:
|
devDependencies:
|
||||||
bun-types:
|
bun-types:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
@@ -100,6 +130,9 @@ importers:
|
|||||||
|
|
||||||
packages/db:
|
packages/db:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
'@memohome/shared':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../shared
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.2.3
|
specifier: ^17.2.3
|
||||||
version: 17.2.3
|
version: 17.2.3
|
||||||
@@ -524,6 +557,21 @@ packages:
|
|||||||
'@drizzle-team/brocli@0.10.2':
|
'@drizzle-team/brocli@0.10.2':
|
||||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||||
|
|
||||||
|
'@elysiajs/cors@1.4.1':
|
||||||
|
resolution: {integrity: sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ==}
|
||||||
|
peerDependencies:
|
||||||
|
elysia: '>= 1.4.0'
|
||||||
|
|
||||||
|
'@elysiajs/cron@1.4.1':
|
||||||
|
resolution: {integrity: sha512-Y+jqXtMJ+m17QzNWlWc09ugd1Fn1Wh7lqE+y9qSW8eiQZEqsRADXWmbLuXRSRaSC5dkfOyRSiwNCp6n8L/yuqA==}
|
||||||
|
peerDependencies:
|
||||||
|
elysia: '>= 1.4.0'
|
||||||
|
|
||||||
|
'@elysiajs/eden@1.4.6':
|
||||||
|
resolution: {integrity: sha512-Tsa4NwXEWg/u73vWiYZQ3L5/ecgZSxqiEjYwpS+4qBKXeTZqZKl2hcgHJSVBL+InEDMi35Xugct7qyAXE5oM4Q==}
|
||||||
|
peerDependencies:
|
||||||
|
elysia: '>=1.4.19'
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
||||||
deprecated: 'Merged into tsx: https://tsx.is'
|
deprecated: 'Merged into tsx: https://tsx.is'
|
||||||
@@ -2129,6 +2177,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
croner@6.0.7:
|
||||||
|
resolution: {integrity: sha512-k3Xx3Rcclfr60Yx4TmvsF3Yscuiql8LSvYLaphTsaq5Hk8La4Z/udmUANMOTKpgGGroI2F6/XOr9cU9OFkYluQ==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@@ -3253,6 +3305,10 @@ packages:
|
|||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
|
node-cron@4.2.1:
|
||||||
|
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
node-domexception@1.0.0:
|
node-domexception@1.0.0:
|
||||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||||
engines: {node: '>=10.5.0'}
|
engines: {node: '>=10.5.0'}
|
||||||
@@ -4592,6 +4648,19 @@ snapshots:
|
|||||||
|
|
||||||
'@drizzle-team/brocli@0.10.2': {}
|
'@drizzle-team/brocli@0.10.2': {}
|
||||||
|
|
||||||
|
'@elysiajs/cors@1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@elysiajs/cron@1.4.1(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
croner: 6.0.7
|
||||||
|
elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||||
|
|
||||||
|
'@elysiajs/eden@1.4.6(elysia@1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3))':
|
||||||
|
dependencies:
|
||||||
|
elysia: 1.4.21(@sinclair/typebox@0.34.47)(exact-mirror@0.2.6(@sinclair/typebox@0.34.47))(file-type@21.3.0)(openapi-types@12.1.3)(typescript@5.9.3)
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.18.20
|
esbuild: 0.18.20
|
||||||
@@ -6139,6 +6208,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-what: 5.5.0
|
is-what: 5.5.0
|
||||||
|
|
||||||
|
croner@6.0.7: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
path-key: 3.1.1
|
path-key: 3.1.1
|
||||||
@@ -7253,6 +7324,8 @@ snapshots:
|
|||||||
|
|
||||||
node-addon-api@7.1.1: {}
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
|
node-cron@4.2.1: {}
|
||||||
|
|
||||||
node-domexception@1.0.0: {}
|
node-domexception@1.0.0: {}
|
||||||
|
|
||||||
node-fetch@2.7.0(encoding@0.1.13):
|
node-fetch@2.7.0(encoding@0.1.13):
|
||||||
|
|||||||
Reference in New Issue
Block a user