diff --git a/docs/development/backend.md b/docs/development/backend.md index 12a28c3..dc30957 100644 --- a/docs/development/backend.md +++ b/docs/development/backend.md @@ -37,7 +37,7 @@ SQLite + bun:sqlite + Drizzle ORM。 | `providers.ts` | createProvider、getProvider、listProviders、listProviderOptions、updateProvider、deleteProvider | | `models.ts` | createModel、getModel、listModels、getModelWithProvider、getModelsByProviderId、updateModel、deleteModel | | `conversations.ts` | createConversation、getConversation、listConversations、updateConversation、updateConversationTimestamp、deleteConversation、createMessage、createMessages、listMessages | -| `materials.ts` | createMaterial、getMaterial、listMaterials、deleteMaterial | +| `materials.ts` | createMaterial、getMaterial、listMaterials、deleteMaterial、approveMaterial、discardMaterial、retryMaterial | 输入输出类型来自 `src/shared/api.ts`。 @@ -66,14 +66,31 @@ SQLite + bun:sqlite + Drizzle ORM。 ## 素材 API -| 方法 | 路径 | 说明 | -| ------ | ---------------------------------- | ---------------------- | -| GET | `/api/projects/:id/materials` | 列出项目下素材(分页) | -| POST | `/api/projects/:id/materials` | 创建素材 | -| GET | `/api/projects/:id/materials/:mid` | 获取素材详情 | -| DELETE | `/api/projects/:id/materials/:mid` | 删除素材(软删除) | +| 方法 | 路径 | 说明 | +| ------ | ------------------------------------------ | ------------------------------- | +| GET | `/api/projects/:id/materials` | 列出项目下素材(分页+状态筛选) | +| POST | `/api/projects/:id/materials` | 创建素材 | +| GET | `/api/projects/:id/materials/:mid` | 获取素材详情 | +| DELETE | `/api/projects/:id/materials/:mid` | 删除素材(软删除) | +| POST | `/api/projects/:id/materials/:mid/approve` | 审核通过(需 review 状态) | +| POST | `/api/projects/:id/materials/:mid/discard` | 审核放弃(需 review 状态) | +| POST | `/api/projects/:id/materials/:mid/retry` | 重试处理(需 failed 状态) | -校验:description 必填非空,associatedDate 必填 YYYY-MM-DD,项目须存在且 active 且未删除,素材归属校验不匹配返回 403。 +素材状态流转:`pending → processing → review → approved/discarded`,失败分支 `processing → failed`,用户重试 `failed → pending`。共 6 种状态:`pending`、`processing`、`review`、`approved`、`discarded`、`failed`。 + +素材类型:`general`(通用)和 `meeting`(会议),创建时可选,默认 `general`。 + +校验:description 必填非空,associatedDate 必填 YYYY-MM-DD,项目须存在且 active 且未删除,素材归属校验不匹配返回 403。processing 状态禁止删除(409)。approve/discard 仅限 review 状态(409),retry 仅限 failed 状态(409)。 + +## 素材处理引擎 + +`src/server/processing/`: + +- `processor.ts`:`MaterialProcessor` 类 — 后台定时扫描 pending 素材,按 FIFO 顺序处理(每 5 秒扫描一次),每次处理最多重试 3 次,成功后 status=review 并设置 processedContent,全部失败后 status=failed。启动时自动恢复所有 processing 状态的素材为 pending(`recoverStuckMaterials`)。 +- `templates/`:处理模板目录 — `general.ts`(通用模板)和 `meeting.ts`(会议模板),当前为占位实现(原样回显 description)。`index.ts` 导出 `ProcessingTemplate` 类型和 `getTemplate(type)` 映射函数。 +- `index.ts`:`startMaterialProcessor(db, logger)` — 工厂函数,创建并启动处理器实例。 + +处理器在 `bootstrap.ts` 中启动,shutdown 时先停止处理器再关闭数据库。 ## 聊天 API diff --git a/drizzle/0006_material_processing.sql b/drizzle/0006_material_processing.sql new file mode 100644 index 0000000..a90249c --- /dev/null +++ b/drizzle/0006_material_processing.sql @@ -0,0 +1,24 @@ +-- 扩展 materials 表:新增 material_type 和 processed_content 字段,更新 status CHECK 约束以支持处理流水线状态 + +CREATE TABLE `materials_new` ( + `id` text PRIMARY KEY NOT NULL, + `project_id` text NOT NULL, + `associated_date` text NOT NULL, + `description` text NOT NULL, + `material_type` text NOT NULL DEFAULT 'general' CHECK (`material_type` IN ('general', 'meeting')), + `processed_content` text, + `status` text NOT NULL DEFAULT 'pending' CHECK (`status` IN ('pending', 'processing', 'review', 'approved', 'discarded', 'failed')), + `created_at` text NOT NULL, + `updated_at` text NOT NULL, + `deleted_at` text, + FOREIGN KEY (`project_id`) REFERENCES `projects`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +INSERT INTO `materials_new` (`id`, `project_id`, `associated_date`, `description`, `material_type`, `processed_content`, `status`, `created_at`, `updated_at`, `deleted_at`) +SELECT `id`, `project_id`, `associated_date`, `description`, 'general', NULL, `status`, `created_at`, `updated_at`, `deleted_at` FROM `materials`; +--> statement-breakpoint +DROP TABLE `materials`; +--> statement-breakpoint +ALTER TABLE `materials_new` RENAME TO `materials`; +--> statement-breakpoint +CREATE INDEX `materials_project_id_idx` ON `materials` (`project_id`); diff --git a/src/server/bootstrap.ts b/src/server/bootstrap.ts index 1b7df44..9335683 100644 --- a/src/server/bootstrap.ts +++ b/src/server/bootstrap.ts @@ -4,11 +4,13 @@ import type { RuntimeMode } from "../shared/api"; import type { ResolvedConfig, ResolvedLoggingConfig } from "./config/types"; import type { MigrationRecord } from "./db/load-migrations"; import type { Logger } from "./logger"; +import type { MaterialProcessor } from "./processing"; import type { StartServerOptions } from "./server"; import { loadServerConfig } from "./config"; import { createDatabase, loadMigrationsFromDir, runMigrations } from "./db"; import { createConsoleFallback, createRuntimeLogger } from "./logger"; +import { startMaterialProcessor } from "./processing"; import { startServer } from "./server"; export interface BootstrapDependencies { @@ -66,8 +68,11 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr const db = createDatabase(config.dataDir, logger!); runMigrations(db, migrations, config.dataDir, logger!); + const processor: MaterialProcessor = startMaterialProcessor(db, logger!.child({ component: "processor" })); + const shutdown = () => { logger?.info("收到退出信号,开始优雅关闭"); + processor.stop(); db.close(); logger?.flush(); exit(0); diff --git a/src/server/db/materials.ts b/src/server/db/materials.ts index 56248b1..6d3000b 100644 --- a/src/server/db/materials.ts +++ b/src/server/db/materials.ts @@ -2,12 +2,37 @@ import type Database from "bun:sqlite"; import { and, desc, eq } from "drizzle-orm"; -import type { CreateMaterialRequest, Material, MaterialStatus } from "../../shared/api"; +import type { CreateMaterialRequest, Material, MaterialStatus, MaterialType } from "../../shared/api"; import type { Logger } from "../logger"; import { notDeleted, paginateQuery, softDeleteRecord, timestamp, wrap } from "./connection"; import { materials, projects } from "./schema"; +const ALLOWED_MATERIAL_TYPES: MaterialType[] = ["general", "meeting"]; + +export function approveMaterial( + raw: Database, + projectId: string, + materialId: string, + _logger: Logger, +): { error: string; status: number } | { material: Material } { + const db = wrap(raw); + const row = db + .select() + .from(materials) + .where(and(eq(materials.id, materialId), notDeleted(materials))) + .get(); + if (!row) return { error: "素材不存在", status: 404 }; + if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 }; + if (row.status !== "review") return { error: "仅待审核素材可通过", status: 409 }; + + const now = timestamp(); + db.update(materials).set({ status: "approved", updatedAt: now }).where(eq(materials.id, materialId)).run(); + + const updated = db.select().from(materials).where(eq(materials.id, materialId)).get(); + return { material: toMaterial(updated!) }; +} + export function createMaterial( raw: Database, projectId: string, @@ -31,6 +56,11 @@ export function createMaterial( return { error: "associatedDate 格式错误,要求 YYYY-MM-DD", status: 400 }; } + const materialType: MaterialType = request.materialType ?? "general"; + if (!ALLOWED_MATERIAL_TYPES.includes(materialType)) { + return { error: "materialType 无效,仅支持 general/meeting", status: 400 }; + } + const id = crypto.randomUUID(); const now = timestamp(); @@ -40,6 +70,7 @@ export function createMaterial( createdAt: now, description, id, + materialType, projectId, status: "pending", updatedAt: now, @@ -64,11 +95,37 @@ export function deleteMaterial( .get(); if (!row) return { error: "素材不存在", status: 404 }; if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 }; + if (row.status === "processing") { + return { error: "处理中的素材不可删除", status: 409 }; + } softDeleteRecord(db, materials, materialId); return { success: true }; } +export function discardMaterial( + raw: Database, + projectId: string, + materialId: string, + _logger: Logger, +): { error: string; status: number } | { material: Material } { + const db = wrap(raw); + const row = db + .select() + .from(materials) + .where(and(eq(materials.id, materialId), notDeleted(materials))) + .get(); + if (!row) return { error: "素材不存在", status: 404 }; + if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 }; + if (row.status !== "review") return { error: "仅待审核素材可放弃", status: 409 }; + + const now = timestamp(); + db.update(materials).set({ status: "discarded", updatedAt: now }).where(eq(materials.id, materialId)).run(); + + const updated = db.select().from(materials).where(eq(materials.id, materialId)).get(); + return { material: toMaterial(updated!) }; +} + export function getMaterial( raw: Database, projectId: string, @@ -107,12 +164,40 @@ export function listMaterials( }); } +export function retryMaterial( + raw: Database, + projectId: string, + materialId: string, + _logger: Logger, +): { error: string; status: number } | { material: Material } { + const db = wrap(raw); + const row = db + .select() + .from(materials) + .where(and(eq(materials.id, materialId), notDeleted(materials))) + .get(); + if (!row) return { error: "素材不存在", status: 404 }; + if (row.projectId !== projectId) return { error: "素材不属于该项目", status: 403 }; + if (row.status !== "failed") return { error: "仅失败素材可重试", status: 409 }; + + const now = timestamp(); + db.update(materials) + .set({ processedContent: null, status: "pending", updatedAt: now }) + .where(eq(materials.id, materialId)) + .run(); + + const updated = db.select().from(materials).where(eq(materials.id, materialId)).get(); + return { material: toMaterial(updated!) }; +} + function toMaterial(row: typeof materials.$inferSelect): Material { return { associatedDate: row.associatedDate, createdAt: row.createdAt, description: row.description, id: row.id, + materialType: row.materialType, + processedContent: row.processedContent, projectId: row.projectId, status: row.status, updatedAt: row.updatedAt, diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 6438338..1c8460a 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -57,10 +57,16 @@ export const materials = sqliteTable( ...baseColumns, associatedDate: text("associated_date").notNull(), description: text("description").notNull(), + materialType: text("material_type", { enum: ["general", "meeting"] }) + .notNull() + .default("general"), + processedContent: text("processed_content"), projectId: text("project_id") .notNull() .references(() => projects.id), - status: text("status", { enum: ["pending", "approved", "discarded"] }) + status: text("status", { + enum: ["pending", "processing", "review", "approved", "discarded", "failed"], + }) .notNull() .default("pending"), }, diff --git a/src/server/processing/index.ts b/src/server/processing/index.ts new file mode 100644 index 0000000..d1ea028 --- /dev/null +++ b/src/server/processing/index.ts @@ -0,0 +1,14 @@ +import type Database from "bun:sqlite"; + +import type { Logger } from "../logger"; + +import { MaterialProcessor } from "./processor"; + +export { MaterialProcessor, type ProcessableMaterial } from "./processor"; +export { getTemplate, type ProcessingTemplate } from "./templates"; + +export function startMaterialProcessor(db: Database, logger: Logger): MaterialProcessor { + const processor = new MaterialProcessor(db, logger); + processor.start(); + return processor; +} diff --git a/src/server/processing/processor.ts b/src/server/processing/processor.ts new file mode 100644 index 0000000..2dc68c4 --- /dev/null +++ b/src/server/processing/processor.ts @@ -0,0 +1,151 @@ +import type Database from "bun:sqlite"; + +import type { MaterialType } from "../../shared/api"; +import type { Logger } from "../logger"; + +import { and, asc, eq } from "drizzle-orm"; + +import { notDeleted, timestamp, wrap } from "../db/connection"; +import { materials } from "../db/schema"; + +import { getTemplate } from "./templates"; + +const MAX_RETRIES = 3; +const DEFAULT_INTERVAL_MS = 5000; + +export interface ProcessableMaterial { + description: string; + id: string; + materialType: MaterialType; +} + +export class MaterialProcessor { + private readonly db: Database; + private readonly logger: Logger; + private timer: ReturnType | null = null; + private running = false; + + constructor(db: Database, logger: Logger) { + this.db = db; + this.logger = logger.child({ component: "material-processor" }); + } + + recoverStuckMaterials(): number { + const db = wrap(this.db); + const now = timestamp(); + const restored = db + .update(materials) + .set({ status: "pending", updatedAt: now }) + .where(and(eq(materials.status, "processing"), notDeleted(materials))) + .returning({ id: materials.id }) + .all(); + + const count = restored.length; + if (count > 0) { + this.logger.info({ count }, "恢复卡住的素材到 pending 状态"); + } + return count; + } + + start(intervalMs: number = DEFAULT_INTERVAL_MS): void { + const recovered = this.recoverStuckMaterials(); + this.logger.info({ intervalMs, recovered }, "素材处理器启动"); + this.timer = setInterval(() => { + void this.tick(); + }, intervalMs); + } + + stop(): void { + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + this.running = false; + this.logger.info("素材处理器停止"); + } + + private async tick(): Promise { + if (this.running) { + this.logger.debug("上一轮处理尚未完成,跳过本次扫描"); + return; + } + this.running = true; + try { + await this.processNext(); + } catch (error: unknown) { + this.logger.error({ error: error instanceof Error ? error.message : String(error) }, "处理过程中发生未捕获错误"); + } finally { + this.running = false; + } + } + + async processNext(): Promise { + const db = wrap(this.db); + const row = db + .select() + .from(materials) + .where(and(eq(materials.status, "pending"), notDeleted(materials))) + .orderBy(asc(materials.createdAt)) + .limit(1) + .get(); + + if (!row) { + this.logger.debug("无待处理素材"); + return; + } + + const processingAt = timestamp(); + db.update(materials).set({ status: "processing", updatedAt: processingAt }).where(eq(materials.id, row.id)).run(); + + const material: ProcessableMaterial = { + description: row.description, + id: row.id, + materialType: row.materialType as MaterialType, + }; + + let lastError: unknown; + let success = false; + + for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { + try { + const result = await this.processOne(material); + const finishedAt = timestamp(); + db.update(materials) + .set({ processedContent: result, status: "review", updatedAt: finishedAt }) + .where(eq(materials.id, row.id)) + .run(); + this.logger.info({ attempt, materialId: row.id }, "素材处理成功"); + success = true; + break; + } catch (error: unknown) { + lastError = error; + this.logger.warn( + { + attempt, + error: error instanceof Error ? error.message : String(error), + materialId: row.id, + }, + `素材处理第 ${attempt} 次失败`, + ); + } + } + + if (!success) { + const failedAt = timestamp(); + db.update(materials).set({ status: "failed", updatedAt: failedAt }).where(eq(materials.id, row.id)).run(); + this.logger.warn( + { + error: lastError instanceof Error ? lastError.message : String(lastError), + materialId: row.id, + }, + `素材处理 ${MAX_RETRIES} 次均失败,标记为 failed`, + ); + } + } + + protected processOne(material: ProcessableMaterial): Promise { + const template = getTemplate(material.materialType); + // TODO: 替换为真实 AI Agent 调用 + return Promise.resolve(template.outputTemplate.replace("{description}", material.description)); + } +} diff --git a/src/server/processing/templates/general.ts b/src/server/processing/templates/general.ts new file mode 100644 index 0000000..779aec4 --- /dev/null +++ b/src/server/processing/templates/general.ts @@ -0,0 +1,4 @@ +export const GENERAL_TEMPLATE = { + outputTemplate: "{description}", + systemPrompt: "通用素材处理", +} as const; diff --git a/src/server/processing/templates/index.ts b/src/server/processing/templates/index.ts new file mode 100644 index 0000000..fb2576b --- /dev/null +++ b/src/server/processing/templates/index.ts @@ -0,0 +1,18 @@ +import type { MaterialType } from "../../../shared/api"; + +import { GENERAL_TEMPLATE } from "./general"; +import { MEETING_TEMPLATE } from "./meeting"; + +export interface ProcessingTemplate { + outputTemplate: string; + systemPrompt: string; +} + +const TEMPLATES: Record = { + general: GENERAL_TEMPLATE, + meeting: MEETING_TEMPLATE, +}; + +export function getTemplate(type: MaterialType): ProcessingTemplate { + return TEMPLATES[type]; +} diff --git a/src/server/processing/templates/meeting.ts b/src/server/processing/templates/meeting.ts new file mode 100644 index 0000000..5c76500 --- /dev/null +++ b/src/server/processing/templates/meeting.ts @@ -0,0 +1,4 @@ +export const MEETING_TEMPLATE = { + outputTemplate: "{description}", + systemPrompt: "会议素材处理", +} as const; diff --git a/src/server/routes/materials/approve.ts b/src/server/routes/materials/approve.ts new file mode 100644 index 0000000..bbd9695 --- /dev/null +++ b/src/server/routes/materials/approve.ts @@ -0,0 +1,29 @@ +import type Database from "bun:sqlite"; + +import type { RuntimeMode } from "../../../shared/api"; +import type { Logger } from "../../logger"; + +import { approveMaterial } from "../../db/materials"; +import { createApiError, jsonResponse } from "../../helpers"; +import { validateIdParam } from "../../middleware"; + +export function handleApproveMaterial(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response { + const url = new URL(req.url); + const parts = url.pathname.split("/"); + const projectIdStr = parts[3]; + const materialIdStr = parts[5]; + + const validatedProject = validateIdParam(projectIdStr ?? "", mode); + if (validatedProject instanceof Response) return validatedProject; + + const validatedMaterial = validateIdParam(materialIdStr ?? "", mode); + if (validatedMaterial instanceof Response) return validatedMaterial; + + const result = approveMaterial(db, validatedProject.id, validatedMaterial.id, logger); + if ("error" in result) { + return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status }); + } + + logger.info({ materialId: validatedMaterial.id, projectId: validatedProject.id }, "素材审核通过"); + return jsonResponse(result, { mode, status: 201 }); +} diff --git a/src/server/routes/materials/discard.ts b/src/server/routes/materials/discard.ts new file mode 100644 index 0000000..aff3291 --- /dev/null +++ b/src/server/routes/materials/discard.ts @@ -0,0 +1,29 @@ +import type Database from "bun:sqlite"; + +import type { RuntimeMode } from "../../../shared/api"; +import type { Logger } from "../../logger"; + +import { discardMaterial } from "../../db/materials"; +import { createApiError, jsonResponse } from "../../helpers"; +import { validateIdParam } from "../../middleware"; + +export function handleDiscardMaterial(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response { + const url = new URL(req.url); + const parts = url.pathname.split("/"); + const projectIdStr = parts[3]; + const materialIdStr = parts[5]; + + const validatedProject = validateIdParam(projectIdStr ?? "", mode); + if (validatedProject instanceof Response) return validatedProject; + + const validatedMaterial = validateIdParam(materialIdStr ?? "", mode); + if (validatedMaterial instanceof Response) return validatedMaterial; + + const result = discardMaterial(db, validatedProject.id, validatedMaterial.id, logger); + if ("error" in result) { + return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status }); + } + + logger.info({ materialId: validatedMaterial.id, projectId: validatedProject.id }, "素材已放弃"); + return jsonResponse(result, { mode, status: 201 }); +} diff --git a/src/server/routes/materials/list.ts b/src/server/routes/materials/list.ts index 75599a3..4fb4bb7 100644 --- a/src/server/routes/materials/list.ts +++ b/src/server/routes/materials/list.ts @@ -21,14 +21,15 @@ export function handleListMaterials(req: Request, db: Database, mode: RuntimeMod const pagination = validatePagination(pageParam, pageSizeParam, mode); if (pagination instanceof Response) return pagination; - if (statusParam && statusParam !== "pending" && statusParam !== "approved" && statusParam !== "discarded") { + const ALLOWED_STATUSES = ["pending", "processing", "review", "approved", "discarded", "failed"] as const; + if (statusParam && !(ALLOWED_STATUSES as readonly string[]).includes(statusParam)) { return jsonResponse(createApiError("Invalid status parameter", 400), { mode, status: 400 }); } const result = listMaterials(db, validated.id, { page: pagination.page, pageSize: pagination.pageSize, - status: (statusParam as "approved" | "discarded" | "pending") ?? undefined, + status: (statusParam as (typeof ALLOWED_STATUSES)[number]) ?? undefined, }); return jsonResponse(result, { mode }); diff --git a/src/server/routes/materials/retry.ts b/src/server/routes/materials/retry.ts new file mode 100644 index 0000000..e664107 --- /dev/null +++ b/src/server/routes/materials/retry.ts @@ -0,0 +1,29 @@ +import type Database from "bun:sqlite"; + +import type { RuntimeMode } from "../../../shared/api"; +import type { Logger } from "../../logger"; + +import { retryMaterial } from "../../db/materials"; +import { createApiError, jsonResponse } from "../../helpers"; +import { validateIdParam } from "../../middleware"; + +export function handleRetryMaterial(req: Request, db: Database, mode: RuntimeMode, logger: Logger): Response { + const url = new URL(req.url); + const parts = url.pathname.split("/"); + const projectIdStr = parts[3]; + const materialIdStr = parts[5]; + + const validatedProject = validateIdParam(projectIdStr ?? "", mode); + if (validatedProject instanceof Response) return validatedProject; + + const validatedMaterial = validateIdParam(materialIdStr ?? "", mode); + if (validatedMaterial instanceof Response) return validatedMaterial; + + const result = retryMaterial(db, validatedProject.id, validatedMaterial.id, logger); + if ("error" in result) { + return jsonResponse(createApiError(result.error, result.status), { mode, status: result.status }); + } + + logger.info({ materialId: validatedMaterial.id, projectId: validatedProject.id }, "素材重试已触发"); + return jsonResponse(result, { mode, status: 201 }); +} diff --git a/src/server/server.ts b/src/server/server.ts index 8e05cff..2ba156c 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -256,6 +256,36 @@ export function startServer(options: StartServerOptions) { logger, ), }, + "/api/projects/:id/materials/:mid/approve": { + POST: withErrorHandler( + async (req) => { + const { handleApproveMaterial } = await import("./routes/materials/approve"); + return handleApproveMaterial(req, db, mode, logger); + }, + mode, + logger, + ), + }, + "/api/projects/:id/materials/:mid/discard": { + POST: withErrorHandler( + async (req) => { + const { handleDiscardMaterial } = await import("./routes/materials/discard"); + return handleDiscardMaterial(req, db, mode, logger); + }, + mode, + logger, + ), + }, + "/api/projects/:id/materials/:mid/retry": { + POST: withErrorHandler( + async (req) => { + const { handleRetryMaterial } = await import("./routes/materials/retry"); + return handleRetryMaterial(req, db, mode, logger); + }, + mode, + logger, + ), + }, "/api/projects/:id/restore": { POST: withErrorHandler( async (req) => { diff --git a/src/shared/api.ts b/src/shared/api.ts index 5f2500b..cf1a369 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -31,6 +31,7 @@ export interface CreateConversationRequest { export interface CreateMaterialRequest { associatedDate: string; description: string; + materialType?: MaterialType; } export interface CreateModelRequest { @@ -69,6 +70,8 @@ export interface Material { createdAt: string; description: string; id: string; + materialType: MaterialType; + processedContent: null | string; projectId: string; status: MaterialStatus; updatedAt: string; @@ -85,7 +88,9 @@ export interface MaterialResponse { material: Material; } -export type MaterialStatus = "approved" | "discarded" | "pending"; +export type MaterialStatus = "approved" | "discarded" | "failed" | "pending" | "processing" | "review"; + +export type MaterialType = "general" | "meeting"; export interface Message { content: string; diff --git a/src/web/features/inbox/components/AddMaterialModal.tsx b/src/web/features/inbox/components/AddMaterialModal.tsx index 053feca..0894a12 100644 --- a/src/web/features/inbox/components/AddMaterialModal.tsx +++ b/src/web/features/inbox/components/AddMaterialModal.tsx @@ -1,8 +1,8 @@ -import { App as AntApp, DatePicker, Form, Input, Modal } from "antd"; +import { App as AntApp, DatePicker, Form, Input, Modal, Select } from "antd"; import dayjs from "dayjs"; import { useEffect, useState } from "react"; -import type { CreateMaterialRequest, Material } from "../types"; +import type { CreateMaterialRequest, Material, MaterialType } from "../types"; interface AddMaterialModalProps { onAdd: (body: CreateMaterialRequest) => Promise; @@ -13,8 +13,14 @@ interface AddMaterialModalProps { interface FormValues { associatedDate: dayjs.Dayjs; description: string; + materialType: MaterialType; } +const MATERIAL_TYPE_OPTIONS = [ + { label: "通用", value: "general" }, + { label: "会议", value: "meeting" }, +]; + export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModalProps) { const { message } = AntApp.useApp(); const [form] = Form.useForm(); @@ -29,6 +35,7 @@ export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModal const body: CreateMaterialRequest = { associatedDate: values.associatedDate.format("YYYY-MM-DD"), description: values.description, + materialType: values.materialType, }; setSubmitting(true); @@ -55,7 +62,7 @@ export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModal >
void handleFinish(values)} > @@ -69,6 +76,9 @@ export function AddMaterialModal({ onAdd, onOpenChange, open }: AddMaterialModal + +