AI视频创作Agent实战03:DeepSeek文案裂变与草稿版本控制

AI视频创作Agent实战03:DeepSeek文案裂变与草稿版本控制

把一段文案发给模型,再把返回结果放进文本框,是演示程序中最容易完成的一步。进入真实创作流程后,问题会变成:用户连点两次是否调用两次?生成过程中修改原文,旧结果还能回填吗?两页同时编辑同一份草稿,后保存的人会不会覆盖前一个人的修改?

本文沿着一个正在开发的视频创作 Agent 的实际链路,拆解文案优化、裂变、候选校验、草稿版本控制和结果恢复。代码基于 2026 年 9 月 8 日工作区快照,文中业务样例均明确为虚构。重点是已经实现的工程机制,不把模型返回一段文字等同于视频生成和平台发布都已完成。

1. 文案是生产输入,需要可验证的结构

新版创作页提供四种文案模式:keep 保留原文,optimize 优化一条,fission 按原文裂变,optimize_fission 先优化再裂变。前两种的目标生成数量必须为 1;新版单次文案生成、候选列表与成片批次的上限为 15 条。

这个 15 是视频数量的上限,与发布平台数量不同。生成 5 条视频并选择 3 个平台,会规划 15 个发布任务,不应该先生成 15 条内容相同的视频。旧任务接口仍限制每个平台最多 5 条、每批合计最多 9 条(server/store.mjs:109);15 条上限属于显式启用 productionBatch 的新版 /v2 创作流程。

虚构样例可以设为:"澄山工作室展示木作打磨过程,仅提供到店体验。"事实锁填写"澄山工作室"。裂变可以分别从工艺步骤、空间观察等角度组织表达,但不能自行增加不存在的价格、效果承诺或客户评价。这些约束一部分写进提示词,另一部分必须在模型返回后用代码验证。

源码位置(相对项目根目录) 作用
shared/production-v2.mjs:22 模式、15 条上限及草稿结构
shared/production-v2.mjs:327 候选数量、字段、重复和事实锁校验
server/script-generation.mjs:10 DeepSeek 文案专用配置与生成任务
server/provider.mjs:202 Chat Completions 请求封装
server/index.mjs:1744 新版文案生成接口、版本和请求标识校验
server/production-repository.mjs:580 SQLite 草稿条件更新
components/video-creation-wizard.tsx:556:1270 保存草稿后提交文案任务
lib/script-candidates.ts:33 判断结果是否可以回填
tests/production-v2.test.mjstests/script-candidates.test.mjs 模型输出规则与前端编辑保护
tests/v2-stage1.test.mjstests/next-batch-draft.test.mjs 本地模拟服务、接口冲突及下一份草稿恢复
环境项 项目基线 本篇用途
Node.js 本机 24.14.1;项目要求不低于 22.13.0 HTTP、队列与测试
React 19.2.6 文案编辑、保存状态和轮询回填
TypeScript 5.9.3 前端草稿与候选类型检查
vinext 1.0.0-beta.5 当前页面开发与构建
SQLite Node.js 内置 node:sqlite 草稿版本持久化

本文描述项目当前适配分支,不将测试中的模型名称当作服务商长期不变的可用性承诺。模型与接口兼容性应在真实接入时另行核对。

2. DeepSeek 文案配置与图片理解配置分开

项目中的 semantic 配置负责 DeepSeek 文案与语义任务,不能因为图片理解配置可用,就认为文案生成也可用。server/script-generation.mjs:10 的原始代码如下:

js 复制代码
function scriptConfig(directory) {
  const config = loadSettings(directory).semantic;
  if (!config?.baseUrl || !config?.model || !config?.key)
    throw new UserError(
      'DeepSeek 文案接口尚未配置,请在"接口设置 → DeepSeek 文案与语义切片"保存配置。',
      409,
      'SCRIPT_PROVIDER_MISSING',
    );
  return config;
}

这段代码检查的是配置字段是否齐全,不等于验证凭据有效或远端服务可达。真正调用时仍可能出现鉴权失败、超时和不符合约定的结果。keep 分支不需要调用外部模型,可以直接保留原始文本;其余三种模式在任务创建时检查这份专用配置。

前端使用 POST /api/video/v2/drafts/{draftId}/script-generation 创建任务,随后通过 /api/video/v2/script-generations 刷新状态。后端公共封装使用 /chat/completions,本地测试把基地址设为带 /v1 的地址,因此断言实际请求路径为 /v1/chat/completions。这两个路径分别属于业务 API 和模型 API,排查错误时不要混用。

请求体要求 JSON 对象,并且禁用流式返回。针对当前代码中的 DeepSeek v4 模型分支,使用 thinking: { type: 'disabled' };阿里云分支则有自己的字段。下面是 server/provider.mjs:218 的原始节选:

js 复制代码
model: config.model,
stream: false,
...(config.provider === 'aliyun' ? { enable_thinking: false } : {}),
...(config.model.startsWith('deepseek-v4-')
  ? { thinking: { type: 'disabled' } }
  : {}),
response_format: { type: 'json_object' },
messages: [
  { role: 'system', content: system },
  { role: 'user', content },
],

模型输入中的用户原文被当作资料,系统提示明确约束输出结构和不可编造的事实。但提示词只能表达规则,无法替代后端校验,也不能保证自然语言层面完全没有事实偏差。

DeepSeek 官方 JSON Output 文档同样要求指定 json_object、在提示中说明 JSON 格式并合理设置输出长度,还提示可能出现空内容。因此 JSON 模式解决输出格式问题,候选数量、事实锁和重复检查仍由业务代码负责。DeepSeek JSON Output 官方说明

3. 候选校验要落实到确定的失败条件

模型返回的结构为 candidates 数组,每项包含标题、独特角度、正文与选择状态。后端先验证条数,再检查字段和事实锁,最后给通过校验的候选生成 ID。候选列表一旦无效,不会伪造一组成功结果继续进入成片流程。

shared/production-v2.mjs:337 的原始节选:

js 复制代码
if (!Array.isArray(value) || (!allowEmpty && !value.length))
  throw new Error('AI 没有返回有效文案候选。');
if (expectedCount !== undefined && value.length !== expectedCount)
  throw new Error(`AI 应返回 ${expectedCount} 条文案候选。`);
if (value.length > MAX_PRODUCTION_VIDEOS)
  throw new Error(`文案候选不能超过 ${MAX_PRODUCTION_VIDEOS} 条。`);

标题最长 60 字,正文最长 2500 字,角度最长 120 字。输出规范里还给模型提示了更短的标题目标,这与服务器接受字段的最大长度不是同一个概念。

比较文本前,项目会转换大小写并移除空白、标点和符号。因此仅改变标点的两条正文仍会被判重复;不同正文如果角度字段完全相同,也会被拒绝。事实锁同样通过归一化后的字符串包含关系检查。这是可执行的基本约束,并不是语义级事实核查:同义表达、隐含承诺、前后矛盾仍需用户审核。

编辑阶段与正式生成阶段还采用不同校验强度。用户把旧正文删空、准备重新输入时,草稿允许暂存不完整候选;进入待生成状态时会严格校验候选字段、重复内容和事实锁。点击"生成视频"还要求已勾选候选数等于 script.countserver/index.mjs:1848),并不是要求草稿中保存的全部候选数等于它。模型新生成候选时,才使用 expectedCount 检查返回总数。这样既保留正常编辑中的短暂空白,也避免混淆草稿列表、模型返回和最终选择三个数量。

4. 幂等键与草稿版本解决不同问题

只在按钮上加 disabled,无法挡住页面刷新、响应丢失后的重试或同一操作的不同请求标识。后端会把生成操作定义为一组稳定字段,server/script-generation.mjs:20 原始代码如下:

js 复制代码
const operationSignature = (draft) =>
  JSON.stringify({
    draftId: draft.id,
    draftRevision: draft.revision,
    mode: draft.script.mode,
    count: draft.script.count,
    source: draft.script.current.trim(),
    factLocks: draft.script.factLocks,
  });

requestKey 回答"这是不是同一次提交",revision 回答"提交基于哪个草稿版本",签名则约束这个版本具体代表哪一组内容。三个条件合起来,才能防止用旧标识悄悄启动另一份新文案。

相同请求标识与相同签名再次到达时,服务返回已有任务;换了请求标识,但草稿版本和签名相同,也复用任务并保存别名。同一个标识用于新版本,会报 IDEMPOTENCY_CONFLICT;同一个草稿版本出现另一份内容,会报 REVISION_PAYLOAD_CONFLICT。已删除任务也有对应记录,旧请求不能轻易把它复活。

任务队列还限制了处于 queued/running 的文案任务数量,达到 5 个时返回 429。这里的 5 指待处理的文案任务,并不是一批可生成 5 条视频。把不同层次的计数写在一起,是项目扩展后很容易出现的混淆。

5. SQLite 乐观锁让旧页面无法覆盖新草稿

前端自动保存并不是无条件覆盖一个 JSON 文件。每次请求都会携带已知版本,数据库只在版本仍一致时更新,同时把版本加一。server/production-repository.mjs:580 的原始代码如下:

js 复制代码
update(id, document, status, expectedRevision) {
  const at = now();
  const result = db
    .prepare(`UPDATE workflow_drafts SET step = ?, status = ?,
    payload_json = ?, revision = revision + 1, updated_at = ?
    WHERE id = ? AND revision = ?`)
    .run(
      document.step,
      status,
      safeJson(document),
      at,
      id,
      expectedRevision,
    );
  return result.changes ? drafts.get(id) : undefined;
},

用虚构的两页面操作说明:A、B 都拿到版本 7;A 先保存,数据库进入版本 8;B 再按版本 7 保存,条件更新不会命中。接口返回版本冲突,前端显示草稿在其他页面更新并提供重新载入入口,而不是把 A 的内容静默抹掉。

这是冲突检测,不是自动合并。用户需要重新核对内容;项目没有宣称提供实时多人协同编辑。自动保存队列负责串行提交当前页的修改,版本号负责跨请求检查,两者各有边界。

点击生成文案时,前端先等待 flushDraft() 完成,再提交服务器确认过的版本。文案任务和草稿也不是同一种存储对象:草稿进入 SQLite,文案任务状态通过现有 store 保存。两类数据各自的恢复路径需要保留,不能把一次 SQL 提交理解为所有外部工作都已完成。

SQLite 支持多个连接同时读取,但同一时刻只有一个写事务;事务中的提交、回滚和锁竞争可以对照官方说明理解。草稿"版本 7 已过期"的含义则来自本项目的条件更新,不是数据库自动理解了表单冲突。SQLite 事务官方说明

6. 结果回填必须尊重生成期间的人工修改

用户在模型处理期间修改了正文,轮询回来时不能简单执行 setCandidates(job.candidates)lib/script-candidates.ts:33 会检查任务完成、草稿内容是否匹配、数量是否一致、候选是否已经接收,以及等待期间候选有没有被编辑。以下为原始节选:

ts 复制代码
if (
  !draft ||
  !job ||
  job.status !== 'completed' ||
  !scriptGenerationMatches(draft, job) ||
  job.candidates.length !== job.count ||
  hasScriptGenerationCandidates(draft, job) ||
  (draft.script.candidates.length > 0 &&
    JSON.stringify(draft.script.candidates) !== candidatesBeforeRequest)
)
  return undefined;
return job.candidates.map((candidate) => ({ ...candidate }));

末行复制每个候选,避免编辑框与任务快照共享对象。匹配逻辑还比较草稿 ID、原文、模式、条数和事实锁。因此,旧任务即使最终成功,也不能覆盖已经切换到另一份输入的页面。

前后端顺序是:编辑原文 → 保存草稿 → 提交版本与请求标识 → 后端复用或创建任务 → 模型调用 → 候选验证 → 保存任务结果 → 前端核对当前草稿 → 复制候选供用户修改 → 再保存草稿。把这条链走通,才算文案能力真正接入创作页。

React 官方文档将一次渲染中的 state 描述为快照,事件处理函数也使用该次渲染对应的值。异步回填不能只依赖"发起请求时看到的表单";本文的匹配逻辑额外检查回填时的草稿和人工修改。React:State as a Snapshot

下图将失败分支放进实际流程,区分"任务失败"和"结果成功但不应覆盖当前表单":
#mermaid-svg-QfeLspoIWAZP37Hp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QfeLspoIWAZP37Hp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QfeLspoIWAZP37Hp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QfeLspoIWAZP37Hp .error-icon{fill:#552222;}#mermaid-svg-QfeLspoIWAZP37Hp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QfeLspoIWAZP37Hp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QfeLspoIWAZP37Hp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QfeLspoIWAZP37Hp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QfeLspoIWAZP37Hp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QfeLspoIWAZP37Hp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QfeLspoIWAZP37Hp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QfeLspoIWAZP37Hp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QfeLspoIWAZP37Hp .marker.cross{stroke:#333333;}#mermaid-svg-QfeLspoIWAZP37Hp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QfeLspoIWAZP37Hp p{margin:0;}#mermaid-svg-QfeLspoIWAZP37Hp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-QfeLspoIWAZP37Hp .cluster-label text{fill:#333;}#mermaid-svg-QfeLspoIWAZP37Hp .cluster-label span{color:#333;}#mermaid-svg-QfeLspoIWAZP37Hp .cluster-label span p{background-color:transparent;}#mermaid-svg-QfeLspoIWAZP37Hp .label text,#mermaid-svg-QfeLspoIWAZP37Hp span{fill:#333;color:#333;}#mermaid-svg-QfeLspoIWAZP37Hp .node rect,#mermaid-svg-QfeLspoIWAZP37Hp .node circle,#mermaid-svg-QfeLspoIWAZP37Hp .node ellipse,#mermaid-svg-QfeLspoIWAZP37Hp .node polygon,#mermaid-svg-QfeLspoIWAZP37Hp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QfeLspoIWAZP37Hp .rough-node .label text,#mermaid-svg-QfeLspoIWAZP37Hp .node .label text,#mermaid-svg-QfeLspoIWAZP37Hp .image-shape .label,#mermaid-svg-QfeLspoIWAZP37Hp .icon-shape .label{text-anchor:middle;}#mermaid-svg-QfeLspoIWAZP37Hp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QfeLspoIWAZP37Hp .rough-node .label,#mermaid-svg-QfeLspoIWAZP37Hp .node .label,#mermaid-svg-QfeLspoIWAZP37Hp .image-shape .label,#mermaid-svg-QfeLspoIWAZP37Hp .icon-shape .label{text-align:center;}#mermaid-svg-QfeLspoIWAZP37Hp .node.clickable{cursor:pointer;}#mermaid-svg-QfeLspoIWAZP37Hp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QfeLspoIWAZP37Hp .arrowheadPath{fill:#333333;}#mermaid-svg-QfeLspoIWAZP37Hp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QfeLspoIWAZP37Hp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QfeLspoIWAZP37Hp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QfeLspoIWAZP37Hp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QfeLspoIWAZP37Hp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QfeLspoIWAZP37Hp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QfeLspoIWAZP37Hp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QfeLspoIWAZP37Hp .cluster text{fill:#333;}#mermaid-svg-QfeLspoIWAZP37Hp .cluster span{color:#333;}#mermaid-svg-QfeLspoIWAZP37Hp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QfeLspoIWAZP37Hp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QfeLspoIWAZP37Hp rect.text{fill:none;stroke-width:0;}#mermaid-svg-QfeLspoIWAZP37Hp .icon-shape,#mermaid-svg-QfeLspoIWAZP37Hp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QfeLspoIWAZP37Hp .icon-shape p,#mermaid-svg-QfeLspoIWAZP37Hp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QfeLspoIWAZP37Hp .icon-shape .label rect,#mermaid-svg-QfeLspoIWAZP37Hp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QfeLspoIWAZP37Hp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QfeLspoIWAZP37Hp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QfeLspoIWAZP37Hp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否



未知或失败



编辑原文和事实锁
先保存草稿与预期版本
数据库版本仍一致
返回冲突并保留待核对修改
提交版本与请求标识
复用已有任务或创建持久任务
保留原文模式
本地建立候选
请求 DeepSeek
响应结果明确且有效
保存原因并等待核对或重试
校验候选并保存任务结果
前端轮询取得候选
输入仍匹配且候选未被人工修改
复制候选到草稿并保存
保留当前编辑而不自动回填

可用两个页面手动复核:同时打开同一草稿,在 A 页改标题并等待保存成功,再在 B 页保存旧版本,预期 B 收到冲突。另一个独立场景是在文案生成期间修改原文,预期旧结果不会自动覆盖新输入。这是读者可执行的验收示例,并非本文新增运行记录;版本冲突部分不需要调用模型,AI 模式生成则仍涉及实际模型请求。

7. 未知结果不能一律自动重试

请求超时只说明本机没有拿到确定响应,远端可能已经完成处理。服务启动时会检查遗留的 sending/unknown 尝试,把相关任务标成 unknown_external。重试接口要求先明确核对,再允许重新请求。

这不能保证服务商侧严格只执行一次,但能避免把网络不确定性直接转换成无限自动重试。普通失败、已完成、正在处理、未知外部结果应分别处理;已完成任务的重试直接返回原任务,不能无条件再计一次生成。

在文案之外,项目已有生成成功后准备下一份草稿的恢复逻辑。队列未真正接受时保留原表单,存储失败时回滚原草稿归档,已归档草稿的迟到自动保存会被拒绝。这些机制的共同目标,是保留用户确认过的输入以及每次生产对应的版本。

8. 如何复跑测试并解释断言

在项目根目录安装好锁定依赖后执行:

powershell 复制代码
node --test tests/production-v2.test.mjs tests/script-candidates.test.mjs
node --test tests/v2-stage1.test.mjs tests/next-batch-draft.test.mjs
npm run test:agent
npx tsc --noEmit

2026 年 9 月 8 日项目已有的全量回归基线为 292 项测试通过,TypeScript 检查通过。本文引用该基线,没有把本地模拟接口测试描述为 DeepSeek 真实账号联调,也没有以测试通过替代实际内容质量评估。

v2-stage1.test.mjs 为三种 AI 模式分别启动本地 HTTP 服务,捕获模型请求并返回构造的候选。它验证候选数量、提示输入、请求路径和请求次数。下面是其中关于幂等冲突的原始断言节选:

js 复制代码
assert.throws(
  () => generator.create({ ...draft, revision: 2 }, requestKey),
  (error) =>
    error.code === 'IDEMPOTENCY_CONFLICT' && error.status === 409,
);

测试还断言同一版本换一个请求标识仍返回原任务 ID,最终模型请求计数仍为 1。只断言 HTTP 200 看不到这类重复调用问题,必须同时观察任务身份和调用计数。

其他有价值的验收点包括:15 条候选可保存、16 条被拒绝;正文或角度重复失败;遗漏事实锁失败;仅有图片理解配置不能开启 DeepSeek 文案;保留原文时尝试记录为空;旧版本保存返回 409;轮询不覆盖修改后的标题、正文、角度和勾选状态;新草稿创建失败不会丢掉上一份草稿。

故障现象 判断依据 修复方向
图片理解可用,文案却无法开始 SCRIPT_PROVIDER_MISSING 检查独立的 semantic 配置
重试提示提交标识冲突 旧 requestKey 对应另一版本 保存新版本并使用新的操作标识
AI 返回两条,却要求三条 候选数量校验失败 检查任务 count 和完整响应,不能补空白凑数
保存时出现 409 草稿版本已前进 重新读取并核对修改,而非盲目覆盖
文案成功但没有覆盖编辑框 输入变化或候选已被编辑 保留当前编辑,确认是否要重新生成
重启后显示外部结果待核对 上次请求处于不确定状态 核对远端结果后决定是否重试

最终验收应覆盖"正常生成一次"和"异常时不丢、不乱、不重复"两条线。文案模块输出的是经校验的候选,成片仍依赖素材准备、图片理解、模板和声音方案;自有声音分句与音色克隆目前仍未接入,不能因为可以上传声音样本就认为能完成克隆配音。

系列前两篇分别讨论素材持久化与无声派生、带人声原视频转写与语义切片;本篇把输入推进到可编辑、可追溯的文案版本。后续将继续介绍素材匹配、视频合成及任务恢复。

相关推荐
火山引擎开发者社区1 小时前
Anker 首届黑客松挑战赛|9 月 7 日报名启动
人工智能
Dawson Zhu3 小时前
工作流与 Agent 的工程选型:从“控制权归属“看 LLM 应用架构
人工智能·语言模型·架构·aigc·agi
计算机源码社3 小时前
【大数据项目实战】基于大数据的影视内容生态综合质量分析与可视化-基于数据挖掘的影视内容类型共现与口碑聚类分析系统
大数据·人工智能·python·数据挖掘·数据分析·毕业设计·课程设计
C^h3 小时前
pytorch 适合初学者 0基础学习
人工智能·pytorch·python
Rocky Ding*3 小时前
【三年面试五年模拟】2026-09-06 拼多多 AI Agent研发岗秋招笔试4道算法题完整题解
论文阅读·人工智能·深度学习·机器学习·aigc·ai-native·拼多多
小柯南敲键盘4 小时前
跨马翻译:AI批量图片翻译工具,跨境电商视频字幕翻译与智能抠图一体搞定
人工智能·python·音视频
luckystar513~4 小时前
Geo + AI:【时空智能体】技术剖析
人工智能·ai·gis·geoai·空间智能体·时空智能体
吨吨ai4 小时前
2026年9月8日|GPT‑6 Astra + Codex:Pro 开发者的 AI Agent 工具链
人工智能·gpt
leoZ2314 小时前
2026-09-09-springboot-cloud-deploy-pitfalls
java·前端·javascript·vue.js·人工智能·spring boot·后端