UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

357 lines (356 loc) 14.4 kB
/** * http-requests.ts — HTTP 路由请求 Zod Schemas * * 为 Express 路由提供运行时输入校验,覆盖: * - knowledge(CRUD + 生命周期) * - guardRules(规则管理 + 批量操作) * - search(统合搜索 + 上下文搜索) * - candidates(候选条目操作) * - guard(文件质量检查) * - skills(技能管理) * - task(统一任务分发) * - modules(模块扫描) * - ai(AI 配置、摘要、翻译、对话、Agent 工具/任务) * - extract(路径/文本提取) * - auth(登录) * - commands(文件读写) * - remote(远程指令、通知) * * @module shared/schemas/http-requests */ import { z } from 'zod'; // ─── 复用基础片段 ───────────────────────────── /** Id + limit 分页共用 */ const MAX_BATCH_SIZE = 100; const BatchIds = z.object({ ids: z.array(z.string().min(1)).min(1).max(MAX_BATCH_SIZE), }); const PaginationQuery = z.object({ page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(1000).default(20), }); // ═══ Knowledge ═══════════════════════════════════ export const CreateKnowledgeBody = z .object({ title: z.string().min(1, 'title is required'), content: z.union([z.string().min(1), z.record(z.string(), z.unknown())]), description: z.string().optional(), kind: z.enum(['rule', 'pattern', 'fact']).nullish(), language: z.string().optional(), category: z.string().optional(), knowledgeType: z.string().optional(), complexity: z.string().nullish(), scope: z.string().nullish(), tags: z.array(z.string()).optional(), }) .loose(); export const UpdateKnowledgeBody = z .object({ title: z.string().min(1).optional(), description: z.string().optional(), content: z.union([z.string(), z.record(z.string(), z.unknown())]).optional(), kind: z.enum(['rule', 'pattern', 'fact']).nullish(), language: z.string().optional(), category: z.string().optional(), tags: z.array(z.string()).optional(), }) .loose() .refine((data) => Object.keys(data).length > 0, { message: 'At least one field must be provided for update', }); export const DeprecateKnowledgeBody = z.object({ reason: z.string().min(1, 'reason is required'), }); export const BatchPublishBody = BatchIds; export const BatchDeleteBody = BatchIds; export const BatchDeprecateBody = BatchIds.extend({ reason: z.string().optional(), }); export const KnowledgeUsageBody = z.object({ type: z.enum(['adoption', 'view', 'feedback']).default('adoption'), feedback: z.unknown().optional(), }); export const KnowledgeListQuery = PaginationQuery.extend({ lifecycle: z.string().optional(), kind: z.string().optional(), category: z.string().optional(), language: z.string().optional(), knowledgeType: z.string().optional(), scope: z.string().optional(), keyword: z.string().optional(), tag: z.string().optional(), source: z.string().optional(), }); // ═══ Guard Rules ═════════════════════════════════ export const CreateGuardRuleBody = z .object({ name: z.string().min(1).optional(), ruleId: z.string().min(1).optional(), description: z.string().optional(), message: z.string().optional(), pattern: z.string().min(1, 'pattern is required'), severity: z.enum(['error', 'warning', 'info']).default('warning'), category: z.string().optional(), sourceRecipeId: z.string().optional(), sourceReason: z.string().optional(), note: z.string().optional(), languages: z.array(z.string()).optional(), dimension: z.string().optional(), }) .refine((data) => data.name || data.ruleId, { message: 'Either name or ruleId is required', }); export const BatchEnableBody = BatchIds; export const BatchDisableBody = BatchIds.extend({ reason: z.string().optional(), }); export const DisableRuleBody = z.object({ reason: z.string().optional(), }); export const CheckCodeBody = z.object({ code: z.string().min(1, 'code is required'), language: z.string().optional(), ruleIds: z.array(z.string()).optional(), }); export const ImportFromRecipeBody = z.object({ recipeId: z.string().min(1, 'recipeId is required'), rules: z.array(z.record(z.string(), z.unknown())).min(1, 'rules array must not be empty'), }); export const GuardRulesListQuery = PaginationQuery.extend({ severity: z.string().optional(), category: z.string().optional(), sourceRecipe: z.string().optional(), keyword: z.string().optional(), enabled: z .enum(['true', 'false']) .optional() .transform((v) => (v === undefined ? undefined : v === 'true')), }); export const ComplianceQuery = z.object({ path: z.string().optional(), maxErrors: z.coerce.number().int().min(0).default(0), maxWarnings: z.coerce.number().int().min(0).default(20), minScore: z.coerce.number().int().min(0).max(100).default(70), maxFiles: z.coerce.number().int().min(1).max(10000).default(500), }); // ═══ Search ══════════════════════════════════════ export const SearchQuery = PaginationQuery.extend({ q: z.string().min(1, 'search query is required'), type: z.enum(['all', 'recipe', 'solution', 'rule', 'candidate']).default('all'), mode: z.enum(['keyword', 'bm25', 'semantic']).default('keyword'), groupByKind: z .enum(['true', 'false']) .optional() .transform((v) => v === 'true'), }); export const ContextAwareSearchBody = z.object({ keyword: z.string().min(1, 'keyword is required'), limit: z.number().int().min(1).max(100).default(10), language: z.string().optional(), sessionHistory: z.array(z.record(z.string(), z.unknown())).optional(), }); export const SimilarityBody = z.object({ code: z.string().optional(), targetName: z.string().optional(), candidateId: z.string().optional(), candidate: z .object({ title: z.string().optional(), summary: z.string().optional(), code: z.string().optional(), pattern: z.string().optional(), usageGuide: z.string().optional(), markdown: z.string().optional(), }) .optional(), }); // ═══ Candidates ══════════════════════════════════ export const EnrichBody = z.object({ candidateIds: z.array(z.string().min(1)).min(1).max(20), }); export const BootstrapRefineBody = z.object({ candidateIds: z.array(z.string().min(1)).optional(), userPrompt: z.string().optional(), dryRun: z.boolean().default(false), }); export const RefinePreviewBody = z.object({ candidateId: z.string().min(1, 'candidateId is required'), userPrompt: z.string().min(1, 'userPrompt is required'), }); export const RefineApplyBody = z.object({ candidateId: z.string().min(1, 'candidateId is required'), userPrompt: z.string().optional(), preview: z.record(z.string(), z.unknown()).optional(), }); // ═══ Guard (file check) ══════════════════════════ export const GuardFileBody = z.object({ filePath: z.string().min(1, 'filePath is required'), content: z.string().optional(), language: z.string().optional(), }); export const GuardBatchBody = z.object({ files: z .array(z.object({ filePath: z.string().min(1), content: z.string().optional(), language: z.string().optional(), })) .min(1, 'files array must not be empty') .max(50, 'maximum 50 files per batch'), }); // ═══ Skills ══════════════════════════════════════ export const CreateSkillBody = z.object({ name: z.string().min(1, 'name is required'), description: z.string().min(1, 'description is required'), content: z.string().min(1, 'content is required'), overwrite: z.boolean().default(false), createdBy: z.string().default('manual'), }); export const UpdateSkillBody = z .object({ description: z.string().optional(), content: z.string().optional(), }) .refine((data) => data.description || data.content, { message: 'At least one of description or content must be provided', }); // ═══ Task (unified dispatch) ═════════════════════ export const TaskDispatchBody = z .object({ operation: z.string().min(1, 'operation is required'), // 各操作的参数透传为 passthrough(具体校验在 handler 中) }) .passthrough(); // ═══ Modules ═════════════════════════════════════ export const ScanFolderBody = z.object({ path: z.string().min(1, 'path is required'), options: z.record(z.string(), z.unknown()).optional(), }); export const ScanTargetBody = z .object({ target: z.record(z.string(), z.unknown()).optional(), targetName: z.string().optional(), options: z.record(z.string(), z.unknown()).optional(), }) .refine((data) => data.target || data.targetName, { message: 'Either target or targetName is required', }); export const ScanProjectBody = z.object({ options: z.record(z.string(), z.unknown()).optional(), }); export const ModuleBootstrapBody = z.object({ maxFiles: z.number().int().min(1).max(10000).default(500), skipGuard: z.boolean().default(false), contentMaxLines: z.number().int().min(1).max(10000).default(120), }); export const ModuleRescanBody = z.object({ reason: z.string().optional(), dimensions: z.array(z.string()).optional(), }); // ═══ Graph Search ════════════════════════════════ export const GraphQuery = z.object({ nodeId: z.string().min(1, 'nodeId is required'), nodeType: z.string().min(1, 'nodeType is required'), relation: z.string().optional(), direction: z.enum(['both', 'in', 'out']).default('both'), }); export const GraphImpactQuery = z.object({ nodeId: z.string().min(1, 'nodeId is required'), nodeType: z.string().min(1, 'nodeType is required'), maxDepth: z.coerce.number().int().min(1).max(5).default(3), }); // ═══ AI Routes ═══════════════════════════════════ export const AiLangBody = z.object({ lang: z.enum(['zh', 'en'], { message: 'lang must be "zh" or "en"' }), }); export const AiConfigBody = z.object({ provider: z.string().min(1, 'provider is required'), model: z.string().optional(), }); export const AiSummarizeBody = z.object({ code: z.string().min(1, 'code is required'), language: z.string().optional(), }); export const AiTranslateBody = z.object({ summary: z.string().optional(), usageGuide: z.string().optional(), }); export const AiChatBody = z.object({ prompt: z.string().min(1, 'prompt is required'), history: z.array(z.record(z.string(), z.unknown())).default([]), lang: z.string().optional(), conversationId: z.string().optional(), sseSessionId: z.string().optional(), }); export const AiStreamBody = z.object({ prompt: z.string().min(1, 'prompt is required'), history: z.array(z.record(z.string(), z.unknown())).default([]), lang: z.string().optional(), }); export const AiToolBody = z.object({ tool: z.string().min(1, 'tool name is required'), params: z.record(z.string(), z.unknown()).default({}), }); export const AiTaskBody = z.object({ task: z.string().min(1, 'task name is required'), params: z.record(z.string(), z.unknown()).default({}), }); export const AiFormatUsageGuideBody = z.object({ text: z.string().optional(), }); export const AiEnvConfigBody = z.object({ provider: z.string().min(1, 'provider is required'), model: z.string().optional(), apiKey: z.string().optional(), proxy: z.string().optional(), embedProvider: z.string().optional(), embedModel: z.string().optional(), embedBaseUrl: z.string().optional(), embedApiKey: z.string().optional(), }); // ═══ Extract Routes ══════════════════════════════ export const ExtractPathBody = z.object({ relativePath: z.string().min(1, 'relativePath is required'), projectRoot: z.string().optional(), }); export const ExtractTextBody = z.object({ text: z.string().min(1, 'text is required'), language: z.string().optional(), relativePath: z.string().optional(), projectRoot: z.string().optional(), }); // ═══ Auth Routes ═════════════════════════════════ export const AuthLoginBody = z.object({ username: z.string().min(1, '用户名不能为空'), password: z.string().min(1, '密码不能为空'), }); // ═══ Commands Routes ═════════════════════════════ export const FileReadQuery = z.object({ path: z.string().min(1, 'path is required'), }); export const FileSaveBody = z.object({ path: z.string().min(1, 'path is required'), content: z.string({ message: 'content is required' }), }); // ═══ Wiki Routes ═════════════════════════════════ /* Wiki validation stays as inline path-param check (wildcard route) */ // ═══ Remote Routes ═══════════════════════════════ export const RemoteSendBody = z.object({ command: z .string() .min(1, 'command is required') .transform((s) => s.trim()), }); export const RemoteNotifyBody = z.object({ text: z .string() .min(1, 'text is required') .transform((s) => s.trim()), }); export const RemoteResultBody = z.object({ result: z.string().optional(), status: z.string().default('completed'), }); export const RemoteHistoryQuery = z.object({ limit: z.coerce.number().int().min(1).max(100).default(20), });