mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 00:10:22 +08:00
Merge branch 'feat/agents-new' of github.com:CherryHQ/cherry-studio into feat/agents-new
This commit is contained in:
commit
1c19e529ac
83
CLAUDE.md
83
CLAUDE.md
@ -14,85 +14,10 @@ This file provides guidance to AI coding assistants when working with code in th
|
||||
1. **Code Search**: Use `ast-grep` for semantic code pattern searches when available. Fallback to `rg` (ripgrep) or `grep` for text-based searches.
|
||||
2. **UI Framework**: Exclusively use **HeroUI** for all new UI components. The use of `antd` or `styled-components` is strictly **PROHIBITED**.
|
||||
3. **Quality Assurance**: **Always** run `yarn build:check` before finalizing your work or making any commits. This ensures code quality (linting, testing, and type checking).
|
||||
4. **Session Tracking Documentation**: MUST Consistently maintain the session SDLC log file following the template structure outlined in the Session Tracking section.
|
||||
5. **Centralized Logging**: Use the `loggerService` exclusively for all application logging (info, warn, error levels) with proper context. Do not use `console.log`.
|
||||
6. **External Research**: Leverage `subagent` for gathering external information, including latest documentation, API references, news, or web-based research. This keeps the main conversation focused on the task at hand.
|
||||
|
||||
## Session Tracking Protocol
|
||||
|
||||
Purpose: keep a living SDLC record so any coding agent can pause or resume work without losing momentum.
|
||||
|
||||
### When to Log
|
||||
- Start a new file when kicking off a feature or major task branch.
|
||||
- Append to the existing file whenever you switch focus, finish a task, encounter a blocker, or hand over.
|
||||
- If you resume someone else's session, add a new patch log entry summarizing what you picked up and what remains.
|
||||
|
||||
### File Naming
|
||||
- `.sessions/YYYYMMDD-<feature>.md`
|
||||
- Example: `.sessions/20250916-agent-onboarding.md`
|
||||
|
||||
### Template
|
||||
```md
|
||||
# <feature> — SDLC Session (<YYYY-MM-DD>)
|
||||
|
||||
## Session Metadata
|
||||
- Participants:
|
||||
- Repo state / branch:
|
||||
- Related tickets / docs:
|
||||
- Links to prior sessions:
|
||||
|
||||
## Design Brief
|
||||
- Problem & goals:
|
||||
- Non-goals / scope:
|
||||
- Constraints & risks:
|
||||
- Acceptance criteria:
|
||||
|
||||
## Solution Plan
|
||||
- Architecture / flow:
|
||||
- Key interfaces or modules:
|
||||
- Data considerations:
|
||||
- Test strategy:
|
||||
|
||||
## Work Plan
|
||||
| ID | Task | Owner | Depends | Est | Status | Notes |
|
||||
| --- | ---- | ----- | ------- | --- | ------ | ----- |
|
||||
| T1 | | | | | TODO | |
|
||||
|
||||
_Status values: TODO, IN PROGRESS, BLOCKED, DONE. Update estimates as work evolves._
|
||||
|
||||
## Execution Log
|
||||
### <YYYY-MM-DD HH:MM>
|
||||
- Activity summary (what changed, decisions made)
|
||||
- Artifacts (PRs, commits, file paths, specs)
|
||||
- Tests / Commands run:
|
||||
- Issues / Risks:
|
||||
- Next focus before handoff:
|
||||
|
||||
_Append a new timestamped block for each meaningful work segment._
|
||||
|
||||
## Handoff Checklist
|
||||
- [ ] Remaining work captured in Work Plan
|
||||
- [ ] Blockers / questions called out
|
||||
- [ ] Links to diffs / PRs / relevant artifacts
|
||||
- [ ] Next session entry point documented
|
||||
|
||||
## Validation
|
||||
- [ ] Acceptance criteria met
|
||||
- [ ] `yarn build:check` passes
|
||||
- [ ] Tests required by strategy green
|
||||
- [ ] Docs / tickets updated (if applicable)
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
```
|
||||
### 2025-09-16 18:40
|
||||
- Activity: Finished wiring HeroUI settings panel skeleton; left TODO for data bindings.
|
||||
- Artifacts: src/renderer/.../SettingsPanel.tsx, PR #1234 (draft).
|
||||
- Tests / Commands: yarn lint
|
||||
- Issues / Risks: Waiting on API schema (#456).
|
||||
- Next focus: Bind `updateSettings` once API lands; run yarn build:check before flip to PR.
|
||||
```
|
||||
|
||||
4. **Centralized Logging**: Use the `loggerService` exclusively for all application logging (info, warn, error levels) with proper context. Do not use `console.log`.
|
||||
5. **External Research**: Leverage `subagent` for gathering external information, including latest documentation, API references, news, or web-based research. This keeps the main conversation focused on the task at hand.
|
||||
6. **Code Reviews**: Always seek a code review from a human developer before merging significant changes. This ensures adherence to project standards and catches potential issues.
|
||||
7. **Documentation**: Update or create documentation for any new features, modules, or significant changes to existing functionality.
|
||||
|
||||
## Development Commands
|
||||
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { ListAgentsResponse } from '@types'
|
||||
import { ListAgentsResponse,type ReplaceAgentRequest, type UpdateAgentRequest } from '@types'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import { agentService } from '../../../../services/agents'
|
||||
import type { ValidationRequest } from '../validators/zodValidator'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerAgentsHandlers')
|
||||
|
||||
@ -263,7 +264,10 @@ export const updateAgent = async (req: Request, res: Response): Promise<Response
|
||||
logger.info(`Updating agent: ${agentId}`)
|
||||
logger.debug('Update data:', req.body)
|
||||
|
||||
const agent = await agentService.updateAgent(agentId, req.body)
|
||||
const { validatedBody } = req as ValidationRequest
|
||||
const replacePayload = (validatedBody ?? {}) as ReplaceAgentRequest
|
||||
|
||||
const agent = await agentService.updateAgent(agentId, replacePayload, { replace: true })
|
||||
|
||||
if (!agent) {
|
||||
logger.warn(`Agent not found for update: ${agentId}`)
|
||||
@ -395,7 +399,10 @@ export const patchAgent = async (req: Request, res: Response): Promise<Response>
|
||||
logger.info(`Partially updating agent: ${agentId}`)
|
||||
logger.debug('Partial update data:', req.body)
|
||||
|
||||
const agent = await agentService.updateAgent(agentId, req.body)
|
||||
const { validatedBody } = req as ValidationRequest
|
||||
const updatePayload = (validatedBody ?? {}) as UpdateAgentRequest
|
||||
|
||||
const agent = await agentService.updateAgent(agentId, updatePayload)
|
||||
|
||||
if (!agent) {
|
||||
logger.warn(`Agent not found for partial update: ${agentId}`)
|
||||
|
||||
@ -12,7 +12,7 @@ const verifyAgentAndSession = async (agentId: string, sessionId: string) => {
|
||||
throw { status: 404, code: 'agent_not_found', message: 'Agent not found' }
|
||||
}
|
||||
|
||||
const session = await sessionService.getSession(sessionId)
|
||||
const session = await sessionService.getSession(agentId, sessionId)
|
||||
if (!session) {
|
||||
throw { status: 404, code: 'session_not_found', message: 'Session not found' }
|
||||
}
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import { loggerService } from '@logger'
|
||||
import { sessionMessageService, sessionService } from '@main/services/agents'
|
||||
import { CreateSessionResponse, ListAgentSessionsResponse, UpdateSessionResponse } from '@types'
|
||||
import {
|
||||
CreateSessionResponse,
|
||||
ListAgentSessionsResponse,
|
||||
type ReplaceSessionRequest,
|
||||
UpdateSessionResponse
|
||||
} from '@types'
|
||||
import { Request, Response } from 'express'
|
||||
|
||||
import type { ValidationRequest } from '../validators/zodValidator'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerSessionsHandlers')
|
||||
|
||||
export const createSession = async (req: Request, res: Response): Promise<Response> => {
|
||||
@ -64,7 +71,7 @@ export const getSession = async (req: Request, res: Response): Promise<Response>
|
||||
const { agentId, sessionId } = req.params
|
||||
logger.info(`Getting session: ${sessionId} for agent: ${agentId}`)
|
||||
|
||||
const session = await sessionService.getSession(sessionId)
|
||||
const session = await sessionService.getSession(agentId, sessionId)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found: ${sessionId}`)
|
||||
@ -119,7 +126,7 @@ export const updateSession = async (req: Request, res: Response): Promise<Respon
|
||||
logger.debug('Update data:', req.body)
|
||||
|
||||
// First check if session exists and belongs to agent
|
||||
const existingSession = await sessionService.getSession(sessionId)
|
||||
const existingSession = await sessionService.getSession(agentId, sessionId)
|
||||
if (!existingSession || existingSession.agent_id !== agentId) {
|
||||
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
|
||||
return res.status(404).json({
|
||||
@ -131,9 +138,10 @@ export const updateSession = async (req: Request, res: Response): Promise<Respon
|
||||
})
|
||||
}
|
||||
|
||||
// For PUT, we replace the entire resource
|
||||
const sessionData = { ...req.body, main_agent_id: agentId }
|
||||
const session = await sessionService.updateSession(sessionId, sessionData)
|
||||
const { validatedBody } = req as ValidationRequest
|
||||
const replacePayload = (validatedBody ?? {}) as ReplaceSessionRequest
|
||||
|
||||
const session = await sessionService.updateSession(agentId, sessionId, replacePayload)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found for update: ${sessionId}`)
|
||||
@ -167,7 +175,7 @@ export const patchSession = async (req: Request, res: Response): Promise<Respons
|
||||
logger.debug('Patch data:', req.body)
|
||||
|
||||
// First check if session exists and belongs to agent
|
||||
const existingSession = await sessionService.getSession(sessionId)
|
||||
const existingSession = await sessionService.getSession(agentId, sessionId)
|
||||
if (!existingSession || existingSession.agent_id !== agentId) {
|
||||
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
|
||||
return res.status(404).json({
|
||||
@ -180,7 +188,7 @@ export const patchSession = async (req: Request, res: Response): Promise<Respons
|
||||
}
|
||||
|
||||
const updateSession = { ...existingSession, ...req.body }
|
||||
const session = await sessionService.updateSession(sessionId, updateSession)
|
||||
const session = await sessionService.updateSession(agentId, sessionId, updateSession)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found for patch: ${sessionId}`)
|
||||
@ -213,7 +221,7 @@ export const deleteSession = async (req: Request, res: Response): Promise<Respon
|
||||
logger.info(`Deleting session: ${sessionId} for agent: ${agentId}`)
|
||||
|
||||
// First check if session exists and belongs to agent
|
||||
const existingSession = await sessionService.getSession(sessionId)
|
||||
const existingSession = await sessionService.getSession(agentId, sessionId)
|
||||
if (!existingSession || existingSession.agent_id !== agentId) {
|
||||
logger.warn(`Session ${sessionId} not found for agent ${agentId}`)
|
||||
return res.status(404).json({
|
||||
@ -225,7 +233,7 @@ export const deleteSession = async (req: Request, res: Response): Promise<Respon
|
||||
})
|
||||
}
|
||||
|
||||
const deleted = await sessionService.deleteSession(sessionId)
|
||||
const deleted = await sessionService.deleteSession(agentId, sessionId)
|
||||
|
||||
if (!deleted) {
|
||||
logger.warn(`Session not found for deletion: ${sessionId}`)
|
||||
@ -287,7 +295,7 @@ export const getSessionById = async (req: Request, res: Response): Promise<Respo
|
||||
const { sessionId } = req.params
|
||||
logger.info(`Getting session: ${sessionId}`)
|
||||
|
||||
const session = await sessionService.getSession(sessionId)
|
||||
const session = await sessionService.getSessionById(sessionId)
|
||||
|
||||
if (!session) {
|
||||
logger.warn(`Session not found: ${sessionId}`)
|
||||
|
||||
@ -5,11 +5,13 @@ import { checkAgentExists, handleValidationErrors } from './middleware'
|
||||
import {
|
||||
validateAgent,
|
||||
validateAgentId,
|
||||
validateAgentReplace,
|
||||
validateAgentUpdate,
|
||||
validatePagination,
|
||||
validateSession,
|
||||
validateSessionId,
|
||||
validateSessionMessage,
|
||||
validateSessionReplace,
|
||||
validateSessionUpdate
|
||||
} from './validators'
|
||||
|
||||
@ -152,7 +154,13 @@ const agentsRouter = express.Router()
|
||||
agentsRouter.post('/', validateAgent, handleValidationErrors, agentHandlers.createAgent)
|
||||
agentsRouter.get('/', validatePagination, handleValidationErrors, agentHandlers.listAgents)
|
||||
agentsRouter.get('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.getAgent)
|
||||
agentsRouter.put('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.updateAgent)
|
||||
agentsRouter.put(
|
||||
'/:agentId',
|
||||
validateAgentId,
|
||||
validateAgentReplace,
|
||||
handleValidationErrors,
|
||||
agentHandlers.updateAgent
|
||||
)
|
||||
agentsRouter.patch('/:agentId', validateAgentId, validateAgentUpdate, handleValidationErrors, agentHandlers.patchAgent)
|
||||
agentsRouter.delete('/:agentId', validateAgentId, handleValidationErrors, agentHandlers.deleteAgent)
|
||||
|
||||
@ -167,7 +175,7 @@ const createSessionsRouter = (): express.Router => {
|
||||
sessionsRouter.put(
|
||||
'/:sessionId',
|
||||
validateSessionId,
|
||||
validateSessionUpdate,
|
||||
validateSessionReplace,
|
||||
handleValidationErrors,
|
||||
sessionHandlers.updateSession
|
||||
)
|
||||
|
||||
@ -1,24 +1,12 @@
|
||||
import { Request, Response } from 'express'
|
||||
import { validationResult } from 'express-validator'
|
||||
|
||||
import { agentService } from '../../../../services/agents'
|
||||
import { loggerService } from '../../../../services/LoggerService'
|
||||
|
||||
const logger = loggerService.withContext('ApiServerMiddleware')
|
||||
|
||||
// Error handler for validation
|
||||
export const handleValidationErrors = (req: Request, res: Response, next: any): void => {
|
||||
const errors = validationResult(req)
|
||||
if (!errors.isEmpty()) {
|
||||
res.status(400).json({
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
details: errors.array()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// Since Zod validators handle their own errors, this is now a pass-through
|
||||
export const handleValidationErrors = (_req: Request, _res: Response, next: any): void => {
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +1,24 @@
|
||||
import { body, param } from 'express-validator'
|
||||
import {
|
||||
AgentIdParamSchema,
|
||||
CreateAgentRequestSchema,
|
||||
ReplaceAgentRequestSchema,
|
||||
UpdateAgentRequestSchema
|
||||
} from '@types'
|
||||
|
||||
export const validateAgent = [
|
||||
body('name').notEmpty().withMessage('Name is required'),
|
||||
body('model').notEmpty().withMessage('Model is required'),
|
||||
body('description').optional().isString(),
|
||||
body('avatar').optional().isString(),
|
||||
body('instructions').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateAgentUpdate = [
|
||||
body('name').optional().notEmpty().withMessage('Name cannot be empty'),
|
||||
body('model').optional().notEmpty().withMessage('Model cannot be empty'),
|
||||
body('description').optional().isString(),
|
||||
body('avatar').optional().isString(),
|
||||
body('instructions').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
export const validateAgent = createZodValidator({
|
||||
body: CreateAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentId = [param('agentId').notEmpty().withMessage('Agent ID is required')]
|
||||
export const validateAgentReplace = createZodValidator({
|
||||
body: ReplaceAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentUpdate = createZodValidator({
|
||||
body: UpdateAgentRequestSchema
|
||||
})
|
||||
|
||||
export const validateAgentId = createZodValidator({
|
||||
params: AgentIdParamSchema
|
||||
})
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { query } from 'express-validator'
|
||||
import { PaginationQuerySchema } from '@types'
|
||||
|
||||
export const validatePagination = [
|
||||
query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('Limit must be between 1 and 100'),
|
||||
query('offset').optional().isInt({ min: 0 }).withMessage('Offset must be non-negative'),
|
||||
query('status')
|
||||
.optional()
|
||||
.isIn(['idle', 'running', 'completed', 'failed', 'stopped'])
|
||||
.withMessage('Invalid status filter')
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validatePagination = createZodValidator({
|
||||
query: PaginationQuerySchema
|
||||
})
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { body } from 'express-validator'
|
||||
import { CreateSessionMessageRequestSchema } from '@types'
|
||||
|
||||
export const validateSessionMessage = [
|
||||
body('content').notEmpty().isString().withMessage('Content must be a valid string')
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateSessionMessage = createZodValidator({
|
||||
body: CreateSessionMessageRequestSchema
|
||||
})
|
||||
|
||||
@ -1,47 +1,24 @@
|
||||
import { body, param } from 'express-validator'
|
||||
import {
|
||||
CreateSessionRequestSchema,
|
||||
ReplaceSessionRequestSchema,
|
||||
SessionIdParamSchema,
|
||||
UpdateSessionRequestSchema
|
||||
} from '@types'
|
||||
|
||||
export const validateSession = [
|
||||
body('name').optional().isString(),
|
||||
body('sub_agent_ids').optional().isArray(),
|
||||
body('user_goal').optional().isString(),
|
||||
body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']),
|
||||
body('external_session_id').optional().isString(),
|
||||
body('model').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
import { createZodValidator } from './zodValidator'
|
||||
|
||||
export const validateSessionUpdate = [
|
||||
body('name').optional().isString(),
|
||||
body('main_agent_id').optional().notEmpty().withMessage('Main agent ID cannot be empty'),
|
||||
body('sub_agent_ids').optional().isArray(),
|
||||
body('user_goal').optional().isString(),
|
||||
body('status').optional().isIn(['idle', 'running', 'completed', 'failed', 'stopped']),
|
||||
body('external_session_id').optional().isString(),
|
||||
body('model').optional().isString(),
|
||||
body('plan_model').optional().isString(),
|
||||
body('small_model').optional().isString(),
|
||||
body('built_in_tools').optional().isArray(),
|
||||
body('mcps').optional().isArray(),
|
||||
body('knowledges').optional().isArray(),
|
||||
body('configuration').optional().isObject(),
|
||||
body('accessible_paths').optional().isArray(),
|
||||
body('permission_mode').optional().isIn(['readOnly', 'acceptEdits', 'bypassPermissions']),
|
||||
body('max_steps').optional().isInt({ min: 1 })
|
||||
]
|
||||
export const validateSession = createZodValidator({
|
||||
body: CreateSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateStatusUpdate = [
|
||||
body('status')
|
||||
.notEmpty()
|
||||
.isIn(['idle', 'running', 'completed', 'failed', 'stopped'])
|
||||
.withMessage('Valid status is required')
|
||||
]
|
||||
export const validateSessionReplace = createZodValidator({
|
||||
body: ReplaceSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateSessionId = [param('sessionId').notEmpty().withMessage('Session ID is required')]
|
||||
export const validateSessionUpdate = createZodValidator({
|
||||
body: UpdateSessionRequestSchema
|
||||
})
|
||||
|
||||
export const validateSessionId = createZodValidator({
|
||||
params: SessionIdParamSchema
|
||||
})
|
||||
|
||||
68
src/main/apiServer/routes/agents/validators/zodValidator.ts
Normal file
68
src/main/apiServer/routes/agents/validators/zodValidator.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { NextFunction,Request, Response } from 'express'
|
||||
import { ZodError, ZodType } from 'zod'
|
||||
|
||||
export interface ValidationRequest extends Request {
|
||||
validatedBody?: any
|
||||
validatedParams?: any
|
||||
validatedQuery?: any
|
||||
}
|
||||
|
||||
export interface ZodValidationConfig {
|
||||
body?: ZodType
|
||||
params?: ZodType
|
||||
query?: ZodType
|
||||
}
|
||||
|
||||
export const createZodValidator = (config: ZodValidationConfig) => {
|
||||
return (req: ValidationRequest, res: Response, next: NextFunction): void => {
|
||||
try {
|
||||
if (config.body && req.body) {
|
||||
req.validatedBody = config.body.parse(req.body)
|
||||
}
|
||||
|
||||
if (config.params && req.params) {
|
||||
req.validatedParams = config.params.parse(req.params)
|
||||
}
|
||||
|
||||
if (config.query && req.query) {
|
||||
req.validatedQuery = config.query.parse(req.query)
|
||||
}
|
||||
|
||||
next()
|
||||
} catch (error) {
|
||||
if (error instanceof ZodError) {
|
||||
const validationErrors = error.issues.map((err) => ({
|
||||
type: 'field',
|
||||
value: err.input,
|
||||
msg: err.message,
|
||||
path: err.path.map(p => String(p)).join('.'),
|
||||
location: getLocationFromPath(err.path, config)
|
||||
}))
|
||||
|
||||
res.status(400).json({
|
||||
error: {
|
||||
message: 'Validation failed',
|
||||
type: 'validation_error',
|
||||
details: validationErrors
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: {
|
||||
message: 'Internal validation error',
|
||||
type: 'internal_error',
|
||||
code: 'validation_processing_failed'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLocationFromPath(path: (string | number | symbol)[], config: ZodValidationConfig): string {
|
||||
if (config.body && path.length > 0) return 'body'
|
||||
if (config.params && path.length > 0) return 'params'
|
||||
if (config.query && path.length > 0) return 'query'
|
||||
return 'unknown'
|
||||
}
|
||||
@ -27,7 +27,7 @@ export abstract class BaseService {
|
||||
protected static db: LibSQLDatabase<typeof schema> | null = null
|
||||
protected static isInitialized = false
|
||||
protected static initializationPromise: Promise<void> | null = null
|
||||
protected jsonFields: string[] = ['built_in_tools', 'mcps', 'configuration', 'accessible_paths']
|
||||
protected jsonFields: string[] = ['built_in_tools', 'mcps', 'configuration', 'accessible_paths', 'allowed_tools']
|
||||
|
||||
/**
|
||||
* Initialize database with retry logic and proper error handling
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { getDataPath } from '@main/utils'
|
||||
import type {
|
||||
import {
|
||||
AgentBaseSchema,
|
||||
AgentEntity,
|
||||
CreateAgentRequest,
|
||||
CreateAgentResponse,
|
||||
@ -111,7 +112,11 @@ export class AgentService extends BaseService {
|
||||
return { agents, total: totalResult[0].count }
|
||||
}
|
||||
|
||||
async updateAgent(id: string, updates: UpdateAgentRequest): Promise<UpdateAgentResponse | null> {
|
||||
async updateAgent(
|
||||
id: string,
|
||||
updates: UpdateAgentRequest,
|
||||
options: { replace?: boolean } = {}
|
||||
): Promise<UpdateAgentResponse | null> {
|
||||
this.ensureInitialized()
|
||||
|
||||
// Check if agent exists
|
||||
@ -126,18 +131,20 @@ export class AgentService extends BaseService {
|
||||
const updateData: Partial<AgentRow> = {
|
||||
updated_at: now
|
||||
}
|
||||
const replaceableFields = Object.keys(AgentBaseSchema.shape) as (keyof AgentRow)[]
|
||||
const shouldReplace = options.replace ?? false
|
||||
|
||||
for (const field of replaceableFields) {
|
||||
if (shouldReplace || Object.prototype.hasOwnProperty.call(serializedUpdates, field)) {
|
||||
if (Object.prototype.hasOwnProperty.call(serializedUpdates, field)) {
|
||||
const value = serializedUpdates[field as keyof typeof serializedUpdates]
|
||||
;(updateData as Record<string, unknown>)[field] = value ?? null
|
||||
} else if (shouldReplace) {
|
||||
;(updateData as Record<string, unknown>)[field] = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only update fields that are provided
|
||||
if (serializedUpdates.name !== undefined) updateData.name = serializedUpdates.name
|
||||
if (serializedUpdates.description !== undefined) updateData.description = serializedUpdates.description
|
||||
if (serializedUpdates.instructions !== undefined) updateData.instructions = serializedUpdates.instructions
|
||||
if (serializedUpdates.model !== undefined) updateData.model = serializedUpdates.model
|
||||
if (serializedUpdates.plan_model !== undefined) updateData.plan_model = serializedUpdates.plan_model
|
||||
if (serializedUpdates.small_model !== undefined) updateData.small_model = serializedUpdates.small_model
|
||||
if (serializedUpdates.mcps !== undefined) updateData.mcps = serializedUpdates.mcps
|
||||
if (serializedUpdates.configuration !== undefined) updateData.configuration = serializedUpdates.configuration
|
||||
if (serializedUpdates.accessible_paths !== undefined)
|
||||
updateData.accessible_paths = serializedUpdates.accessible_paths
|
||||
await this.database.update(agentsTable).set(updateData).where(eq(agentsTable.id, id))
|
||||
return await this.getAgent(id)
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
} from '@types'
|
||||
import { ModelMessage, UIMessage, UIMessageChunk } from 'ai'
|
||||
import { convertToModelMessages, readUIMessageStream } from 'ai'
|
||||
import { count, eq } from 'drizzle-orm'
|
||||
import { eq } from 'drizzle-orm'
|
||||
|
||||
import { BaseService } from '../BaseService'
|
||||
import { InsertSessionMessageRow, sessionMessagesTable } from '../database/schema'
|
||||
@ -148,17 +148,9 @@ export class SessionMessageService extends BaseService {
|
||||
async listSessionMessages(
|
||||
sessionId: string,
|
||||
options: ListOptions = {}
|
||||
): Promise<{ messages: AgentSessionMessageEntity[]; total: number }> {
|
||||
): Promise<{ messages: AgentSessionMessageEntity[] }> {
|
||||
this.ensureInitialized()
|
||||
|
||||
// Get total count
|
||||
const totalResult = await this.database
|
||||
.select({ count: count() })
|
||||
.from(sessionMessagesTable)
|
||||
.where(eq(sessionMessagesTable.session_id, sessionId))
|
||||
|
||||
const total = totalResult[0].count
|
||||
|
||||
// Get messages with pagination
|
||||
const baseQuery = this.database
|
||||
.select()
|
||||
@ -175,7 +167,7 @@ export class SessionMessageService extends BaseService {
|
||||
|
||||
const messages = result.map((row) => this.deserializeSessionMessage(row)) as AgentSessionMessageEntity[]
|
||||
|
||||
return { messages, total }
|
||||
return { messages }
|
||||
}
|
||||
|
||||
async saveUserMessage(
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
import type {
|
||||
AgentEntity,
|
||||
AgentSessionEntity,
|
||||
CreateSessionRequest,
|
||||
CreateSessionResponse,
|
||||
GetAgentSessionResponse,
|
||||
ListOptions,
|
||||
UpdateSessionRequest,
|
||||
import {
|
||||
AgentBaseSchema,
|
||||
type AgentEntity,
|
||||
type AgentSessionEntity,
|
||||
type CreateSessionRequest,
|
||||
type CreateSessionResponse,
|
||||
type GetAgentSessionResponse,
|
||||
type ListOptions,
|
||||
type UpdateSessionRequest,
|
||||
UpdateSessionResponse
|
||||
} from '@types'
|
||||
import { and, count, eq, type SQL } from 'drizzle-orm'
|
||||
@ -79,7 +80,25 @@ export class SessionService extends BaseService {
|
||||
return this.deserializeJsonFields(result[0]) as AgentSessionEntity
|
||||
}
|
||||
|
||||
async getSession(id: string): Promise<GetAgentSessionResponse | null> {
|
||||
async getSession(agentId: string, id: string): Promise<GetAgentSessionResponse | null> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database
|
||||
.select()
|
||||
.from(sessionsTable)
|
||||
.where(and(eq(sessionsTable.id, id), eq(sessionsTable.agent_id, agentId)))
|
||||
.limit(1)
|
||||
|
||||
if (!result[0]) {
|
||||
return null
|
||||
}
|
||||
|
||||
const session = this.deserializeJsonFields(result[0]) as GetAgentSessionResponse
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
async getSessionById(id: string): Promise<GetAgentSessionResponse | null> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database.select().from(sessionsTable).where(eq(sessionsTable.id, id)).limit(1)
|
||||
@ -93,14 +112,6 @@ export class SessionService extends BaseService {
|
||||
return session
|
||||
}
|
||||
|
||||
async getSessionWithAgent(id: string): Promise<any | null> {
|
||||
this.ensureInitialized()
|
||||
|
||||
// TODO: Implement join query with agents table when needed
|
||||
// For now, just return the session
|
||||
return await this.getSession(id)
|
||||
}
|
||||
|
||||
async listSessions(
|
||||
agentId?: string,
|
||||
options: ListOptions = {}
|
||||
@ -140,11 +151,15 @@ export class SessionService extends BaseService {
|
||||
return { sessions, total }
|
||||
}
|
||||
|
||||
async updateSession(id: string, updates: UpdateSessionRequest): Promise<UpdateSessionResponse | null> {
|
||||
async updateSession(
|
||||
agentId: string,
|
||||
id: string,
|
||||
updates: UpdateSessionRequest
|
||||
): Promise<UpdateSessionResponse | null> {
|
||||
this.ensureInitialized()
|
||||
|
||||
// Check if session exists
|
||||
const existing = await this.getSession(id)
|
||||
const existing = await this.getSession(agentId, id)
|
||||
if (!existing) {
|
||||
return null
|
||||
}
|
||||
@ -158,40 +173,37 @@ export class SessionService extends BaseService {
|
||||
const updateData: Partial<SessionRow> = {
|
||||
updated_at: now
|
||||
}
|
||||
const replaceableFields = Object.keys(AgentBaseSchema.shape) as (keyof SessionRow)[]
|
||||
|
||||
// Only update fields that are provided
|
||||
if (serializedUpdates.name !== undefined) updateData.name = serializedUpdates.name
|
||||
|
||||
if (serializedUpdates.model !== undefined) updateData.model = serializedUpdates.model
|
||||
if (serializedUpdates.plan_model !== undefined) updateData.plan_model = serializedUpdates.plan_model
|
||||
if (serializedUpdates.small_model !== undefined) updateData.small_model = serializedUpdates.small_model
|
||||
|
||||
if (serializedUpdates.mcps !== undefined) updateData.mcps = serializedUpdates.mcps
|
||||
|
||||
if (serializedUpdates.configuration !== undefined) updateData.configuration = serializedUpdates.configuration
|
||||
if (serializedUpdates.accessible_paths !== undefined)
|
||||
updateData.accessible_paths = serializedUpdates.accessible_paths
|
||||
for (const field of replaceableFields) {
|
||||
if (Object.prototype.hasOwnProperty.call(serializedUpdates, field)) {
|
||||
const value = serializedUpdates[field as keyof typeof serializedUpdates]
|
||||
;(updateData as Record<string, unknown>)[field] = value ?? null
|
||||
}
|
||||
}
|
||||
|
||||
await this.database.update(sessionsTable).set(updateData).where(eq(sessionsTable.id, id))
|
||||
|
||||
return await this.getSession(id)
|
||||
return await this.getSession(agentId, id)
|
||||
}
|
||||
|
||||
async deleteSession(id: string): Promise<boolean> {
|
||||
async deleteSession(agentId: string, id: string): Promise<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database.delete(sessionsTable).where(eq(sessionsTable.id, id))
|
||||
const result = await this.database
|
||||
.delete(sessionsTable)
|
||||
.where(and(eq(sessionsTable.id, id), eq(sessionsTable.agent_id, agentId)))
|
||||
|
||||
return result.rowsAffected > 0
|
||||
}
|
||||
|
||||
async sessionExists(id: string): Promise<boolean> {
|
||||
async sessionExists(agentId: string, id: string): Promise<boolean> {
|
||||
this.ensureInitialized()
|
||||
|
||||
const result = await this.database
|
||||
.select({ id: sessionsTable.id })
|
||||
.from(sessionsTable)
|
||||
.where(eq(sessionsTable.id, id))
|
||||
.where(and(eq(sessionsTable.id, id), eq(sessionsTable.agent_id, agentId)))
|
||||
.limit(1)
|
||||
|
||||
return result.length > 0
|
||||
|
||||
@ -2,14 +2,24 @@
|
||||
* Database entity types for Agent, Session, and SessionMessage
|
||||
* Shared between main and renderer processes
|
||||
*/
|
||||
import { ModelMessage, modelMessageSchema, TextStreamPart, UIMessageChunk } from 'ai'
|
||||
import { ModelMessage, modelMessageSchema, TextStreamPart } from 'ai'
|
||||
import { z } from 'zod'
|
||||
|
||||
// ------------------ Core enums and helper types ------------------
|
||||
export const PermissionModeSchema = z.enum(['default', 'acceptEdits', 'bypassPermissions', 'plan'])
|
||||
export type PermissionMode = z.infer<typeof PermissionModeSchema>
|
||||
|
||||
export type SessionMessageRole = ModelMessage['role']
|
||||
|
||||
const sessionMessageRoles = ['assistant', 'user', 'system', 'tool'] as const satisfies readonly [
|
||||
SessionMessageRole,
|
||||
...SessionMessageRole[]
|
||||
]
|
||||
|
||||
export const SessionMessageRoleSchema = z.enum(sessionMessageRoles)
|
||||
|
||||
export type SessionMessageType = TextStreamPart<Record<string, any>>['type']
|
||||
|
||||
export const AgentTypeSchema = z.enum(['claude-code'])
|
||||
export type AgentType = z.infer<typeof AgentTypeSchema>
|
||||
|
||||
@ -17,8 +27,7 @@ export const isAgentType = (type: unknown): type is AgentType => {
|
||||
return AgentTypeSchema.safeParse(type).success
|
||||
}
|
||||
|
||||
export type SessionMessageType = TextStreamPart<Record<string, any>>['type']
|
||||
|
||||
// ------------------ Tool metadata ------------------
|
||||
export const ToolSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
@ -28,6 +37,7 @@ export const ToolSchema = z.object({
|
||||
|
||||
export type Tool = z.infer<typeof ToolSchema>
|
||||
|
||||
// ------------------ Agent configuration & base schema ------------------
|
||||
export const AgentConfigurationSchema = z
|
||||
.object({
|
||||
permission_mode: PermissionModeSchema.default('default'), // Permission mode, default to 'default'
|
||||
@ -62,6 +72,8 @@ export const AgentBaseSchema = z.object({
|
||||
|
||||
export type AgentBase = z.infer<typeof AgentBaseSchema>
|
||||
|
||||
// ------------------ Persistence entities ------------------
|
||||
|
||||
// Agent entity representing an autonomous agent configuration
|
||||
export const AgentEntitySchema = AgentBaseSchema.extend({
|
||||
id: z.string(),
|
||||
@ -95,7 +107,7 @@ export const AgentSessionMessageEntitySchema = z.object({
|
||||
id: z.number(), // Auto-increment primary key
|
||||
session_id: z.string(), // Reference to session
|
||||
// manual defined. may not synced with ai sdk definition
|
||||
role: z.enum(['assistant', 'user', 'system', 'tool']), // 'assistant' | 'user' | 'system' | 'tool'
|
||||
role: SessionMessageRoleSchema, // Enforce roles supported by modelMessageSchema
|
||||
content: modelMessageSchema,
|
||||
agent_session_id: z.string(), // agent session id, use to resume agent session
|
||||
metadata: z.record(z.string(), z.any()).optional(), // Additional metadata (optional)
|
||||
@ -105,13 +117,7 @@ export const AgentSessionMessageEntitySchema = z.object({
|
||||
|
||||
export type AgentSessionMessageEntity = z.infer<typeof AgentSessionMessageEntitySchema>
|
||||
|
||||
// Structured content for session messages that preserves both AI SDK and raw data
|
||||
export interface SessionMessageContent {
|
||||
chunk: UIMessageChunk[] // UI-friendly AI SDK chunks for rendering
|
||||
raw: any[] // Original agent-specific messages for data integrity (agent-agnostic)
|
||||
agentResult?: any // Complete result from the underlying agent service
|
||||
agentType: string // The type of agent that generated this message (e.g., 'claude-code', 'openai', etc.)
|
||||
}
|
||||
// ------------------ Session message payload ------------------
|
||||
|
||||
// Not implemented fields:
|
||||
// - plan_model: Optional model for planning/thinking tasks
|
||||
@ -119,10 +125,11 @@ export interface SessionMessageContent {
|
||||
// - mcps: Optional array of MCP (Model Control Protocol) tool IDs
|
||||
// - allowed_tools: Optional array of permitted tool IDs
|
||||
// - configuration: Optional agent settings (temperature, top_p, etc.)
|
||||
export interface BaseAgentForm {
|
||||
// ------------------ Form models ------------------
|
||||
export type BaseAgentForm = {
|
||||
id?: string
|
||||
type: AgentType
|
||||
// These fileds should be editable by user.
|
||||
// These fields should be editable by user.
|
||||
name: string
|
||||
description?: string
|
||||
instructions?: string
|
||||
@ -130,30 +137,22 @@ export interface BaseAgentForm {
|
||||
accessible_paths: string[]
|
||||
}
|
||||
|
||||
export interface AddAgentForm extends BaseAgentForm {
|
||||
id?: never
|
||||
}
|
||||
export type AddAgentForm = Omit<BaseAgentForm, 'id'> & { id?: never }
|
||||
|
||||
export interface UpdateAgentForm extends Partial<BaseAgentForm> {
|
||||
export type UpdateAgentForm = Partial<Omit<BaseAgentForm, 'type'>> & {
|
||||
id: string
|
||||
type?: never
|
||||
}
|
||||
|
||||
export type AgentForm = AddAgentForm | UpdateAgentForm
|
||||
|
||||
export interface BaseSessionForm extends AgentBase {}
|
||||
export type BaseSessionForm = AgentBase
|
||||
|
||||
export interface CreateSessionForm extends BaseSessionForm {
|
||||
id?: never
|
||||
}
|
||||
export type CreateSessionForm = BaseSessionForm & { id?: never }
|
||||
|
||||
export interface UpdateSessionForm extends Partial<BaseSessionForm> {
|
||||
id: string
|
||||
}
|
||||
export type UpdateSessionForm = Partial<BaseSessionForm> & { id: string }
|
||||
|
||||
// ------------------------
|
||||
// API Data Transfer Object
|
||||
// ------------------------
|
||||
// ------------------ API data transfer objects ------------------
|
||||
export interface CreateAgentRequest extends AgentBase {
|
||||
type: AgentType
|
||||
}
|
||||
@ -164,6 +163,8 @@ export type CreateAgentResponse = AgentEntity
|
||||
|
||||
export interface UpdateAgentRequest extends Partial<AgentBase> {}
|
||||
|
||||
export type ReplaceAgentRequest = AgentBase
|
||||
|
||||
export const GetAgentResponseSchema = AgentEntitySchema.extend({
|
||||
built_in_tools: z.array(ToolSchema).optional() // Built-in tools available to the agent
|
||||
})
|
||||
@ -224,3 +225,51 @@ export const AgentServerErrorSchema = z.object({
|
||||
})
|
||||
|
||||
export type AgentServerError = z.infer<typeof AgentServerErrorSchema>
|
||||
|
||||
// ------------------ API validation schemas ------------------
|
||||
|
||||
// Parameter validation schemas
|
||||
export const AgentIdParamSchema = z.object({
|
||||
agentId: z.string().min(1, 'Agent ID is required')
|
||||
})
|
||||
|
||||
export const SessionIdParamSchema = z.object({
|
||||
sessionId: z.string().min(1, 'Session ID is required')
|
||||
})
|
||||
|
||||
// Query validation schemas
|
||||
export const PaginationQuerySchema = z.object({
|
||||
limit: z.coerce.number().int().min(1).max(100).optional().default(20),
|
||||
offset: z.coerce.number().int().min(0).optional().default(0),
|
||||
status: z.enum(['idle', 'running', 'completed', 'failed', 'stopped']).optional()
|
||||
})
|
||||
|
||||
// Request body validation schemas derived from shared bases
|
||||
const agentCreatableSchema = AgentBaseSchema.extend({
|
||||
name: z.string().min(1, 'Name is required'),
|
||||
model: z.string().min(1, 'Model is required')
|
||||
})
|
||||
|
||||
export const CreateAgentRequestSchema = agentCreatableSchema.extend({
|
||||
type: AgentTypeSchema
|
||||
})
|
||||
|
||||
export const UpdateAgentRequestSchema = AgentBaseSchema.partial()
|
||||
|
||||
export const ReplaceAgentRequestSchema = AgentBaseSchema
|
||||
|
||||
const sessionCreatableSchema = AgentBaseSchema.extend({
|
||||
model: z.string().min(1, 'Model is required')
|
||||
})
|
||||
|
||||
export const CreateSessionRequestSchema = sessionCreatableSchema
|
||||
|
||||
export const UpdateSessionRequestSchema = sessionCreatableSchema.partial()
|
||||
|
||||
export const ReplaceSessionRequestSchema = sessionCreatableSchema
|
||||
|
||||
export type ReplaceSessionRequest = z.infer<typeof ReplaceSessionRequestSchema>
|
||||
|
||||
export const CreateSessionMessageRequestSchema = z.object({
|
||||
content: z.string().min(1, 'Content must be a valid string')
|
||||
})
|
||||
|
||||
Loading…
Reference in New Issue
Block a user