You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

7.9 KiB

剧本智能拆解 API 对接实现

日期: 2026-02-09
类型: 功能实现
模块: 前端 - 剧本智能拆解弹窗

📋 变更概述

ParseFlowDialog 组件中实现了"智能拆解"功能与后端 API 的完整对接,将用户在预览面板输入的个性化要求和分镜数量参数传递到后端进行 AI 解析。

🎯 背景

之前的"智能拆解"按钮只有 mock 逻辑,未真实调用后端 API。用户点击后仅执行 3 秒延时模拟,无法实际触发剧本智能解析功能。

实现内容

1. API Service 添加解析接口

文件: client/src/services/api/screenplay.ts

新增 parse 方法:

async parse(
  id: string,
  data: {
    customRequirements?: string;
    storyboardCount?: number;
  }
): Promise<{
  jobId: string;
  taskId: string;
  status: string;
  estimatedCredits: number;
}>

简化说明:前端只传递用户输入的两个参数,其他参数由后端使用默认配置。

请求参数映射

  • 前端 customRequirements → 后端 custom_requirements(个性化要求文本)
  • 前端 storyboardCount → 后端 storyboard_count(分镜数量)

注意:其他参数(如 modelautoCreateElements 等)由后端使用默认值,前端无需传递。

2. React Query Hook

文件: client/src/hooks/api/useScreenplays.ts

新增 useParseScreenplay Hook:

export function useParseScreenplay() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: ({ id, data }) => screenplayApi.parse(id, data),
    onSuccess: (_, { id }) => {
      queryClient.invalidateQueries({
        queryKey: screenplayKeys.detail(id),
      });
    },
  });
}

3. 组件集成

文件: client/src/components/features/storyboard/ParseFlowDialog.tsx

导入 Hook

import {
  useUploadScreenplay,
  useParseStatus,
  useParseScreenplayFile,
  useScreenplay,
  useUpdateScreenplay,
  useDeleteScreenplay,
  useParseScreenplay,  // ✅ 新增
} from '@/hooks/api/useScreenplays';

初始化 Mutation

// 智能解析 mutation
const parseScreenplayMutation = useParseScreenplay();

重写 handleSmartBreakdown 函数

核心逻辑

  1. 前置校验:检查是否选中剧本
  2. 调用 API:传递 customRequirementsstoryboardCount 参数
  3. 成功提示:显示任务 ID 和预计消耗积分
  4. 错误处理:捕获异常并显示友好提示
const handleSmartBreakdown = useCallback(
  async (options: { personalizedRequirements: string; storyboardCount: number }) => {
    if (!activeScreenplayId) {
      toast({
        title: '无法拆解',
        description: '请先选择一个剧本',
        variant: 'destructive',
      });
      return;
    }

    setIsBreakingDown(true);

    try {
      const result = await parseScreenplayMutation.mutateAsync({
        id: activeScreenplayId,
        data: {
          customRequirements: options.personalizedRequirements,
          storyboardCount: options.storyboardCount,
        },
      });

      toast({
        title: '解析任务已提交',
        description: `任务 ID: ${result.taskId},预计消耗 ${result.estimatedCredits} 积分`,
      });

      setTimeout(() => {
        setIsBreakingDown(false);
        setViewMode('upload');
      }, 3000);
    } catch (error) {
      console.error('智能拆解失败:', error);
      toast({
        title: '拆解失败',
        description: error instanceof Error ? error.message : '请稍后重试',
        variant: 'destructive',
      });
      setIsBreakingDown(false);
    }
  },
  [activeScreenplayId, parseScreenplayMutation, toast]
);

简化说明:只传递 customRequirementsstoryboardCount 两个用户输入的参数,其他配置由后端处理。

📊 API 接口详情

请求

端点: POST /api/v1/screenplays/{screenplay_id}/parse

Headers:

Authorization: Bearer {JWT_TOKEN}
Content-Type: application/json

Body:

{
  "custom_requirements": "例如:增加更多特写镜头、强调情绪变化、注重场景切换等...",
  "storyboard_count": 6
}

说明

  • 前端只传递 custom_requirementsstoryboard_count 两个参数
  • 其他参数(如 modelauto_create_elements 等)由后端使用默认值

响应

状态码: 202 Accepted

Body:

{
  "success": true,
  "data": {
    "jobId": "019d1234-5678-7abc-9def-0123456789ab",
    "taskId": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
    "status": "pending",
    "estimatedCredits": 50
  },
  "message": "剧本解析任务已提交,请使用 jobId 查询任务状态"
}

🔄 数据流

  1. 用户输入:在"剧本预览"面板输入个性化要求和分镜数量
  2. 点击按钮:点击"智能拆解"触发 handleSmartBreakdown
  3. 参数传递personalizedRequirementscustomRequirementsstoryboardCountstoryboard_count
  4. API 调用POST /api/v1/screenplays/{id}/parse
  5. 异步任务:后端返回 taskId,前端可轮询状态
  6. 用户反馈:Toast 提示任务已提交

⚠️ 注意事项

  1. 必须选中剧本:未选中时会提示"请先选择一个剧本"
  2. 异步执行:解析任务提交后在后台执行,需要轮询状态(TODO)
  3. 积分消耗:每次解析会消耗用户积分,UI 显示预计消耗量
  4. 错误处理:网络错误、权限错误、积分不足等异常均会捕获并提示
  5. 参数简化:前端只传递 customRequirementsstoryboardCount,其他配置由后端使用默认值

🚀 后续优化

1. 任务状态轮询

参考 useParseStatus 实现任务状态轮询机制:

const { data: parseTaskStatus } = useQuery({
  queryKey: ['screenplay-parse-task', result.taskId],
  queryFn: () => apiClient.get(`/tasks/${result.taskId}/status`),
  enabled: !!result.taskId,
  refetchInterval: (query) => {
    const status = query.state.data?.status;
    return status === 'completed' || status === 'failed' ? false : 2000;
  },
});

2. 进度展示

在 Loading 遮罩中显示实时进度:

{isBreakingDown && (
  <div className="absolute inset-0 z-60">
    <Progress value={parseProgress} />
    <p>{parseStatusMessage}</p>
  </div>
)}

3. 结果同步

解析完成后自动刷新:

  • 分镜列表(useStoryboards
  • 项目资源(useResources
  • 剧本详情(useScreenplay

4. 积分余额检查

解析前检查用户积分是否足够:

if (user.credits < result.estimatedCredits) {
  toast({
    title: '积分不足',
    description: '请充值后再试',
    variant: 'destructive',
  });
  return;
}

📝 测试建议

单元测试

  • screenplayApi.parse 方法参数映射正确性
  • useParseScreenplay Hook 成功/失败回调

集成测试

  1. 选中剧本 → 输入参数 → 点击"智能拆解" → 验证 API 调用
  2. 未选中剧本 → 点击按钮 → 验证提示
  3. API 失败 → 验证错误提示

E2E 测试

  1. 完整流程:上传剧本 → 智能拆解 → 等待完成 → 查看分镜列表
  2. 边界场景:积分不足、网络异常、权限不足

🔗 相关文档

验收标准

  • 个性化要求参数正确传递到 customRequirements
  • 分镜数量参数正确传递到 storyboardCount
  • API 调用成功返回 taskIdestimatedCredits
  • 错误场景显示友好提示
  • TypeScript 类型检查无错误

👤 作者

AI Assistant (Claude Sonnet 4.5)

📅 时间线

  • 2026-02-09 22:30: 完成 API Service 和 Hook 实现
  • 2026-02-09 22:45: 完成组件集成和错误处理
  • 2026-02-09 23:00: 完成文档编写