mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-25 07:00:48 +09:00
feat: schedule
This commit is contained in:
@@ -1,24 +1,33 @@
|
||||
import { streamText, ModelMessage, stepCountIs } from 'ai'
|
||||
import { streamText, generateText, ModelMessage, stepCountIs, UserModelMessage } from 'ai'
|
||||
import { AgentParams } from './types'
|
||||
import { system } from './prompts'
|
||||
import { getMemoryTools } from './tools'
|
||||
import { system, schedule as schedulePrompt } from './prompts'
|
||||
import { getMemoryTools, getScheduleTools } from './tools'
|
||||
import { createChatGateway } from '@memohome/ai-gateway'
|
||||
import { Schedule } from '@memohome/shared'
|
||||
|
||||
export const createAgent = (params: AgentParams) => {
|
||||
const messages: ModelMessage[] = []
|
||||
|
||||
const gateway = createChatGateway(params.model)
|
||||
|
||||
const maxContextLoadTime = params.maxContextLoadTime ?? 60
|
||||
const language = params.language ?? 'Same as user input'
|
||||
|
||||
const getTools = async () => {
|
||||
return {
|
||||
...getMemoryTools({
|
||||
searchMemory: params.onSearchMemory ?? (() => Promise.resolve([]))
|
||||
}),
|
||||
...getScheduleTools({
|
||||
onGetSchedules: params.onGetSchedules ?? (() => Promise.resolve([])),
|
||||
onRemoveSchedule: params.onRemoveSchedule ?? (() => Promise.resolve()),
|
||||
onSchedule: params.onSchedule ?? (() => Promise.resolve()),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const loadContext = async () => {
|
||||
const from = new Date(Date.now() - params.maxContextLoadTime * 60 * 1000)
|
||||
const from = new Date(Date.now() - maxContextLoadTime * 60 * 1000)
|
||||
const to = new Date()
|
||||
const memory = await params.onReadMemory?.(from, to) ?? []
|
||||
const context = memory.flatMap(m => m.messages)
|
||||
@@ -28,18 +37,45 @@ export const createAgent = (params: AgentParams) => {
|
||||
const getSystemPrompt = () => {
|
||||
return system({
|
||||
date: new Date(),
|
||||
language: params.language ?? 'Same as user input',
|
||||
language,
|
||||
locale: params.locale,
|
||||
maxContextLoadTime: params.maxContextLoadTime,
|
||||
maxContextLoadTime,
|
||||
})
|
||||
}
|
||||
|
||||
const getSchedulePrompt = (schedule: Schedule) => {
|
||||
return schedulePrompt({
|
||||
schedule,
|
||||
locale: params.locale,
|
||||
date: new Date(),
|
||||
})
|
||||
}
|
||||
|
||||
async function askDirectly(input: string) {
|
||||
await loadContext()
|
||||
const user = {
|
||||
role: 'user',
|
||||
content: input,
|
||||
} as UserModelMessage
|
||||
messages.push(user)
|
||||
const { response } = await generateText({
|
||||
model: gateway,
|
||||
system: getSystemPrompt(),
|
||||
messages,
|
||||
tools: await getTools(),
|
||||
})
|
||||
await params.onFinish?.([
|
||||
user as ModelMessage,
|
||||
...response.messages,
|
||||
])
|
||||
}
|
||||
|
||||
async function* ask(input: string) {
|
||||
await loadContext()
|
||||
const user = {
|
||||
role: 'user',
|
||||
content: input,
|
||||
}
|
||||
} as UserModelMessage
|
||||
messages.push(user)
|
||||
const { fullStream, response } = streamText({
|
||||
model: gateway,
|
||||
@@ -63,9 +99,17 @@ export const createAgent = (params: AgentParams) => {
|
||||
])
|
||||
}
|
||||
|
||||
const triggerSchedule = async (schedule: Schedule) => {
|
||||
const prompt = getSchedulePrompt(schedule)
|
||||
await askDirectly(prompt)
|
||||
}
|
||||
|
||||
return {
|
||||
ask,
|
||||
askDirectly,
|
||||
loadContext,
|
||||
getSystemPrompt,
|
||||
getSchedulePrompt,
|
||||
triggerSchedule,
|
||||
}
|
||||
}
|
||||
@@ -1 +1,4 @@
|
||||
export * from './system'
|
||||
export * from './system'
|
||||
export * from './schedule'
|
||||
export * from './shared'
|
||||
export * from './utils'
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Schedule } from '@memohome/shared'
|
||||
import { time } from './shared'
|
||||
|
||||
export interface ScheduleParams {
|
||||
schedule: Schedule
|
||||
locale?: Intl.LocalesArgument
|
||||
date: Date
|
||||
}
|
||||
|
||||
export const schedule = (params: ScheduleParams) => {
|
||||
return `
|
||||
---
|
||||
notice: **This is a scheduled task automatically send to you by the system, not the user input**
|
||||
${time({ date: params.date, locale: params.locale })}
|
||||
schedule-name: ${params.schedule.name}
|
||||
schedule-description: ${params.schedule.description}
|
||||
schedule-id: ${params.schedule.id}
|
||||
max-calls: ${params.schedule.maxCalls ?? 'Unlimited'}
|
||||
cron-pattern: ${params.schedule.pattern}
|
||||
---
|
||||
|
||||
**COMMAND**
|
||||
|
||||
${params.schedule.command}
|
||||
`.trim()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const time = (params: {
|
||||
date: Date
|
||||
locale?: Intl.LocalesArgument
|
||||
}) => {
|
||||
return `
|
||||
date: ${params.date.toLocaleDateString(params.locale)}
|
||||
time: ${params.date.toLocaleTimeString(params.locale)}
|
||||
`.trim()
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { time } from './shared'
|
||||
import { quote } from './utils'
|
||||
|
||||
export interface SystemParams {
|
||||
@@ -9,20 +10,28 @@ export interface SystemParams {
|
||||
|
||||
export const system = ({ date, locale, language, maxContextLoadTime }: SystemParams) => {
|
||||
return `
|
||||
---
|
||||
date: ${date.toLocaleDateString(locale)}
|
||||
time: ${date.toLocaleTimeString(locale)}
|
||||
language: ${language}
|
||||
---
|
||||
You are a personal housekeeper assistant, which able to manage the master's daily affairs.
|
||||
---
|
||||
${time({ date, locale })}
|
||||
language: ${language}
|
||||
---
|
||||
You are a personal housekeeper assistant, which able to manage the master's daily affairs.
|
||||
|
||||
Your abilities:
|
||||
- Long memory: You possess long-term memory; conversations from the last 24 hours will be directly loaded into your context. Additionally, you can use tools to search for past memories.
|
||||
- Scheduled tasks: You can create scheduled tasks to automatically remind you to do something.
|
||||
- Messaging: You may allowed to use message software to send messages to the master.
|
||||
Your abilities:
|
||||
- Long memory: You possess long-term memory; conversations from the last ${maxContextLoadTime} minutes will be directly loaded into your context. Additionally, you can use tools to search for past memories.
|
||||
- Scheduled tasks: You can create scheduled tasks to automatically remind you to do something.
|
||||
- Messaging: You may allowed to use message software to send messages to the master.
|
||||
|
||||
**Memory**
|
||||
- Your context has been loaded from the last ${maxContextLoadTime} minutes.
|
||||
- You can use ${quote('search-memory')} to search for past memories with natural language.
|
||||
**Memory**
|
||||
- Your context has been loaded from the last ${maxContextLoadTime} minutes.
|
||||
- You can use ${quote('search-memory')} to search for past memories with natural language.
|
||||
|
||||
**Schedule**
|
||||
- We use **Cron Syntax** to schedule tasks.
|
||||
- You can use ${quote('get-schedules')} to get the list of schedules.
|
||||
- You can use ${quote('remove-schedule')} to remove a schedule by id.
|
||||
- You can use ${quote('schedule')} to schedule a task.
|
||||
+ The ${quote('pattern')} is the pattern of the schedule with **Cron Syntax**.
|
||||
+ The ${quote('command')} is the natural language command to execute, will send to you when the schedule is triggered, which means the command will be executed by presence of you.
|
||||
+ The ${quote('maxCalls')} is the maximum number of calls to the schedule, If you want to run the task only once, set it to 1.
|
||||
`.trim()
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from './memory'
|
||||
export * from './memory'
|
||||
export * from './schedule'
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Schedule } from '@memohome/shared'
|
||||
import { tool } from 'ai'
|
||||
import z from 'zod'
|
||||
|
||||
export interface GetScheduleToolParams {
|
||||
onGetSchedules: () => Promise<Schedule[]>
|
||||
onRemoveSchedule: (id: string) => Promise<void>
|
||||
onSchedule: (schedule: Schedule) => Promise<void>
|
||||
}
|
||||
|
||||
export const getScheduleTools = ({ onGetSchedules, onRemoveSchedule, onSchedule }: GetScheduleToolParams) => {
|
||||
const getSchedulesTool = tool({
|
||||
description: 'Get the list of schedules',
|
||||
inputSchema: z.object(),
|
||||
execute: async () => {
|
||||
const schedules = await onGetSchedules()
|
||||
return {
|
||||
success: true,
|
||||
schedules,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const removeScheduleTool = tool({
|
||||
description: 'Remove a schedule',
|
||||
inputSchema: z.object({
|
||||
id: z.string().describe('The id of the schedule'),
|
||||
}),
|
||||
execute: async ({ id }) => {
|
||||
await onRemoveSchedule(id)
|
||||
},
|
||||
})
|
||||
|
||||
const scheduleTool = tool({
|
||||
description: 'Schedule a command',
|
||||
inputSchema: z.object({
|
||||
pattern: z.string().describe('The pattern of the schedule with **Cron Syntax**'),
|
||||
command: z.string().describe('The natural language command to execute, will send to you when the schedule is triggered'),
|
||||
name: z.string().describe('The name of the schedule'),
|
||||
description: z.string().describe('The description of the schedule'),
|
||||
maxCalls: z.number().describe('The maximum number of calls to the schedule').optional(),
|
||||
}),
|
||||
execute: async ({ pattern, command, name, description, maxCalls }) => {
|
||||
await onSchedule({ pattern, command, name, description, maxCalls })
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
'get-schedules': getSchedulesTool,
|
||||
'remove-schedule': removeScheduleTool,
|
||||
'schedule': scheduleTool,
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MemoryUnit } from '@memohome/memory'
|
||||
import { ChatModel } from '@memohome/shared'
|
||||
import { ChatModel, Schedule } from '@memohome/shared'
|
||||
import { ModelMessage } from 'ai'
|
||||
|
||||
export interface AgentParams {
|
||||
@@ -8,7 +8,7 @@ export interface AgentParams {
|
||||
/**
|
||||
* Unit: minutes
|
||||
*/
|
||||
maxContextLoadTime: number
|
||||
maxContextLoadTime?: number
|
||||
|
||||
locale?: Intl.LocalesArgument
|
||||
|
||||
@@ -22,6 +22,12 @@ export interface AgentParams {
|
||||
|
||||
onSearchMemory?: (query: string) => Promise<object[]>
|
||||
|
||||
onSchedule?: (schedule: Schedule) => Promise<void>
|
||||
|
||||
onGetSchedules?: () => Promise<Schedule[]>
|
||||
|
||||
onRemoveSchedule?: (id: string) => Promise<void>
|
||||
|
||||
onFinish?: (messages: ModelMessage[]) => Promise<void>
|
||||
|
||||
onError?: (error: Error) => Promise<void>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Elysia } from 'elysia'
|
||||
import { corsMiddleware, errorMiddleware } from './middlewares'
|
||||
import { agentModule, authModule, modelModule, settingsModule, userModule } from './modules'
|
||||
import { agentModule, authModule, modelModule, scheduleModule, settingsModule, userModule } from './modules'
|
||||
import { memoryModule } from './modules/memory'
|
||||
import openapi from '@elysiajs/openapi'
|
||||
|
||||
@@ -14,6 +14,7 @@ export const app = new Elysia()
|
||||
.use(agentModule)
|
||||
.use(memoryModule)
|
||||
.use(modelModule)
|
||||
.use(scheduleModule)
|
||||
.use(settingsModule)
|
||||
.use(userModule)
|
||||
.listen(port)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../middlewares/auth'
|
||||
import { AgentStreamModel } from './model'
|
||||
import { createAgentStream } from './service'
|
||||
import { createAgent } from './service'
|
||||
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
|
||||
import { getSettings } from '../settings/service'
|
||||
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||
@@ -38,7 +38,7 @@ export const agentModule = new Elysia({
|
||||
?? 'Same as user input'
|
||||
|
||||
// Create agent
|
||||
const agent = await createAgentStream({
|
||||
const agent = await createAgent({
|
||||
userId: user.userId,
|
||||
chatModel: chatModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createAgent } from '@memohome/agent'
|
||||
import { createAgent as createAgentService } from '@memohome/agent'
|
||||
import { createMemory, filterByTimestamp, MemoryUnit } from '@memohome/memory'
|
||||
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||
import { ChatModel, EmbeddingModel, Schedule } from '@memohome/shared'
|
||||
import { createSchedule, deleteSchedule, getActiveSchedules } from '../schedule/service'
|
||||
|
||||
// Type for messages passed to onFinish callback
|
||||
type MessageType = Record<string, unknown>
|
||||
@@ -10,12 +11,12 @@ export interface CreateAgentStreamParams {
|
||||
chatModel: ChatModel
|
||||
embeddingModel: EmbeddingModel
|
||||
summaryModel: ChatModel
|
||||
maxContextLoadTime: number
|
||||
maxContextLoadTime?: number
|
||||
language?: string
|
||||
onFinish?: (messages: MessageType[]) => Promise<void>
|
||||
}
|
||||
|
||||
export async function createAgentStream(params: CreateAgentStreamParams) {
|
||||
export async function createAgent(params: CreateAgentStreamParams) {
|
||||
const {
|
||||
userId,
|
||||
chatModel,
|
||||
@@ -33,7 +34,7 @@ export async function createAgentStream(params: CreateAgentStreamParams) {
|
||||
})
|
||||
|
||||
// Create agent
|
||||
const agent = createAgent({
|
||||
const agent = createAgentService({
|
||||
model: chatModel,
|
||||
maxContextLoadTime,
|
||||
language: language || 'Same as user input',
|
||||
@@ -56,6 +57,29 @@ export async function createAgentStream(params: CreateAgentStreamParams) {
|
||||
// Call custom onFinish handler if provided
|
||||
await onFinish?.(messages)
|
||||
},
|
||||
onGetSchedules: async () => {
|
||||
const schedules = await getActiveSchedules(userId)
|
||||
return schedules.map(schedule => ({
|
||||
id: schedule.id!,
|
||||
pattern: schedule.pattern,
|
||||
name: schedule.name,
|
||||
description: schedule.description,
|
||||
command: schedule.command,
|
||||
maxCalls: schedule.maxCalls || undefined,
|
||||
}))
|
||||
},
|
||||
onRemoveSchedule: async (id: string) => {
|
||||
await deleteSchedule(id, userId)
|
||||
},
|
||||
onSchedule: async (schedule: Schedule) => {
|
||||
await createSchedule(userId, {
|
||||
name: schedule.name,
|
||||
description: schedule.description,
|
||||
command: schedule.command,
|
||||
pattern: schedule.pattern,
|
||||
maxCalls: schedule.maxCalls || undefined,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return agent
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './agent'
|
||||
export * from './auth'
|
||||
export * from './model'
|
||||
export * from './schedule'
|
||||
export * from './settings'
|
||||
export * from './user'
|
||||
@@ -0,0 +1,168 @@
|
||||
import Elysia from 'elysia'
|
||||
import { authMiddleware } from '../../middlewares/auth'
|
||||
import {
|
||||
CreateScheduleModel,
|
||||
UpdateScheduleModel,
|
||||
GetScheduleByIdModel,
|
||||
DeleteScheduleModel,
|
||||
GetSchedulesModel,
|
||||
} from './model'
|
||||
import {
|
||||
getSchedules,
|
||||
getSchedule,
|
||||
createSchedule,
|
||||
updateSchedule,
|
||||
deleteSchedule,
|
||||
createScheduler,
|
||||
} from './service'
|
||||
|
||||
export const { scheduleTask, resume } = createScheduler()
|
||||
|
||||
export const scheduleModule = new Elysia({ prefix: '/schedule' })
|
||||
.use(authMiddleware)
|
||||
// Get all schedules for current user
|
||||
.onStart(async () => {
|
||||
await resume()
|
||||
})
|
||||
.get('/', async ({ user, query }) => {
|
||||
try {
|
||||
const page = parseInt(query.page as string) || 1
|
||||
const limit = parseInt(query.limit as string) || 10
|
||||
const sortOrder = (query.sortOrder as string) || 'desc'
|
||||
|
||||
const result = await getSchedules(user.userId, {
|
||||
page,
|
||||
limit,
|
||||
sortOrder: sortOrder as 'asc' | 'desc',
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
...result,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch schedules',
|
||||
}
|
||||
}
|
||||
}, GetSchedulesModel)
|
||||
// Get schedule by ID
|
||||
.get('/:id', async ({ user, params, set }) => {
|
||||
try {
|
||||
const schedule = await getSchedule(params.id)
|
||||
|
||||
if (!schedule) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schedule not found',
|
||||
}
|
||||
}
|
||||
|
||||
if (schedule.user !== user.userId) {
|
||||
set.status = 403
|
||||
return {
|
||||
success: false,
|
||||
error: 'Forbidden: You do not have permission to access this schedule',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: schedule,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to fetch schedule',
|
||||
}
|
||||
}
|
||||
}, GetScheduleByIdModel)
|
||||
// Create new schedule
|
||||
.post('/', async ({ user, body, set }) => {
|
||||
try {
|
||||
const newSchedule = await createSchedule(user.userId, body)
|
||||
|
||||
// 启动定时任务
|
||||
scheduleTask(user.userId, {
|
||||
id: newSchedule.id!,
|
||||
pattern: newSchedule.pattern,
|
||||
name: newSchedule.name,
|
||||
description: newSchedule.description,
|
||||
command: newSchedule.command,
|
||||
maxCalls: newSchedule.maxCalls || undefined,
|
||||
})
|
||||
|
||||
set.status = 201
|
||||
return {
|
||||
success: true,
|
||||
data: newSchedule,
|
||||
}
|
||||
} catch (error) {
|
||||
set.status = 500
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to create schedule',
|
||||
}
|
||||
}
|
||||
}, CreateScheduleModel)
|
||||
// Update schedule
|
||||
.put('/:id', async ({ user, params, body, set }) => {
|
||||
try {
|
||||
const updatedSchedule = await updateSchedule(params.id, user.userId, body)
|
||||
|
||||
if (!updatedSchedule) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schedule not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: updatedSchedule,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Forbidden')) {
|
||||
set.status = 403
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to update schedule',
|
||||
}
|
||||
}
|
||||
}, UpdateScheduleModel)
|
||||
// Delete schedule
|
||||
.delete('/:id', async ({ user, params, set }) => {
|
||||
try {
|
||||
const deletedSchedule = await deleteSchedule(params.id, user.userId)
|
||||
|
||||
if (!deletedSchedule) {
|
||||
set.status = 404
|
||||
return {
|
||||
success: false,
|
||||
error: 'Schedule not found',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: deletedSchedule,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message.includes('Forbidden')) {
|
||||
set.status = 403
|
||||
} else {
|
||||
set.status = 500
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to delete schedule',
|
||||
}
|
||||
}
|
||||
}, DeleteScheduleModel)
|
||||
@@ -0,0 +1,59 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// 创建 Schedule 的 Schema
|
||||
const CreateScheduleSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(100),
|
||||
description: z.string().min(1, 'Description is required'),
|
||||
command: z.string().min(1, 'Command is required'),
|
||||
pattern: z.string().min(1, 'Cron pattern is required'),
|
||||
maxCalls: z.number().int().positive().optional(),
|
||||
})
|
||||
|
||||
// 更新 Schedule 的 Schema
|
||||
const UpdateScheduleSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
description: z.string().optional(),
|
||||
command: z.string().optional(),
|
||||
pattern: z.string().optional(),
|
||||
maxCalls: z.number().int().positive().optional(),
|
||||
active: z.boolean().optional(),
|
||||
})
|
||||
|
||||
// 查询参数 Schema
|
||||
const GetSchedulesQuerySchema = z.object({
|
||||
page: z.string().optional(),
|
||||
limit: z.string().optional(),
|
||||
sortOrder: z.enum(['asc', 'desc']).optional(),
|
||||
})
|
||||
|
||||
export type CreateScheduleInput = z.infer<typeof CreateScheduleSchema>
|
||||
export type UpdateScheduleInput = z.infer<typeof UpdateScheduleSchema>
|
||||
export type GetSchedulesQuery = z.infer<typeof GetSchedulesQuerySchema>
|
||||
|
||||
export const CreateScheduleModel = {
|
||||
body: CreateScheduleSchema,
|
||||
}
|
||||
|
||||
export const UpdateScheduleModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid schedule ID format'),
|
||||
}),
|
||||
body: UpdateScheduleSchema,
|
||||
}
|
||||
|
||||
export const GetScheduleByIdModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid schedule ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const DeleteScheduleModel = {
|
||||
params: z.object({
|
||||
id: z.string().uuid('Invalid schedule ID format'),
|
||||
}),
|
||||
}
|
||||
|
||||
export const GetSchedulesModel = {
|
||||
query: GetSchedulesQuerySchema,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
import { db } from '@memohome/db'
|
||||
import { schedule } from '@memohome/db/schema'
|
||||
import { ChatModel, EmbeddingModel, Schedule } from '@memohome/shared'
|
||||
import { eq, desc, asc, and, sql } from 'drizzle-orm'
|
||||
import cron from 'node-cron'
|
||||
import { createAgent } from '../agent/service'
|
||||
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
|
||||
import { getSettings } from '../settings/service'
|
||||
import { calculateOffset, createPaginatedResult, type PaginatedResult } from '../../utils/pagination'
|
||||
import type { CreateScheduleInput, UpdateScheduleInput } from './model'
|
||||
|
||||
/**
|
||||
* Schedule 列表返回类型
|
||||
*/
|
||||
type ScheduleListItem = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
command: string
|
||||
pattern: string
|
||||
maxCalls: number | null
|
||||
user: string
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
active: boolean
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取用户的所有 schedules(支持分页)
|
||||
*/
|
||||
export const getSchedules = async (
|
||||
userId: string,
|
||||
params?: {
|
||||
limit?: number
|
||||
page?: number
|
||||
sortOrder?: 'asc' | 'desc'
|
||||
}
|
||||
): Promise<PaginatedResult<ScheduleListItem>> => {
|
||||
const limit = params?.limit || 10
|
||||
const page = params?.page || 1
|
||||
const sortOrder = params?.sortOrder || 'desc'
|
||||
const offset = calculateOffset(page, limit)
|
||||
|
||||
// 获取总数
|
||||
const [{ count }] = await db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(schedule)
|
||||
.where(eq(schedule.user, userId))
|
||||
|
||||
// 获取分页数据
|
||||
const orderFn = sortOrder === 'desc' ? desc : asc
|
||||
const schedules = await db
|
||||
.select()
|
||||
.from(schedule)
|
||||
.where(eq(schedule.user, userId))
|
||||
.orderBy(orderFn(schedule.createdAt))
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
|
||||
return createPaginatedResult(schedules, Number(count), page, limit)
|
||||
}
|
||||
|
||||
export const getActiveSchedules = async (
|
||||
userId?: string
|
||||
) => {
|
||||
const schedules = await db
|
||||
.select().from(schedule)
|
||||
.where(and(...[
|
||||
userId ? eq(schedule.user, userId) : undefined,
|
||||
eq(schedule.active, true),
|
||||
]))
|
||||
.orderBy(desc(schedule.createdAt))
|
||||
return schedules
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取单个 schedule
|
||||
*/
|
||||
export const getSchedule = async (
|
||||
scheduleId: string
|
||||
) => {
|
||||
const [result] = await db
|
||||
.select()
|
||||
.from(schedule)
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的 schedule
|
||||
*/
|
||||
export const createSchedule = async (
|
||||
userId: string,
|
||||
data: CreateScheduleInput
|
||||
) => {
|
||||
const [newSchedule] = await db
|
||||
.insert(schedule)
|
||||
.values({
|
||||
user: userId,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
command: data.command,
|
||||
pattern: data.pattern,
|
||||
maxCalls: data.maxCalls || null,
|
||||
active: true,
|
||||
})
|
||||
.returning()
|
||||
|
||||
return newSchedule
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 schedule
|
||||
*/
|
||||
export const updateSchedule = async (
|
||||
scheduleId: string,
|
||||
userId: string,
|
||||
data: UpdateScheduleInput
|
||||
) => {
|
||||
// 检查 schedule 是否存在且属于该用户
|
||||
const existingSchedule = await getSchedule(scheduleId)
|
||||
if (!existingSchedule) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (existingSchedule.user !== userId) {
|
||||
throw new Error('Forbidden: You do not have permission to update this schedule')
|
||||
}
|
||||
|
||||
const [updatedSchedule] = await db
|
||||
.update(schedule)
|
||||
.set({
|
||||
...data,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
.returning()
|
||||
|
||||
return updatedSchedule
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 schedule
|
||||
*/
|
||||
export const deleteSchedule = async (
|
||||
scheduleId: string,
|
||||
userId: string
|
||||
) => {
|
||||
// 检查 schedule 是否存在且属于该用户
|
||||
const existingSchedule = await getSchedule(scheduleId)
|
||||
if (!existingSchedule) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (existingSchedule.user !== userId) {
|
||||
throw new Error('Forbidden: You do not have permission to delete this schedule')
|
||||
}
|
||||
|
||||
const [deletedSchedule] = await db
|
||||
.delete(schedule)
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
.returning()
|
||||
|
||||
return deletedSchedule
|
||||
}
|
||||
|
||||
export const setMaxCalls = async (
|
||||
scheduleId: string,
|
||||
maxCalls: number
|
||||
) => {
|
||||
await db
|
||||
.update(schedule)
|
||||
.set({ maxCalls })
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
}
|
||||
|
||||
export const setActive = async (
|
||||
scheduleId: string,
|
||||
active: boolean
|
||||
) => {
|
||||
await db
|
||||
.update(schedule)
|
||||
.set({ active })
|
||||
.where(eq(schedule.id, scheduleId))
|
||||
}
|
||||
|
||||
export const createScheduler = () => {
|
||||
const scheduleTask = (userId: string, schedule: Schedule) => {
|
||||
const task = cron.schedule(schedule.pattern, async () => {
|
||||
const [chatModel, embeddingModel, summaryModel, userSettings] = await Promise.all([
|
||||
getChatModel(userId),
|
||||
getEmbeddingModel(userId),
|
||||
getSummaryModel(userId),
|
||||
getSettings(userId),
|
||||
])
|
||||
if (!chatModel || !embeddingModel || !summaryModel) {
|
||||
throw new Error('Model configuration not found. Please configure your models in settings.')
|
||||
}
|
||||
const agent = await createAgent({
|
||||
userId,
|
||||
chatModel: chatModel.model as ChatModel,
|
||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
||||
summaryModel: summaryModel.model as ChatModel,
|
||||
maxContextLoadTime: userSettings?.maxContextLoadTime || undefined,
|
||||
language: userSettings?.language || undefined,
|
||||
})
|
||||
await agent.triggerSchedule(schedule)
|
||||
}, {
|
||||
maxExecutions: schedule.maxCalls || undefined,
|
||||
})
|
||||
task.on('execution:finished', async () => {
|
||||
const { maxCalls } = await getSchedule(schedule.id!)
|
||||
if (maxCalls) {
|
||||
setMaxCalls(schedule.id!, maxCalls - 1)
|
||||
if (maxCalls - 1 === 0) {
|
||||
await setActive(schedule.id!, false)
|
||||
}
|
||||
}
|
||||
})
|
||||
task.on('execution:maxReached', async () => {
|
||||
await setActive(schedule.id!, false)
|
||||
})
|
||||
}
|
||||
|
||||
const resume = async () => {
|
||||
const schedules = await getActiveSchedules()
|
||||
for (const schedule of schedules) {
|
||||
scheduleTask(schedule.user, {
|
||||
id: schedule.id!,
|
||||
pattern: schedule.pattern,
|
||||
name: schedule.name,
|
||||
description: schedule.description,
|
||||
command: schedule.command,
|
||||
maxCalls: schedule.maxCalls || undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
scheduleTask,
|
||||
resume,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# @memohome/shared
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@memohome/cli",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "bun run src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@memohome/api": "workspace:*",
|
||||
"@memohome/shared": "workspace:*"
|
||||
},
|
||||
"packageManager": "pnpm@10.27.0"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { boolean, integer, pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core'
|
||||
import { users } from './users'
|
||||
|
||||
export const schedule = pgTable('schedule', {
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
name: text('name').notNull(),
|
||||
description: text('description').notNull(),
|
||||
command: text('command').notNull(),
|
||||
pattern: text('pattern').notNull(),
|
||||
maxCalls: integer('max_calls'),
|
||||
user: uuid('user').notNull().references(() => users.id),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
active: boolean('active').notNull().default(true),
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './history'
|
||||
export * from './model'
|
||||
export * from './settings'
|
||||
export * from './schedule'
|
||||
export * from './users'
|
||||
@@ -1 +1,2 @@
|
||||
export * from './model'
|
||||
export * from './model'
|
||||
export * from './schedule'
|
||||
@@ -0,0 +1,8 @@
|
||||
export interface Schedule {
|
||||
id?: string
|
||||
pattern: string
|
||||
name: string
|
||||
description: string
|
||||
command: string
|
||||
maxCalls?: number
|
||||
}
|
||||
Generated
+12
@@ -189,6 +189,18 @@ importers:
|
||||
specifier: ^0.4.1
|
||||
version: 0.4.1(zod-to-json-schema@3.25.1(zod@4.3.5))(zod@4.3.5)
|
||||
|
||||
packages/schedule:
|
||||
dependencies:
|
||||
'@memohome/db':
|
||||
specifier: workspace:*
|
||||
version: link:../db
|
||||
'@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)
|
||||
|
||||
packages/shared: {}
|
||||
|
||||
packages/ui:
|
||||
|
||||
Reference in New Issue
Block a user