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 { AgentParams } from './types'
|
||||||
import { system } from './prompts'
|
import { system, schedule as schedulePrompt } from './prompts'
|
||||||
import { getMemoryTools } from './tools'
|
import { getMemoryTools, getScheduleTools } from './tools'
|
||||||
import { createChatGateway } from '@memohome/ai-gateway'
|
import { createChatGateway } from '@memohome/ai-gateway'
|
||||||
|
import { Schedule } from '@memohome/shared'
|
||||||
|
|
||||||
export const createAgent = (params: AgentParams) => {
|
export const createAgent = (params: AgentParams) => {
|
||||||
const messages: ModelMessage[] = []
|
const messages: ModelMessage[] = []
|
||||||
|
|
||||||
const gateway = createChatGateway(params.model)
|
const gateway = createChatGateway(params.model)
|
||||||
|
|
||||||
|
const maxContextLoadTime = params.maxContextLoadTime ?? 60
|
||||||
|
const language = params.language ?? 'Same as user input'
|
||||||
|
|
||||||
const getTools = async () => {
|
const getTools = async () => {
|
||||||
return {
|
return {
|
||||||
...getMemoryTools({
|
...getMemoryTools({
|
||||||
searchMemory: params.onSearchMemory ?? (() => Promise.resolve([]))
|
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 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 to = new Date()
|
||||||
const memory = await params.onReadMemory?.(from, to) ?? []
|
const memory = await params.onReadMemory?.(from, to) ?? []
|
||||||
const context = memory.flatMap(m => m.messages)
|
const context = memory.flatMap(m => m.messages)
|
||||||
@@ -28,18 +37,45 @@ export const createAgent = (params: AgentParams) => {
|
|||||||
const getSystemPrompt = () => {
|
const getSystemPrompt = () => {
|
||||||
return system({
|
return system({
|
||||||
date: new Date(),
|
date: new Date(),
|
||||||
language: params.language ?? 'Same as user input',
|
language,
|
||||||
locale: params.locale,
|
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) {
|
async function* ask(input: string) {
|
||||||
await loadContext()
|
await loadContext()
|
||||||
const user = {
|
const user = {
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: input,
|
content: input,
|
||||||
}
|
} as UserModelMessage
|
||||||
messages.push(user)
|
messages.push(user)
|
||||||
const { fullStream, response } = streamText({
|
const { fullStream, response } = streamText({
|
||||||
model: gateway,
|
model: gateway,
|
||||||
@@ -63,9 +99,17 @@ export const createAgent = (params: AgentParams) => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const triggerSchedule = async (schedule: Schedule) => {
|
||||||
|
const prompt = getSchedulePrompt(schedule)
|
||||||
|
await askDirectly(prompt)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ask,
|
ask,
|
||||||
|
askDirectly,
|
||||||
loadContext,
|
loadContext,
|
||||||
getSystemPrompt,
|
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'
|
import { quote } from './utils'
|
||||||
|
|
||||||
export interface SystemParams {
|
export interface SystemParams {
|
||||||
@@ -9,20 +10,28 @@ export interface SystemParams {
|
|||||||
|
|
||||||
export const system = ({ date, locale, language, maxContextLoadTime }: SystemParams) => {
|
export const system = ({ date, locale, language, maxContextLoadTime }: SystemParams) => {
|
||||||
return `
|
return `
|
||||||
---
|
---
|
||||||
date: ${date.toLocaleDateString(locale)}
|
${time({ date, locale })}
|
||||||
time: ${date.toLocaleTimeString(locale)}
|
language: ${language}
|
||||||
language: ${language}
|
---
|
||||||
---
|
You are a personal housekeeper assistant, which able to manage the master's daily affairs.
|
||||||
You are a personal housekeeper assistant, which able to manage the master's daily affairs.
|
|
||||||
|
|
||||||
Your abilities:
|
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.
|
- 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.
|
- 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.
|
- Messaging: You may allowed to use message software to send messages to the master.
|
||||||
|
|
||||||
**Memory**
|
**Memory**
|
||||||
- Your context has been loaded from the last ${maxContextLoadTime} minutes.
|
- Your context has been loaded from the last ${maxContextLoadTime} minutes.
|
||||||
- You can use ${quote('search-memory')} to search for past memories with natural language.
|
- 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()
|
`.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 type { MemoryUnit } from '@memohome/memory'
|
||||||
import { ChatModel } from '@memohome/shared'
|
import { ChatModel, Schedule } from '@memohome/shared'
|
||||||
import { ModelMessage } from 'ai'
|
import { ModelMessage } from 'ai'
|
||||||
|
|
||||||
export interface AgentParams {
|
export interface AgentParams {
|
||||||
@@ -8,7 +8,7 @@ export interface AgentParams {
|
|||||||
/**
|
/**
|
||||||
* Unit: minutes
|
* Unit: minutes
|
||||||
*/
|
*/
|
||||||
maxContextLoadTime: number
|
maxContextLoadTime?: number
|
||||||
|
|
||||||
locale?: Intl.LocalesArgument
|
locale?: Intl.LocalesArgument
|
||||||
|
|
||||||
@@ -22,6 +22,12 @@ export interface AgentParams {
|
|||||||
|
|
||||||
onSearchMemory?: (query: string) => Promise<object[]>
|
onSearchMemory?: (query: string) => Promise<object[]>
|
||||||
|
|
||||||
|
onSchedule?: (schedule: Schedule) => Promise<void>
|
||||||
|
|
||||||
|
onGetSchedules?: () => Promise<Schedule[]>
|
||||||
|
|
||||||
|
onRemoveSchedule?: (id: string) => Promise<void>
|
||||||
|
|
||||||
onFinish?: (messages: ModelMessage[]) => Promise<void>
|
onFinish?: (messages: ModelMessage[]) => Promise<void>
|
||||||
|
|
||||||
onError?: (error: Error) => Promise<void>
|
onError?: (error: Error) => Promise<void>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Elysia } from 'elysia'
|
import { Elysia } from 'elysia'
|
||||||
import { corsMiddleware, errorMiddleware } from './middlewares'
|
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 { memoryModule } from './modules/memory'
|
||||||
import openapi from '@elysiajs/openapi'
|
import openapi from '@elysiajs/openapi'
|
||||||
|
|
||||||
@@ -14,6 +14,7 @@ export const app = new Elysia()
|
|||||||
.use(agentModule)
|
.use(agentModule)
|
||||||
.use(memoryModule)
|
.use(memoryModule)
|
||||||
.use(modelModule)
|
.use(modelModule)
|
||||||
|
.use(scheduleModule)
|
||||||
.use(settingsModule)
|
.use(settingsModule)
|
||||||
.use(userModule)
|
.use(userModule)
|
||||||
.listen(port)
|
.listen(port)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Elysia from 'elysia'
|
import Elysia from 'elysia'
|
||||||
import { authMiddleware } from '../../middlewares/auth'
|
import { authMiddleware } from '../../middlewares/auth'
|
||||||
import { AgentStreamModel } from './model'
|
import { AgentStreamModel } from './model'
|
||||||
import { createAgentStream } from './service'
|
import { createAgent } from './service'
|
||||||
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
|
import { getChatModel, getEmbeddingModel, getSummaryModel } from '../model/service'
|
||||||
import { getSettings } from '../settings/service'
|
import { getSettings } from '../settings/service'
|
||||||
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
import { ChatModel, EmbeddingModel } from '@memohome/shared'
|
||||||
@@ -38,7 +38,7 @@ export const agentModule = new Elysia({
|
|||||||
?? 'Same as user input'
|
?? 'Same as user input'
|
||||||
|
|
||||||
// Create agent
|
// Create agent
|
||||||
const agent = await createAgentStream({
|
const agent = await createAgent({
|
||||||
userId: user.userId,
|
userId: user.userId,
|
||||||
chatModel: chatModel.model as ChatModel,
|
chatModel: chatModel.model as ChatModel,
|
||||||
embeddingModel: embeddingModel.model as EmbeddingModel,
|
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 { 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 for messages passed to onFinish callback
|
||||||
type MessageType = Record<string, unknown>
|
type MessageType = Record<string, unknown>
|
||||||
@@ -10,12 +11,12 @@ export interface CreateAgentStreamParams {
|
|||||||
chatModel: ChatModel
|
chatModel: ChatModel
|
||||||
embeddingModel: EmbeddingModel
|
embeddingModel: EmbeddingModel
|
||||||
summaryModel: ChatModel
|
summaryModel: ChatModel
|
||||||
maxContextLoadTime: number
|
maxContextLoadTime?: number
|
||||||
language?: string
|
language?: string
|
||||||
onFinish?: (messages: MessageType[]) => Promise<void>
|
onFinish?: (messages: MessageType[]) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAgentStream(params: CreateAgentStreamParams) {
|
export async function createAgent(params: CreateAgentStreamParams) {
|
||||||
const {
|
const {
|
||||||
userId,
|
userId,
|
||||||
chatModel,
|
chatModel,
|
||||||
@@ -33,7 +34,7 @@ export async function createAgentStream(params: CreateAgentStreamParams) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Create agent
|
// Create agent
|
||||||
const agent = createAgent({
|
const agent = createAgentService({
|
||||||
model: chatModel,
|
model: chatModel,
|
||||||
maxContextLoadTime,
|
maxContextLoadTime,
|
||||||
language: language || 'Same as user input',
|
language: language || 'Same as user input',
|
||||||
@@ -56,6 +57,29 @@ export async function createAgentStream(params: CreateAgentStreamParams) {
|
|||||||
// Call custom onFinish handler if provided
|
// Call custom onFinish handler if provided
|
||||||
await onFinish?.(messages)
|
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
|
return agent
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from './agent'
|
export * from './agent'
|
||||||
export * from './auth'
|
export * from './auth'
|
||||||
export * from './model'
|
export * from './model'
|
||||||
|
export * from './schedule'
|
||||||
export * from './settings'
|
export * from './settings'
|
||||||
export * from './user'
|
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 './history'
|
||||||
export * from './model'
|
export * from './model'
|
||||||
export * from './settings'
|
export * from './settings'
|
||||||
|
export * from './schedule'
|
||||||
export * from './users'
|
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
|
specifier: ^0.4.1
|
||||||
version: 0.4.1(zod-to-json-schema@3.25.1(zod@4.3.5))(zod@4.3.5)
|
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/shared: {}
|
||||||
|
|
||||||
packages/ui:
|
packages/ui:
|
||||||
|
|||||||
Reference in New Issue
Block a user