@starreel/mcp 0.1.164 → 0.1.166

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -297,7 +297,8 @@ export const REVIEW_GATES = [
297
297
  export const REVIEW_GATE_RULE = '五道闸全部免费、服务端强制(跳过 → 400;资产层与视频层两道按剧灰度)。每次审查返回 review_token,把它随下游收费工具一起传' +
298
298
  '(资产层那张作为 asset_review_token,与分镜层 review_token 一起传给出图工具);' +
299
299
  'findings 逐条原样告诉客户(code=问题类型 · shots=命中镜号 · action=该调哪个工具修),按 action 修完复审再走。' +
300
- '审查后又改了内容 → token 自动失效,复审一次即可。有 error 时默认拦截;只有客户知情并坚持才带 acknowledge_review:true——不要替客户做这个决定。' +
300
+ '审查后又改了内容 → token 自动失效,复审一次即可。有 error 时默认拦截;专业模式只有客户知情并坚持才带 acknowledge_review:true——不要替客户做这个决定;' +
301
+ '一键 / 混合模式的托管环节里,白名单内的启发式 error 带 auto_acknowledge:true 放行(平台判、写决策日志),其余自修后复审,修不好就汇总问客户(get_autopilot_log)。' +
301
302
  '`review_all` 是整集体检、不发 token。' +
302
303
  '★每一层审查之后你(agent)还要**亲自质检**:按返回的 agent_qc 清单逐件看素材(图下载看、视频抽帧看、文本通读),' +
303
304
  '调 submit_qc_report 提交报告;灰度剧没交报告 token 不能用,报告有问题与 error 同样默认拦。平台判有没有,你判像不像、对不对。';
@@ -40,8 +40,16 @@ export const HANDOFF_ASSETS = join(dirname(fileURLToPath(import.meta.url)), '..'
40
40
  function jsonResult(data) {
41
41
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
42
42
  }
43
- const CONFIRM_HINT = '⚠️ 批量报价确认流程:先调对应的 quote_* 工具,把返回的 estimated_points 原样告诉用户,' +
44
- '用户明确同意后,才用返回的 quote_id 调本工具。不要擅自确认。' +
43
+ // ★一键出片第二期(2026-10-16):「客户确认才跑 / 别替客户决定」的统一口径。执行模式(专业 / 一键 / 混合)由
44
+ // get_production_plan 的 autopilot 给出;工具描述是静态的,所以写成「按执行模式」。散落各工具的确认要求一律经
45
+ // 下面两个辅助常量措辞,别再各写各的(改口径只改这里)。哨兵:starreel-drama/backend/test/autopilot-p2-mcp.test.ts
46
+ /** 可托管的决定(报价确认、采纳建议、放行启发式问题):专业模式问客户,托管环节照默认决策表自行决定。 */
47
+ export const MODE_DECIDES = (what) => `(${what}按执行模式:专业模式 / 未确认方案 / 本环节由客户把控 → 讲给客户、客户同意才做,别替客户决定;` +
48
+ `本环节托管 → 不问客户,照 get_production_plan 的 autopilot.default_decisions 执行,平台记进 get_autopilot_log)`;
49
+ /** 任何执行模式都不许 agent 自己做的决定(autopilot.never_auto_actions)。 */
50
+ export const ALWAYS_ASK = (what) => `(${what}任何执行模式都须客户明确同意,托管也不例外)`;
51
+ const CONFIRM_HINT = '⚠️ 批量报价确认流程:先调对应的 quote_* 工具拿 estimated_points 与 quote_id,再用 quote_id 调本工具' +
52
+ MODE_DECIDES('报价确认') + '。托管环节报价须在预算硬上限内,被 402 BUDGET_WOULD_EXCEED 拦下就停下问客户。' +
45
53
  '★**出图类报价(定妆图/场景图/出帧)给的是两个数**:estimated_points 是**上界**' +
46
54
  '(按该模型参考图张数上限估,保证你不会按低了的预算开跑、出到一半 402 中断);' +
47
55
  '同一响应里的 typical_points 是**通常花费**。两个都要告诉客户——只说上界会让人以为贵得多' +
@@ -64,7 +72,8 @@ const REVIEW_GATE_HINT = (tool, layer) => `\n★【${layer}审查硬闸·免费
64
72
  `(每条带 code=问题类型、shots=命中镜号、action=该调哪个工具修),再把 review_token 传进本工具。` +
65
73
  `未审查会被 400 拒。审查后又改了内容 → token 自动失效,复审一次即可(仍免费)。` +
66
74
  `有 error 时默认拦截;客户知情并坚持照现状继续,才带 acknowledge_review:true——` +
67
- `带病推进大概率产出废片且照常扣费,不要替客户做这个决定。` +
75
+ `带病推进大概率产出废片且照常扣费${ALWAYS_ASK('acknowledge_review 只代表客户本人同意,')};` +
76
+ `托管环节只有白名单内的启发式 error 能带 auto_acknowledge:true 放行(平台判,不在白名单回 400 AUTOPILOT_MUST_ASK:must_ask_customer=true 才暂停问客户,false = 按 action 自修复审)。` +
68
77
  `★灰度剧还要你(agent)亲自质检:按 ${tool} 返回的 agent_qc 逐件看素材、逐条答清单,调 submit_qc_report 提交后这张 token 才能用` +
69
78
  `(没交 → 400 agent_qc_required;报告有问题 → 与 error 同样默认拦)。`;
70
79
  /** 五个审查工具共用:平台审查之外,外部 agent 亲自看素材的质检(2026-10-14 用户拍板)。 */
@@ -92,7 +101,8 @@ const VIDEO_QC_HINT = '\n★【完成后质检·免费】出完等它落库(get_
92
101
  /** 三个被闸的收费工具共用的参数(zod 形状,展开进各自 schema)。 */
93
102
  const REVIEW_ARGS = {
94
103
  review_token: z.string().optional().describe('★来自本层 review_* 工具的凭据(受闸项目必填;缺了会 400 并告诉你该调哪个审查工具)'),
95
- acknowledge_review: z.boolean().optional().describe('审查有 error 时,客户已知情并明确要求照现状继续才传 true(默认拦截)'),
104
+ acknowledge_review: z.boolean().optional().describe('审查有 error 时,客户本人已知情并明确要求照现状继续才传 true(默认拦截)。★任何执行模式都只代表客户本人同意——托管环节不许用它替客户放行,只能用 auto_acknowledge(受白名单限制)'),
105
+ auto_acknowledge: z.boolean().optional().describe('★一键 / 混合模式的托管环节专用:审查 error 全在自动放行白名单内(review_* 返回 autopilot.auto_ackable=true)时传 true 直接继续,平台记进决策日志;不在白名单回 400 AUTOPILOT_MUST_ASK(must_ask_customer=true 才暂停汇总问客户;false = 自修轮数没到上限,按 action 修完复审)。专业模式传了也无效'),
96
106
  };
97
107
  // 完整工作流顺序(照 get_pipeline_status 的 10 步真相走,不要跳步):
98
108
  // create_drama → set_script(原始) → rewrite_script(AI改写) → [get_script/edit_rewritten_script 审改]
@@ -102,7 +112,17 @@ const REVIEW_ARGS = {
102
112
  // → quote/generate_frames(默认只出首帧) → tail_frame_plan(免费) → generate_frames(frame_type=last_frame) → quote/generate_videos
103
113
  // → compose_episode → get_final_cut / get_export
104
114
  // 项目设定随时可 update_project_settings;色彩脚本保留主干,动作模板仅可选实验。
105
- export const WORKFLOW_HINT = '★三档执行策略(别把三档混着问客户):' +
115
+ /** 执行模式总纲(一键出片第二期):WORKFLOW_HINT 开头,先定「问不问客户」再谈产线顺序。 */
116
+ export const AUTOPILOT_HINT = '★★【执行模式总纲·开工先读】确认过开场方案的剧先 get_production_plan 读 autopilot:' +
117
+ '专业模式(pro,或 autopilot.enabled=false、未确认方案)→ 下文所有「讲给客户、客户同意才做」照字面执行;' +
118
+ '一键(auto)/ 混合(custom)的**托管环节** → 不问客户:照 get_pipeline_status 的 next_action 按序推进,报价在预算硬上限内自行确认,' +
119
+ '每层产出后照常 review_* + agent 质检,审查与质检返回的 autopilot.next_step 告诉你这一层怎么走' +
120
+ '(白名单 error 带 auto_acknowledge:true / 自修第几轮 / 暂停问客户),照做;遇到选择照 autopilot.default_decisions;' +
121
+ 'autopilot.never_auto_actions(mode=replace、删资产、覆盖客户上传图、allow_era_ref_conflict、克隆声音、交付验收)任何模式都要客户同意。' +
122
+ '402 BUDGET_WOULD_EXCEED / 409 MODEL_UNAVAILABLE / 400 AUTOPILOT_MUST_ASK 且 must_ask_customer=true / pending_questions 非空 → 暂停,' +
123
+ '把 get_autopilot_log 的 pending_questions **汇总后一次性**问客户;客户把控的环节照专业模式问。出完片用 get_autopilot_log 的 report 向客户交付。';
124
+ export const WORKFLOW_HINT = AUTOPILOT_HINT +
125
+ '★三档执行策略(别把三档混着问客户):' +
106
126
  '①【基础项目设定·免费·必做地基·建剧即设好,别建空壳】project_type/setting_brief(世界观·ERA LOCK)/' +
107
127
  'ethnicity(族裔)/画幅分辨率,以及一致性锚 cinematography_prompt(摄影DNA)·art_bible(美术圣经)·visual_lock(视觉锁定)——' +
108
128
  '全免费,是驱动全链一致性的地基;不设好,后续所有生成都跑偏、返工重花钱。用 create_drama/update_project_settings 直接设。' +
@@ -121,12 +141,13 @@ export const WORKFLOW_HINT = '★三档执行策略(别把三档混着问客户)
121
141
  '②分镜产出后 → review_storyboards(在 generate_frames 之前);③镜头图片产出后 → review_frames(在 generate_videos 之前)。' +
122
142
  '每次审查返回 review_token,把它随下游收费工具一起传;findings 逐条讲给客户(code=问题类型·shots=命中镜号·action=该调哪个工具修),' +
123
143
  '按 action 修完后**复审**再走。审查后又改了内容 → token 自动失效,复审一次即可(免费)。' +
124
- '有 error 时默认拦截,只有客户明确知情并坚持才带 acknowledge_review:true——别替客户做这个决定。' +
144
+ '有 error 时默认拦截,客户明确知情并坚持才带 acknowledge_review:true' + ALWAYS_ASK('acknowledge_review 只代表客户本人同意,') +
145
+ ';托管环节只有白名单内的启发式 error 能带 auto_acknowledge:true(见【执行模式总纲】)。' +
125
146
  '**软引导(不阻断但强烈建议,同样免费)**:出图/出视频前跑 run_precheck(揪出必被厂商拒的镜,防白花钱)——' +
126
147
  '★**要跑全集就先 run_drama_precheck**(剧级汇总·免费·只读):一次拿到每集问题数与 attention(该先修哪几集),' +
127
148
  '按它定优先级,再对那几集跑 run_precheck 拿逐条明细;别一集一集盲扫。' +
128
149
  '注意它的 not_covered——剧级跳过唯一要看图的那道 lint,「全剧 0 条」≠「全查过了」。' +
129
- '★揪出来之后别自己盲改:plan_precheck_fix 让平台算出提案 → 逐条讲给客户 → 客户点头后 apply_precheck_fix 落库;' +
150
+ '★揪出来之后别自己盲改:plan_precheck_fix 让平台算出提案 → apply_precheck_fix 落库' + MODE_DECIDES('采纳哪几条,托管=全部采纳') + ';' +
130
151
  '分镜后跑 get_health_report;定妆图出完用 get_characters 核对每个出场角色都有 image/sheet;' +
131
152
  '出帧后用 get_storyboards 看 frame_status 与 fail_reason/fail_hint(failed 的镜先修再往下,别带着废帧出视频);' +
132
153
  '出视频后同样看 video_status;成片前用 get_pipeline_status 确认没有缺镜。' +
@@ -226,12 +247,12 @@ export const WORKFLOW_HINT = '★三档执行策略(别把三档混着问客户)
226
247
  'generate_frames 只批量补「缺帧」的镜、已有首帧的镜跳过(正常、不是"拒绝")。★首帧出完调免费的 tail_frame_plan 看哪几镜要独立尾帧,再 frame_type=last_frame 批量补。换定妆图(set_character_portrait)后响应里的 stale_frames 就是被旧图污染、需逐镜重生的镜。' +
227
248
  '★绝不用外部工具自制首尾帧再 upload_shot_frame 来"改画面"——外部图无身份锚/画风锚,人物·服装·画风必漂,那才是废片根源;upload_shot_frame 只用于客户自有真实素材。' +
228
249
  '③【可选增强·AI 主动提示客户·报价确认才做】美术圣经生成/视觉锁抽取/场景组/口型/海报/音效/配乐/字幕翻译——' +
229
- '这些提升一致性/质量、大多收费。★AI 应主动告知客户这些可做并给报价,客户确认才跑;既不默默跳过、也不擅自扣费。' +
250
+ '这些提升一致性/质量、大多收费。★AI 应主动告知客户这些可做并给报价' + MODE_DECIDES('跑不跑') + ';既不默默跳过、也不擅自扣费。' +
230
251
  '★两条锁定纪律:①**画幅比例**在 create_drama 即定、drama 级锁定,之后所有出图/出视频/成片都用它、**别中途改**' +
231
252
  '(改了已生成内容画幅会不一致、漂移);不设默认 9:16。②**拆镜每镜 5-7 秒是对 AI 出视频优化的正常时长**,' +
232
253
  '别因「镜偏长」误判就重拆——generate_storyboards 会**替换整集所有分镜**、已出图白费,已有分镜后端会拦、需 confirm_replace。' +
233
254
  '★改写保真(默认 auto 智能路由):set_script 的原稿**本身已是剧本形态**(场景头/对白行结构)时,rewrite_script 自动走两步保真——' +
234
- '客户台词逐句由机器闸锁定(丢一句即内部拒收重做)、AI 绝不加戏;剧作缺口(钩子/情感锚点)不自动补,写进 get_script 返回的 dramaturgy_suggestions 由客户决定采纳。' +
255
+ '客户台词逐句由机器闸锁定(丢一句即内部拒收重做)、AI 绝不加戏;剧作缺口(钩子/情感锚点)不自动补,写进 get_script 返回的 dramaturgy_suggestions' + MODE_DECIDES('采不采纳') + '。' +
235
256
  '原稿是小说/大纲则自动走创作型改写(AI 铺钩子造情感点),两种客户各得其所、无需手动切换。' +
236
257
  '要覆盖默认用 update_project_settings 的 rewrite_pipeline(auto/two_pass/single_forced)与 fidelity_enforce(1=保真硬闸)。' +
237
258
  '客户说「AI 把我的剧本改偏了」时的处置:①确认完整原稿已进 set_script;②rewrite_pipeline 设 two_pass 强制保真后重跑 rewrite_script;' +
@@ -396,7 +417,7 @@ export function registerProduceTools(server, client) {
396
417
  }, async ({ episode_id, script }) => jsonResult(await client.producePut(`/episodes/${episode_id}/script`, { content: script })));
397
418
  // ---------- AI 改写 / 审阅 / 改稿 ----------
398
419
  server.tool('rewrite_script', 'AI 改写:把原始剧本改写成可拍稿(读 content → 写 script_content)。按项目类型自动选改写 agent' +
399
- '★保真自动路由:原稿已是剧本形态时自动走两步保真(台词逐句机器锁定、AI 不加戏,剧作缺口进 dramaturgy_suggestions 由客户决定);原稿是小说/大纲则走创作型改写。' +
420
+ '★保真自动路由:原稿已是剧本形态时自动走两步保真(台词逐句机器锁定、AI 不加戏,剧作缺口进 dramaturgy_suggestions' + MODE_DECIDES('采不采纳') + ');原稿是小说/大纲则走创作型改写。' +
400
421
  '(广告走 ad 改写;MV 不走标准改写会被拦)。后台异步(分钟级),文本步按 token 后付、不欠费,无需报价。' +
401
422
  '完成后用 get_script 审阅、edit_rewritten_script 改稿。' +
402
423
  '★典型耗时 2~4 分钟(生产实测 ≈169 秒)。**60 秒内查不到结果是正常的,不是失败**——' +
@@ -407,7 +428,7 @@ export function registerProduceTools(server, client) {
407
428
  '所以**首次改写成功之后,后续所有修改一律用 edit_rewritten_script 点改**——免费、秒级、只动指定的' +
408
429
  '那几场,其余逐字不变,结果确定不抽卡;只有"要一个完全不同的版本"才重跑本工具。' +
409
430
  '误重跑后用 get_script(include_previous=1) 取回上一版。' + WORKFLOW_HINT, { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/rewrite`)));
410
- server.tool('get_script', '读某一集的原始内容 + AI 改写后的可拍稿 + 改写状态 + dramaturgy_suggestions(两步保真模式下 AI 识别到但未自动补的剧作缺口——钩子/情感锚点;把它转述给客户决定采纳与否,忽略不影响产线)。免费。' +
431
+ server.tool('get_script', '读某一集的原始内容 + AI 改写后的可拍稿 + 改写状态 + dramaturgy_suggestions(两步保真模式下 AI 识别到但未自动补的剧作缺口——钩子/情感锚点;采纳与否' + MODE_DECIDES('') + ',忽略不影响产线)。免费。' +
411
432
  '★改稿工作流:先用本工具取回 rewritten_script 全文 → 只改客户要改的那几场 → 用 edit_rewritten_script 提交(不要重跑改写)。' +
412
433
  'previous_script_available=true 表示有上一版快照;传 include_previous=1 可取回上一版全文(误重跑/改坏了的回捞路径,默认不带以免响应翻倍)。', {
413
434
  episode_id: z.number().int().positive(),
@@ -517,7 +538,7 @@ export function registerProduceTools(server, client) {
517
538
  '★这里绿了,bulk_import_storyboards 就能一次过。', { payload: z.union([z.string(), z.record(z.string(), z.unknown())]).describe('要检查的 JSON:对象,或 JSON 字符串(客户贴的文本)') }, async ({ payload }) => jsonResult(await client.producePost('/bulk-import/lint', { payload })));
518
539
  server.tool('bulk_import_storyboards', '把一份结构化 JSON 一次性建成本集的分镜(按 storyboard_number 去重更新)。**不建角色/场景/道具**——它们由 extract_assets 从剧本提取,导入只按名字绑定已有的(回执 characters_unmatched / char_refs_unresolved 列出没绑上的;先 extract 再导)。免费·确定性·不调 AI。' +
519
540
  '★三步走:① get_bulk_import_spec 取契约与成品示例;② check_bulk_import 自检到 errors 清零;③ 再调本工具。' +
520
- '★mode=merge(默认)保留未提到的镜;mode=replace 替换本集**全部**分镜(旧镜与已生成图/视频归档可恢复、不会自动重挂)——replace 必须先得到客户明确同意。' +
541
+ '★mode=merge(默认)保留未提到的镜;mode=replace 替换本集**全部**分镜(旧镜与已生成图/视频归档可恢复、不会自动重挂)——replace' + ALWAYS_ASK('') + '。' +
521
542
  '★image_prompt / video_prompt 想自己写就直接写在每镜里,**逐字照用不被覆盖**;没填的镜平台拼基础描述(回执 base_prompts_built 是拼了几镜),导入后可用 get_shot_prompts 读、update_shot 改。' +
522
543
  '(frame_visual_contract 是内部帧契约,导入时忽略。)' +
523
544
  '★导入后:语速律会抬高装不下台词的镜(回执 speech_duration_raised / speech_duration_overflow)。' +
@@ -549,10 +570,11 @@ export function registerProduceTools(server, client) {
549
570
  '★分钟级后台任务;用 get_run_status 判断是否还在跑,别拿 60 秒当失败判据。' +
550
571
  REVIEW_GATE_HINT('review_script', '改写稿') + WORKFLOW_HINT +
551
572
  '\n★【完成后质检】提取完用 get_characters / get_scenes / get_props 核对档案(名字、外貌、时代)与客户剧本一致;' +
552
- '定妆图/设定图/场景图/道具图出完后由 review_assets 统一审覆盖与质量。', { episode_id: z.number().int().positive(), ...REVIEW_ARGS }, async ({ episode_id, review_token, acknowledge_review }) => jsonResult(await client.producePost(`/episodes/${episode_id}/extract`, { review_token, acknowledge_review })));
573
+ '定妆图/设定图/场景图/道具图出完后由 review_assets 统一审覆盖与质量。', { episode_id: z.number().int().positive(), ...REVIEW_ARGS }, async ({ episode_id, review_token, acknowledge_review, auto_acknowledge }) => jsonResult(await client.producePost(`/episodes/${episode_id}/extract`, { review_token, acknowledge_review, auto_acknowledge })));
553
574
  // ---------- 完整工作流进度 ----------
554
575
  server.tool('get_pipeline_status', '查某一集完整工作流的进度(script_rewrite/提取/分镜/语音/出图/出视频/合成/配乐/终拼…各步 ' +
555
576
  'done/partial/pending/not_required)。照它按序推进、不跳步。免费。' +
577
+ '★一键 / 混合模式(托管灰度内)的剧额外返回 next_action(action=run 托管环节直接做 / wait_running 后台还在跑、等它落库别重复派 / wait_customer 客户把控环节或没有预算预检的步骤先问 / ask_customer 有待问事项先汇总问 / done 各步完成——先按 tool=review_final_cut 审成片、有问题修完重拼复审,再出交付报告;tool=该调哪个工具)与 pending_questions,照 next_action 推进。' +
556
578
  'asset_versions_summary 将 generated、input_consistent、review_valid、adopted 和 current_effective 分开计数;不能把 generate_videos.completed 当当前有效数量。' +
557
579
  '★generate_images / generate_videos 的 status=done 只表示「已生成」,不代表可交付:看同一步的 validity(current_effective 当前有效 / input_changed 生成后输入已变,changed_by 列出是剧级设定、镜头本身还是哪个角色变了 / input_unknown / unreviewed / review_failed),validity.note 不为空时先把它告诉客户。输入变了不一定影响画面,别据此整集重出(付费),先用 get_asset_versions 逐镜看原因。' +
558
580
  '★not_required=当前模式不需要该步(如原声剧的 TTS 三步、关配乐的 generate_bgm),不是没做完,别去补做。' +
@@ -591,8 +613,8 @@ export function registerProduceTools(server, client) {
591
613
  quote_id: z.string().describe('来自 quote_storyboards'),
592
614
  confirm_replace: z.boolean().optional().describe('本集已有分镜时必须 true 才重拆(会替换整集所有分镜,已出图白费)'),
593
615
  ...REVIEW_ARGS,
594
- }, async ({ episode_id, quote_id, confirm_replace, review_token, acknowledge_review }) => jsonResult(await client.producePost(`/episodes/${episode_id}/storyboards/generate`, {
595
- quote_id, review_token, acknowledge_review, ...(confirm_replace ? { confirm_replace } : {}),
616
+ }, async ({ episode_id, quote_id, confirm_replace, review_token, acknowledge_review, auto_acknowledge }) => jsonResult(await client.producePost(`/episodes/${episode_id}/storyboards/generate`, {
617
+ quote_id, review_token, acknowledge_review, auto_acknowledge, ...(confirm_replace ? { confirm_replace } : {}),
596
618
  })));
597
619
  server.tool('get_frame_retry_diagnosis', '免费只读查询图片任务的连败诊断、证据任务 ID 和下一步动作。diagnosis_required 时先调用本工具:审计故障重审已有候选,未知原因先复核;不得原样重抽或擅自改剧本。' +
598
620
  '★返回的 recovery_options 是出图失败后的三选一(按免费→免费→付费排序,needs_attention=false 时不必打扰客户):' +
@@ -609,7 +631,7 @@ export function registerProduceTools(server, client) {
609
631
  '客户看过原图确认画面没问题、是审计误判时,用 adopt_frame_candidate 人工采用(免费);manual_adoption 非空 = 该候选是人工放行的。' +
610
632
  'recovery_options 按帧位给出失败后的三选一(挑误拒 / 修图上传 / 重生),与 get_frame_retry_diagnosis 同口径。', { storyboard_id: z.number().int().positive() }, async ({ storyboard_id }) => jsonResult(await client.produceGet(`/storyboards/${storyboard_id}/frame-workbench`)));
611
633
  server.tool('adopt_frame_candidate', '免费:人工采用一张已出完的首帧/尾帧候选(被审计拒掉 rejected、落选 dismissed,或曾经在用后被换下的 completed),作为本镜在用帧——审计误判时的人工兜底(例如作者契约写明的设定被判成冲突),也可随时换回之前那张。' +
612
- '★必须先用 get_frame_candidates 让客户看过原图和拒因、客户明确同意采用后才调;不许为了让流程走下去自行放行。' +
634
+ '★必须先用 get_frame_candidates 让客户看过原图和拒因、客户明确同意采用后才调' + ALWAYS_ASK('采用被拒候选') + ';不许为了让流程走下去自行放行。' +
613
635
  '只能采用本镜本帧位、已出完、有画面、且不是当前在用的那张;会在该候选上记录人工放行(谁/何时/原拒因),原拒因保留供统计。' +
614
636
  '本镜已有视频的会标成待重生(视频是按旧帧出的,需 regenerate_shot_video)。本镜该帧是客户上传或已定稿的,要带 replace_user_frame:true 才会替换(并解除定稿)。' +
615
637
  '同帧位有在途出图或本镜视频在途时返回 409,等它结束再采用。', {
@@ -688,7 +710,7 @@ export function registerProduceTools(server, client) {
688
710
  '既要站立又要坐姿),这类镜**任何正确的图都满足不了**,不改就会反复被拒并反复扣费,' +
689
711
  '出现时应先按提示改分镜再出图,而不是重试。\n' +
690
712
  '★**怎么改不用你猜**:调 plan_precheck_fix 让平台算出提案(哪一镜、把什么改成什么),' +
691
- '讲给客户、客户点头后用 apply_precheck_fix 落库——比你自己用 update_shot 盲改稳,' +
713
+ '再用 apply_precheck_fix 落库' + MODE_DECIDES('采纳哪几条,托管=全部采纳') + '——比你自己用 update_shot 盲改稳,' +
692
714
  '那条路绕开了乐观锁与落库前复核。部分类别系统不替你改(提案里的 blocked),那些才需要人工调。\n' +
693
715
  '★★**镜已经反复出不来时也回来跑这个**——本工具不只是事前预检。' +
694
716
  '它同时报「同镜同因连拒已熔断(repeat-reject,24h 自动解除)」与「帧契约自相矛盾」,' +
@@ -724,8 +746,8 @@ export function registerProduceTools(server, client) {
724
746
  server.tool('plan_precheck_fix', '让平台**算出**该怎么改 run_precheck 揪出的「指令自相矛盾」类问题(第①步,只算不改)。'
725
747
  + '按文本用量计费(很小),不走报价确认。\n'
726
748
  + '★两段式,这一步不动任何数据:拿到提案后**逐条讲给客户**(哪一镜、把什么改成什么、为什么),'
727
- + '客户点头后再把采纳的那几条**原样**传给 apply_precheck_fix 落库。\n'
728
- + '★**不要替客户决定**:这些判据有假阳,平台刻意不做「一键自动修」——'
749
+ + '再把采纳的那几条**原样**传给 apply_precheck_fix 落库' + MODE_DECIDES('采纳哪几条,托管=全部采纳') + '。\n'
750
+ + '★**专业模式不要替客户决定**:这些判据有假阳,平台刻意不做「一键自动修」——'
729
751
  + '曾实测同一批提案里近半是判据误报,静默改会把本来正确的分镜改坏。\n'
730
752
  + '★返回的 blocked 列出「系统不替你改」的类别:那些要人工按 run_precheck 的提示调整分镜。'
731
753
  + '看到 blocked 不等于没问题,反而是**必须人工处理**的那部分。\n'
@@ -749,7 +771,7 @@ export function registerProduceTools(server, client) {
749
771
  old_value: z.string().nullable().optional().describe('★必须是 plan 给的原值:落库时做乐观锁,库里已经不是它了就跳过'),
750
772
  new_value: z.string().nullable().optional(),
751
773
  })),
752
- })).describe('plan_precheck_fix 回的提案,原样传回;只放客户点头采纳的那几条'),
774
+ })).describe('plan_precheck_fix 回的提案,原样传回;只放采纳的那几条(专业模式=客户点头的,托管=全部)'),
753
775
  }, async ({ episode_id, proposals }) => jsonResult(await client.producePost(`/episodes/${episode_id}/precheck-fix/apply`, { proposals })));
754
776
  server.tool('get_health_report', '(★推荐·免费诊断)读分镜出体检报告:时长超标/母题覆盖不足/问题镜。纯读、免费。出图前查,识别问题先改再出、别出了片才发现。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/health-report`)));
755
777
  // ---------- 分阶段审查(三道硬闸的凭据来源;全部免费) ----------
@@ -762,7 +784,7 @@ export function registerProduceTools(server, client) {
762
784
  '把 findings 逐条告诉客户、按 action 修完(改稿走 edit_rewritten_script)后复审,再拿 review_token 往下走。' + AGENT_QC_HINT +
763
785
  '\n★【剧本评估与建议】materials 里有客户原稿(script:original)和改写稿(script):先评估客户给的稿子本身(主线、动机、冲突、节奏、漏洞、能不能拍),' +
764
786
  '再核对改写稿;每个问题在 submit_qc_report 的 suggestions 里给出具体改法(能直接改字的给 before/after)。' +
765
- '提交后把评估和建议逐条讲给客户,让客户选:采纳其中几条 / 全部直接执行 / 不改直接往下做——由客户决定,不要替他选,也不要没问就改。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/review/script`)));
787
+ '提交后把评估和建议逐条讲给客户,让客户选:采纳其中几条 / 全部直接执行 / 不改直接往下做' + MODE_DECIDES('选哪条路') + ';托管环节默认只执行 priority=must 且不改客户原台词的,其余写进交付报告。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/review/script`)));
766
788
  server.tool('review_storyboards', '【第②道硬闸·免费】审查分镜:禁区词(会被厂商审核拒、白扣费)、镜头时长分布、相邻构图重复、' +
767
789
  '同场景角色站位漂移、情绪曲线峰谷、关键镜标记。★generate_frames 之前必须先跑本工具——' +
768
790
  '分镜里的问题一旦整集出图就变成整集废图,单镜修不回来。按 findings.action 用 update_shot/split_shot 修完再复审。' +
@@ -796,7 +818,7 @@ export function registerProduceTools(server, client) {
796
818
  '★别走过场:没打开看就列进 inspected,等于替客户签了字——出了废片照常扣费。' +
797
819
  '★verdict=issues 时这张 token 默认进不了下一步:把问题讲给客户,修完重新 review + 质检;客户明确知情并坚持照现状继续,下一步才带 acknowledge_review:true。' +
798
820
  '★suggestions(修改建议):剧本层(agent_qc.suggestions_required=true)报了问题就必须给,其它层可选。每条写 problem + proposal,能直接改字的再给 before/after;' +
799
- '返回会给每条编号(s1…)。把建议讲给客户,客户选采纳几条 / 全部直接执行 / 不改直接往下做,照客户的选择办。' +
821
+ '返回会给每条编号(s1…)。把建议讲给客户,客户选采纳几条 / 全部直接执行 / 不改直接往下做' + MODE_DECIDES('选哪条路;托管=只执行 priority=must 且不改客户原台词的') + '。' +
800
822
  '审查之后内容又变了,报告会被拒,重新 review 后按新的 agent_qc 再质检。', {
801
823
  episode_id: z.number().int().positive(),
802
824
  review_token: z.string().describe('对应层 review_* 返回的 review_token(资产层也是这个值,出图时再作为 asset_review_token 传)'),
@@ -884,9 +906,9 @@ export function registerProduceTools(server, client) {
884
906
  'gpt-image-2.5-flare(默认·基础11点+每张参考图18点)/gpt-image-2.5-sunburst(同价·中文字形与细节更准)/' +
885
907
  'gemini-3.1-flash-image(Nano Banana 2·71点)/gemini-3-pro-image(Nano Banana Pro·更精细·175点)/' +
886
908
  'gemini-3.1-flash-lite-image(Nano Banana 2 Lite·便宜·31点)/doubao-seedream-5-0-260128(Seedream 5.0)'),
887
- allow_era_ref_conflict: z.boolean().optional().describe('★默认不传。时代审核判定客户上传的定妆图(或由它派生的身体/发型/设定图)与时代设定冲突时,出图会被拦下(400 ERA_USER_REF_CONFLICT,未扣费)。**先把冲突原因告诉客户、由客户确认造型无误**后才带 true:这些图保留不剔、照常出图。造型真不对就改定妆图,时代设定错了就 set_era_contract'),
888
- }, async ({ episode_id, quote_id, frame_type, image_model, review_token, asset_review_token, acknowledge_review, allow_era_ref_conflict }) => jsonResult(await client.producePost(`/episodes/${episode_id}/frames/generate`, {
889
- quote_id, frame_type, image_model, review_token, asset_review_token, acknowledge_review, allow_era_ref_conflict,
909
+ allow_era_ref_conflict: z.boolean().optional().describe('★默认不传。时代审核判定客户上传的定妆图(或由它派生的身体/发型/设定图)与时代设定冲突时,出图会被拦下(400 ERA_USER_REF_CONFLICT,未扣费)。**先把冲突原因告诉客户、由客户确认造型无误**后才带 true' + ALWAYS_ASK('allow_era_ref_conflict ') + ':这些图保留不剔、照常出图。造型真不对就改定妆图,时代设定错了就 set_era_contract'),
910
+ }, async ({ episode_id, quote_id, frame_type, image_model, review_token, asset_review_token, acknowledge_review, auto_acknowledge, allow_era_ref_conflict }) => jsonResult(await client.producePost(`/episodes/${episode_id}/frames/generate`, {
911
+ quote_id, frame_type, image_model, review_token, asset_review_token, acknowledge_review, auto_acknowledge, allow_era_ref_conflict,
890
912
  })));
891
913
  // ---------- 单镜出帧/重生(改某一镜的画面走这里,别去外部平台出图再传回来) ----------
892
914
  server.tool('quote_shot_frame', '报价:重画/补出**某一镜的某一帧**要多少点(一帧=一张图)。返回 estimated_points、quote_id。零扣费。' +
@@ -920,8 +942,8 @@ export function registerProduceTools(server, client) {
920
942
  reopen_pair_id: z.string().optional().describe('来自 get_storyboards 该镜的同名字段;只重掷有问题的那一侧,不必再传 frame_type'),
921
943
  image_model: z.string().optional().describe('临时覆盖本次重画的图片模型(★必须与 quote_shot_frame 传的那个一致,否则 400 IMAGE_MODEL_MISMATCH;不传=用 drama 级设定,默认 ChatGPT Image 2.5 Flare);取值同 generate_frames'),
922
944
  allow_missing_terminal: z.boolean().optional().describe('★逃生门,默认不传。本镜标为「状态改变」却没写终态时,出尾帧会被前置闸拦下;带 true 表示「我知道,照现状出」。正解是先 update_shot 补 last_frame_prompt——那类镜照现状出的成功率 9.4%,这个参数只用于确认本镜就该「几乎不变」的场合'),
923
- allow_era_ref_conflict: z.boolean().optional().describe('★默认不传。时代审核判定客户上传的定妆图(或由它派生的身体/发型/设定图)与时代设定冲突时,出图会被拦下(400 ERA_USER_REF_CONFLICT,未扣费)。**先把冲突原因告诉客户、由客户确认造型无误**后才带 true:这些图保留不剔、照常出图。造型真不对就改定妆图,时代设定错了就 set_era_contract'),
924
- }, async ({ storyboard_id, quote_id, review_token, asset_review_token, acknowledge_review, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal, allow_era_ref_conflict }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/frame/generate`, { quote_id, review_token, asset_review_token, acknowledge_review, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal, allow_era_ref_conflict })));
945
+ allow_era_ref_conflict: z.boolean().optional().describe('★默认不传。时代审核判定客户上传的定妆图(或由它派生的身体/发型/设定图)与时代设定冲突时,出图会被拦下(400 ERA_USER_REF_CONFLICT,未扣费)。**先把冲突原因告诉客户、由客户确认造型无误**后才带 true' + ALWAYS_ASK('allow_era_ref_conflict ') + ':这些图保留不剔、照常出图。造型真不对就改定妆图,时代设定错了就 set_era_contract'),
946
+ }, async ({ storyboard_id, quote_id, review_token, asset_review_token, acknowledge_review, auto_acknowledge, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal, allow_era_ref_conflict }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/frame/generate`, { quote_id, review_token, asset_review_token, acknowledge_review, auto_acknowledge, frame_type, replace_user_frame, reopen_pair_id, image_model, allow_missing_terminal, allow_era_ref_conflict })));
925
947
  // ---------- 出视频(videos,大额) ----------
926
948
  server.tool('quote_videos', '报价:给某一集批量出视频要多少点(与实际扣费同函数,较准)。返回 estimated_points、quote_id。零扣费。' +
927
949
  '⚠️ 视频是大额花费,务必把点数清楚告诉用户并等其确认。' +
@@ -948,7 +970,7 @@ export function registerProduceTools(server, client) {
948
970
  episode_id: z.number().int().positive(),
949
971
  quote_id: z.string().describe('来自 quote_videos'),
950
972
  ...REVIEW_ARGS,
951
- }, async ({ episode_id, quote_id, review_token, acknowledge_review }) => jsonResult(await client.producePost(`/episodes/${episode_id}/videos/generate`, { quote_id, review_token, acknowledge_review })));
973
+ }, async ({ episode_id, quote_id, review_token, acknowledge_review, auto_acknowledge }) => jsonResult(await client.producePost(`/episodes/${episode_id}/videos/generate`, { quote_id, review_token, acknowledge_review, auto_acknowledge })));
952
974
  // ---------- 成片(compose/终拼) ----------
953
975
  server.tool('compose_episode', '把某一集所有镜头视频拼成一条成片(终拼)。**免费**(纯拼接,无需报价确认),后台异步。' +
954
976
  '发起前有终拼预检:缺视频的镜或仍在生成中的视频任务会 400 硬阻断并列出问题镜(blockers),' +
@@ -956,12 +978,12 @@ export function registerProduceTools(server, client) {
956
978
  '响应里的 advisories(时长偏差/孤儿任务/缺TTS)只提醒不阻断,建议逐条处理后再拼。发起后用 get_final_cut 轮询。' +
957
979
  REVIEW_GATE_HINT('review_videos', '视频与配音') +
958
980
  '\n★【完成后质检·免费】成片出来后跑 review_final_cut(缺镜/字幕放不下/身份锚/跳轴/错别字,proofread=false 则不扣费),' +
959
- '结论讲给客户;客户确认后才 confirm_delivery_acceptance。force 只越过终拼预检,不越过审查闸。', {
981
+ '结论讲给客户;客户确认后才 confirm_delivery_acceptance' + ALWAYS_ASK('交付验收') + '。force 只越过终拼预检,不越过审查闸。', {
960
982
  episode_id: z.number().int().positive(),
961
983
  force: z.boolean().optional().describe('true=预检有硬阻断也强制照拼(默认拦截)。缺视频的镜会被静默跳过,成片为部分'),
962
984
  ...REVIEW_ARGS,
963
- }, async ({ episode_id, force, review_token, acknowledge_review }) => jsonResult(await client.producePost(`/episodes/${episode_id}/compose`, {
964
- review_token, acknowledge_review, ...(force ? { force: true } : {}),
985
+ }, async ({ episode_id, force, review_token, acknowledge_review, auto_acknowledge }) => jsonResult(await client.producePost(`/episodes/${episode_id}/compose`, {
986
+ review_token, acknowledge_review, auto_acknowledge, ...(force ? { force: true } : {}),
965
987
  })));
966
988
  server.tool('get_final_cut', '查某一集成片状态与下载链接。status=completed 时返回 download_url(我方 COS 直链,可直接下载)。免费。' +
967
989
  '★bgm_stale=true 表示配乐在成片之后生成/改动、尚未进成片:重新 compose_episode(免费)即可,别用 re-render。' +
@@ -1113,7 +1135,7 @@ export function registerProduceTools(server, client) {
1113
1135
  + '\n出图前做,能减少「上一镜站着下一镜突然坐着」这类接不上的情况。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/derive-pose-chain`, {})));
1114
1136
  server.tool('quote_extend_final', '★成片续写**报价**(免费·无副作用):在已有成片后面再接几轮 AI 生成的片段,把片子拉长。'
1115
1137
  + '回 rounds(轮数)/duration_per_round(每轮秒数)/max_rounds(上限)/estimated_points(预估点数)。'
1116
- + '\n★**必须先报价、把点数讲给客户、客户确认后再调 extend_final**——每轮都真扣厂商的钱。'
1138
+ + '\n★**必须先报价、把点数讲给客户、客户确认后再调 extend_final**' + ALWAYS_ASK('续写没有派发前预算预检,') + '——每轮都真扣厂商的钱。'
1117
1139
  + '\n★回 403 = 本剧没开这个能力(要求剧集视频引擎是 2.5 系)。这不是调用错误,别重试;'
1118
1140
  + '告诉客户换引擎或走别的加长方式。', {
1119
1141
  episode_id: z.number().int().positive(),
@@ -1127,7 +1149,7 @@ export function registerProduceTools(server, client) {
1127
1149
  return jsonResult(await client.produceGet(`/episodes/${episode_id}/extend-final/quote?${qs.toString()}`));
1128
1150
  });
1129
1151
  server.tool('extend_final', '发起成片续写(**收费**,每轮走视频生成扣费链)。后台串行跑,立即返回会话 id,用 get_extend_final 轮询。'
1130
- + '\n★调之前**必须**先 quote_extend_final 报价并让客户确认点数——不要替客户决定花钱。'
1152
+ + '\n★调之前**必须**先 quote_extend_final 报价并让客户确认点数' + ALWAYS_ASK('续写没有派发前预算预检,') + '。'
1131
1153
  + '\n★续写只是「在成片后面接片段」,不改前面已有的内容;接出来的片段也**不会**自动成为本集成片,'
1132
1154
  + '满意之后要显式调 apply_extend_final 才生效。'
1133
1155
  + '\n★403 = 本剧没开(引擎需 2.5 系),别重试。', {
@@ -1204,7 +1226,7 @@ export function registerProduceTools(server, client) {
1204
1226
  });
1205
1227
  // ========== 项目设定页子资源(可选增强,提升一致性/风格锁)==========
1206
1228
  server.tool('generate_world_concept', '(★默认必做·很多第三方平台漏做这步·仍走报价确认)生成世界观概念图(固定4格:全景/时代/主场景/色彩),提升整剧视觉一致性。' +
1207
- '★分镜后默认调这个,别跳过;但仍要告知客户预估点数、客户确认才跑(不静默扣费)。需 setting_brief ≥30字。图片步,按固定成本后付、不欠费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/world-concept`)));
1229
+ '★分镜后默认调这个,别跳过;但仍要告知客户预估点数、客户确认才跑(不静默扣费)' + ALWAYS_ASK('本工具没有报价、不走派发前预算预检,') + '。需 setting_brief ≥30字。图片步,按固定成本后付、不欠费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/world-concept`)));
1208
1230
  server.tool('generate_art_bible', '生成美术圣经(色调/材质/气质,读角色+元数据)。文本步后付不欠费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.producePost(`/dramas/${drama_id}/art-bible`)));
1209
1231
  server.tool('extract_visual_lock', '从世界观 Brief + 角色抽取「视觉锁定」规则(写 visual_lock,统一出图风格/族裔)。文本步后付不欠费。', {
1210
1232
  drama_id: z.number().int().positive(),
@@ -1331,7 +1353,7 @@ export function registerProduceTools(server, client) {
1331
1353
  server.tool('preview_delete_asset', '预览「真删除」某个旧资产文件(免费、只读)。真删除会从存储里**永久删除**这张图的全部版本,不可撤销。' +
1332
1354
  '返回 deletable(能不能删)、reasons(不能删的原因,如 still-referenced 仍被引用 / no-trusted-provenance 无法证明归属 / not-drama-owner 不是剧主)、' +
1333
1355
  'blocking(还在引用它的位置)、history(会失效的历史记录条数)、copies(各存储桶里的版本数),能删时附 confirm_token(10 分钟内有效、只能用一次)。' +
1334
- '★必须把预览结果原样告诉客户,客户明确说「确认删除」后才能调 delete_asset;不要替客户做决定。' +
1356
+ '★必须把预览结果原样告诉客户,客户明确说「确认删除」后才能调 delete_asset' + ALWAYS_ASK('删除资产') + '。' +
1335
1357
  '只有剧主能删;只能删能证明归属的文件——平台为本剧生成的图,或客户通过平台上传(上传时有签发记录)的图;更早上传、没有签发记录的图删不了,只能解除引用。', { drama_id: z.number().int().positive(), url: z.string().describe('要删除的图片地址(通常来自 list_unlinked_assets 或解除引用回执)') }, async ({ drama_id, url }) => jsonResult(await client.producePost(`/dramas/${drama_id}/asset-delete/preview`, { url })));
1336
1358
  server.tool('delete_asset', '★不可撤销:永久删除某个旧资产文件的全部版本(两个存储桶)。必须先 preview_delete_asset,把结果给客户看,客户明确确认后,' +
1337
1359
  '带上同一个 url 与预览返回的 confirm_token 调用。执行前平台会重跑全部检查,影响范围和预览时不一致会拒绝(409),需要重新预览。' +
@@ -1362,7 +1384,7 @@ export function registerProduceTools(server, client) {
1362
1384
  '被拒时把 message 里的问题原样告诉客户;只有客户看过问题后明确坚持,才带 allow_issues:true 重传(自动检查可能误判)。' +
1363
1385
  '登记免费;内容检查是一次小额视觉审核,按用量后付。' +
1364
1386
  '★换了场景图后旧的打光参考图作废,平台**不会**自动补(那要另扣一张图的点数):' +
1365
- '回执 lighting_ref.needed=true 时把 estimated_points 告诉客户,客户同意再调 quote_scene_lighting_ref → generate_scene_lighting_ref。' +
1387
+ '回执 lighting_ref.needed=true 时把 estimated_points 告诉客户' + MODE_DECIDES('跑不跑') + ',再调 quote_scene_lighting_ref → generate_scene_lighting_ref。' +
1366
1388
  '上传时该场景还没有分镜(needed=false 且分镜还没建)的,等分镜建好后再调 quote_scene_lighting_ref 看要不要补——平台不会自动补上传图的打光参考图。' + ASSET_QC_HINT, {
1367
1389
  scene_id: z.number().int().positive(),
1368
1390
  file_path: z.string().describe('本地场景图路径'),
@@ -1490,10 +1512,11 @@ export function registerProduceTools(server, client) {
1490
1512
  ...REVIEW_ARGS,
1491
1513
  prompt: z.string().optional().describe('可选:覆盖该镜视频 prompt'),
1492
1514
  replace_user_footage: z.boolean().optional().describe('本镜是实拍素材镜时,显式允许用 AI 重生的视频覆盖它(不传则被守卫拒 409)。'),
1493
- }, async ({ storyboard_id, quote_id, review_token, acknowledge_review, prompt, replace_user_footage }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/regen/generate`, {
1515
+ }, async ({ storyboard_id, quote_id, review_token, acknowledge_review, auto_acknowledge, prompt, replace_user_footage }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/regen/generate`, {
1494
1516
  quote_id,
1495
1517
  review_token,
1496
1518
  acknowledge_review,
1519
+ auto_acknowledge,
1497
1520
  ...(prompt ? { prompt } : {}),
1498
1521
  ...(replace_user_footage !== undefined ? { replace_user_footage } : {}),
1499
1522
  })));
@@ -1732,7 +1755,7 @@ export function registerProduceTools(server, client) {
1732
1755
  '参数拼错(bpm 越界、调性不是 major/minor/modal、幕号不存在)会 400;配器里写人声/环境声会被丢掉并在 director_warnings 点名。' +
1733
1756
  '客户说「要慢一点、小调、用古筝」这类**明确参数**用 director;说「整体更压抑」这类**方向**用 prompt;两者可同时用。\n' +
1734
1757
  '★candidates 可选=候选整版对比:每幕出 2~3 首**完整**候选让客户挑(挑中的直接用,听到什么成片里就是什么)。' +
1735
- '每首单独按首计费 ⇒ 费用是单首的 N 倍,**必须先 quote_bgm_candidates 报价、把点数告诉客户、客户同意后**带 quote_id 调本工具。' +
1758
+ '每首单独按首计费 ⇒ 费用是单首的 N 倍,**必须先 quote_bgm_candidates 报价、把点数告诉客户、客户同意后**带 quote_id 调本工具' + ALWAYS_ASK('多候选逐首扣费、没有整单预算预检,') + '。' +
1736
1759
  '不传或传 1=原行为(每幕一首,无需报价)。生成后 get_bgm_status 的 candidate_groups 给每首的试听地址,select_bgm_candidate 挑选(免费),不挑默认第 1 首。' + VIDEO_QC_HINT, {
1737
1760
  episode_id: z.number().int().positive(),
1738
1761
  prompt: z.string().optional().describe('配乐方向,整集一条,≤800 字。只写音乐维度,别写剧情。传空字符串=清除已存的提示词。'
@@ -1757,7 +1780,7 @@ export function registerProduceTools(server, client) {
1757
1780
  ...(quote_id ? { quote_id } : {}),
1758
1781
  })));
1759
1782
  server.tool('quote_bgm_candidates', '给「配乐候选整版对比」报价(免费):每幕出 candidates 首完整候选的总点数 = 有配乐的幕数 × 候选数 × 单首价。' +
1760
- '返回 quote_id(15 分钟有效、一次性)。⚠️ 把 estimated_points 原样告诉用户,用户明确同意后才拿 quote_id 调 generate_bgm(candidates 必须与这里一致),不要擅自确认。' +
1783
+ '返回 quote_id(15 分钟有效、一次性)。⚠️ 把 estimated_points 讲给客户,客户同意后才拿 quote_id 调 generate_bgm(candidates 必须与这里一致)' + ALWAYS_ASK('多候选逐首扣费、没有整单预算预检,') + '。' +
1761
1784
  '另有少量配乐总监 LLM 文本费(每幕一次),不在报价内。', {
1762
1785
  episode_id: z.number().int().positive(),
1763
1786
  candidates: z.number().int().min(2).max(3).describe('每幕几首候选(2~3)'),
@@ -1865,7 +1888,7 @@ export function registerProduceTools(server, client) {
1865
1888
  '异步生成后用它看进度。免费。' +
1866
1889
  '★整集出帧(generate_frames)是后台逐镜派发,被拒的镜不会出现在任务列表里——看响应的 frame_dispatch_rejections' +
1867
1890
  '(每镜 storyboard_id/frame_type/code/message)。code=ERA_USER_REF_CONFLICT 表示客户上传的定妆图被时代审核判冲突:' +
1868
- '把 message 转告客户、由客户确认后,带 allow_era_ref_conflict=true 对那几镜 generate_shot_frame。重启后该列表清空。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/jobs`)));
1891
+ '把 message 转告客户' + ALWAYS_ASK('allow_era_ref_conflict ') + ',客户确认后带 allow_era_ref_conflict=true 对那几镜 generate_shot_frame。重启后该列表清空。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/jobs`)));
1869
1892
  server.tool('get_drama_bill', '本剧净费用账单(免费)。核账、回答「到底花了多少、钱花在哪、退了多少」时用它,别自己拼流水。' +
1870
1893
  '\n· totals / by_kind:每类 charged(扣费)、refunded(已退回)、net(净额)、charges/refunds(笔数)。' +
1871
1894
  '**net 才是花费**,与网页「本剧已花费」、get_budget_status 同源;charged 含失败后自动退回的空转,只用于核账。' +
@@ -1896,8 +1919,7 @@ export function registerProduceTools(server, client) {
1896
1919
  'not_included 列出不在预估里的花费(文本步、配音配乐、质检重出),一并讲给客户;质检重出这部分正是「自动追加」兜底的。' +
1897
1920
  'availability 是全部模型对该题材的可用性(专业客户自选模型时用,unavailable 的不能选)。' +
1898
1921
  '★专业客户自选模型:带 custom(图片模型+视频引擎+分辨率)再调一次,返回体多一个 custom(可用性+实时预估);把 custom.estimate 讲给客户,确认时把 custom.estimate.total.upper 作为 expected_total_upper 原样带回。' +
1899
- '★overrun_history = 历史上多少剧会用到自动追加(按扣费账本统计):sufficient=true 时可告诉客户「历史上约 used_overrun_pct% 的剧会用到追加,平均多 avg_excess_pct_when_used%,选 10/20/30% 追加能兜住 covered_pct_by_overrun 里的比例」,并说明这是历史统计不是承诺;' +
1900
- 'sufficient=false(暂无足够数据)时**不许**给客户任何比例或自己估一个数,只说追加用于兜底质检重出、审核拒绝后的重出,用不到不扣。' +
1922
+ '★「历史上多少剧会用到追加」平台目前不提供:**不许**自己估一个比例或数字给客户;客户问追加会不会用到,只说明追加额度用于兜底质检重出、审核拒绝后的重出,用不到不扣。' +
1901
1923
  '★题材 content_class 决定可用性:同一引擎可能动漫可用、写实真人暂不可用——按剧情实际画面选,别为了用某个模型改题材。', {
1902
1924
  content_class: z.enum(['realistic_human', 'stylized', 'product']).describe('题材:realistic_human 写实真人 / stylized 动漫·风格化 / product 产品·空镜'),
1903
1925
  episodes: z.number().int().min(1).max(200).describe('集数'),
@@ -1918,7 +1940,9 @@ export function registerProduceTools(server, client) {
1918
1940
  '调用前必须把汇总完整讲给客户:所选套餐与效果、全剧预估(通常值与上界)、执行模式、预算、追加比例,以及「追加只在预算用完后才动用、到硬上限会停下来问」——客户点头才传 customer_confirmed:true。' +
1919
1941
  '★确认后平台在每次收费派发前复核:①本剧模型对本剧题材仍可用,否则 409 MODEL_UNAVAILABLE;②报价上界+已花 ≤ 预算×(1+追加%),否则 402 BUDGET_WOULD_EXCEED(都未扣费)。' +
1920
1942
  '被拦就停下问客户,别自己换模型、别自己调预算。' +
1921
- '★执行模式本期只记录:各层审查(review_*)与 agent 质检照旧,error 照旧要客户知情才能 acknowledge_review。' +
1943
+ '★执行模式会改变 agent 与审查闸的行为(托管灰度 AUTOPILOT_DRAMAS 内的剧):一键 auto 全部环节托管、混合 custom 按 stage_control 逐环节托管、专业 pro 每步客户把控。' +
1944
+ '托管环节里白名单内的启发式 error 可带 auto_acknowledge:true 放行,其余自修(每层最多 max_fix_rounds 轮,默认 3)后仍不过就暂停问客户;服务端写决策日志(get_autopilot_log)。' +
1945
+ '确认后先调 get_production_plan 读 autopilot.instructions 照它执行。' +
1922
1946
  '★选套餐要带 expected_combo(该档 combo 原样)与 expected_total_upper:确认的必须是客户看到的那组模型与预估。' +
1923
1947
  '★自选组合(custom)要先调 get_production_menu 带同一组 custom 拿到预估讲给客户,确认时带 expected_total_upper=custom.estimate.total.upper;没带或重算后涨幅超 5% ⇒ 409 CUSTOM_ESTIMATE_UNCONFIRMED(未写入),返回体里有最新预估,讲给客户同意后带新值再确认。' +
1924
1948
  '409 PACKAGE_CHANGED = 菜单返回后该档换了组合或预估明显上涨,返回体里有新的组合与预估,重新讲给客户、同意后带新值再确认;' +
@@ -1950,12 +1974,21 @@ export function registerProduceTools(server, client) {
1950
1974
  }).optional().describe('execution_mode=custom 时必填:每个环节 customer(客户把控)或 agent(托管)'),
1951
1975
  budget_points: z.number().int().positive().optional().describe('预算(点)。auto / custom 必填(客户授权 agent 自主花费的上限);建议不低于所选套餐的 estimate.total.upper'),
1952
1976
  overrun_pct: z.union([z.literal(0), z.literal(10), z.literal(20), z.literal(30)]).optional().describe('自动追加上限(%),默认 0。硬上限 = 预算 × (1 + 本值/100)'),
1977
+ max_fix_rounds: z.number().int().min(0).max(5).optional().describe('托管环节每层最多自修几轮(默认 3);到上限仍有问题就暂停、汇总问客户'),
1953
1978
  episodes: z.number().int().min(1).max(200),
1954
1979
  minutes_per_episode: z.number().positive().max(30),
1955
1980
  }, async ({ drama_id, ...body }) => jsonResult(await client.producePost(`/dramas/${drama_id}/production-plan`, body)));
1956
1981
  server.tool('get_production_plan', '查本剧已确认的开场方案(免费)。返回方案快照、当前模型、models_unavailable_now(本剧模型此刻已不可用时非空——出图/出视频前要先问客户怎么办)、' +
1957
1982
  'budget(含 hard_cap 硬上限、overrun_in_use 是否已在动用追加额度)。confirmed=false 表示还没确认过方案。' +
1958
- '★budget.overrun_in_use 变成 true 时主动告诉客户「预算已用完、正在动用追加额度,还剩 cap_remaining 点」。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/production-plan`)));
1983
+ '★budget.overrun_in_use 变成 true 时主动告诉客户「预算已用完、正在动用追加额度,还剩 cap_remaining 点」。' +
1984
+ '★autopilot = 执行模式下发给你的那一份:instructions(照它执行的总纲)、stage_control(每个环节归客户还是托管)、max_fix_rounds、' +
1985
+ 'default_decisions(托管环节遇到选择照表执行,不问客户)、never_auto_actions(任何模式都要客户同意)、auto_ack_codes / never_auto_ack_codes(审查 error 能不能自动放行)。' +
1986
+ 'enabled=false 表示托管还没对本剧开放,一律按专业模式执行。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/production-plan`)));
1987
+ server.tool('get_autopilot_log', '一键 / 混合模式的托管决策日志与交付报告(免费·只读)。日志由**服务端**在审查、审查闸、质检提交时写入——你自报的「修了几轮、放行了什么」不算数,以这里为准。' +
1988
+ '返回 pending_questions(必须客户决定的问题:不在自动放行白名单、或自修已到上限;每条带集号、环节、blocking 码与原因)、' +
1989
+ 'report(交付报告:budget 花费与硬上限、layers 每集每层自修轮数与最后一次审查/质检结论、auto_acknowledged 自动放行了什么、leftovers 遗留瑕疵)、entries(最近 200 条原始记录)。' +
1990
+ '★pending_questions 非空就暂停,把它们**汇总后一次性**问客户(别一条一条问),客户答复前别往那一层下游推;客户同意照现状继续 → 下一步带 acknowledge_review:true。' +
1991
+ '★出完片调一次,把 report 讲给客户:花了多少、每层自修几轮、自动放行了哪些启发式问题、还留着哪些瑕疵。交付验收 confirm_delivery_acceptance 只能客户本人确认。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/autopilot-log`)));
1959
1992
  // ========== P1 · 整集配音(成片前的强制音频步)==========
1960
1993
  server.tool('assign_voices', '★给本集所有角色分配音色(配音导演 agent 按性别/性格/年龄/角色定位+项目语言选)。' +
1961
1994
  'voiceStyle 不是提取时自动填的——不分配,generate_tts 就没音色。文本步(LLM)后付。' +
@@ -1975,7 +2008,7 @@ export function registerProduceTools(server, client) {
1975
2008
  hint: z.string().max(300).optional().describe('用户对曲风的偏好(可选),如「想要男女对唱」'),
1976
2009
  }, async ({ drama_id, hint }) => jsonResult(await client.producePost(`/dramas/${drama_id}/mv/song/styles`, hint ? { hint } : {})));
1977
2010
  server.tool('quote_mv_song', 'MV 项目·AI 作曲第 2 步:报价(免费)。总点数 = candidates × 单首价;返回 quote_id(15 分钟有效、一次性)。' +
1978
- '⚠️ 把 points 原样告诉用户,用户明确同意后才拿 quote_id 调 generate_mv_song(candidates 必须一致),不要擅自确认。', {
2011
+ '⚠️ 把 points 原样告诉客户,客户明确同意后才拿 quote_id 调 generate_mv_song(candidates 必须一致)' + ALWAYS_ASK('多候选并发扣费、没有整单预算预检,') + '。', {
1979
2012
  drama_id: z.number().int().positive(),
1980
2013
  candidates: z.number().int().min(1).max(3).describe('出几首候选对比(1~3),每首单独计费'),
1981
2014
  }, async ({ drama_id, candidates }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/mv/song/quote?candidates=${candidates}`)));
@@ -2175,18 +2208,18 @@ export function registerProduceTools(server, client) {
2175
2208
  '★适用:两镜是同一机位、同一景别的延续(seam_state=unchained/broken 且本镜本来就该和上镜一个构图)。' +
2176
2209
  '★别用于景别有变化的缝(全景→近景等):那会把本镜的景别设计冲掉,且后续出图可能被判「与设计不符」——' +
2177
2210
  '这种缝用 chain_shot_from 的 mode=reframe。本镜已有视频的会被标为待重生。' +
2178
- '本镜首帧若已被客户定稿(first_frame_pinned)会被拒;客户明确同意覆盖才传 force:true。', {
2211
+ '本镜首帧若已被客户定稿(first_frame_pinned)会被拒;覆盖它' + ALWAYS_ASK('') + '才传 force:true。', {
2179
2212
  storyboard_id: z.number().int().positive().describe('要改首帧的那一镜(缝的右侧)'),
2180
2213
  force: z.boolean().optional().describe('仅当客户明确同意覆盖已定稿的首帧时传 true'),
2181
2214
  }, async ({ storyboard_id, force }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/chain-from-prev`, force ? { force: true } : {})));
2182
- server.tool('quote_chain_shot_from', '(免费报价)chain_shot_from 的 mode=reframe 要扣**一张图**(以源帧为锚重生本镜首帧),执行前先报价、经客户确认。' +
2215
+ server.tool('quote_chain_shot_from', '(免费报价)chain_shot_from 的 mode=reframe 要扣**一张图**(以源帧为锚重生本镜首帧),执行前先报价' + MODE_DECIDES('报价确认') + '。' +
2183
2216
  '回 quote_id / estimated_points;quote_id 一次性、只对这一镜有效。mode=copy 不需要报价。', { storyboard_id: z.number().int().positive().describe('要重生首帧的那一镜') }, async ({ storyboard_id }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/chain-from/quote`)));
2184
2217
  server.tool('chain_shot_from', '把同一集**任一镜**的首帧或尾帧接为本镜首帧。两种落法:\n' +
2185
2218
  '· mode=copy(默认,免费):本镜首帧直接换成源帧,画面逐像素相同——适合「回到一模一样的画面」或同机位延续。\n' +
2186
2219
  '· mode=reframe(扣一张图,必须带 quote_chain_shot_from 的 quote_id):以源帧为锚、按本镜画面描述重生首帧——' +
2187
2220
  '人物/服装/道具/光线/场景延续,但保留本镜自己的景别与构图。景别有变化的缝要接,用这个。\n' +
2188
2221
  '★源不是上一镜尾帧时(首尾呼应/回到某个画面),平台会把本镜与上一镜的缝记为「有意切换」,自动帧链不会再覆盖它。' +
2189
- '本镜已有视频的会被标为待重生;首帧已定稿的会被拒,客户同意覆盖才传 force:true。', {
2222
+ '本镜已有视频的会被标为待重生;首帧已定稿的会被拒,覆盖它' + ALWAYS_ASK('') + '才传 force:true。', {
2190
2223
  storyboard_id: z.number().int().positive().describe('要改首帧的那一镜'),
2191
2224
  source_storyboard_id: z.number().int().positive().describe('源镜 id(必须同一集);接上一镜就传上一镜的 id'),
2192
2225
  source_frame: z.enum(['first', 'last']).optional().describe('取源镜的哪一帧,默认 last(尾帧)'),
@@ -2200,7 +2233,7 @@ export function registerProduceTools(server, client) {
2200
2233
  ...(quote_id ? { quote_id } : {}),
2201
2234
  ...(force ? { force: true } : {}),
2202
2235
  })));
2203
- server.tool('quote_align_prev_shot', '(免费报价)align_prev_shot 会重生**上一镜的视频**(扣一条视频),执行前先报价、经客户确认。' +
2236
+ server.tool('quote_align_prev_shot', '(免费报价)align_prev_shot 会重生**上一镜的视频**(扣一条视频),执行前先报价' + MODE_DECIDES('报价确认') + '。' +
2204
2237
  '回 quote_id / estimated_points / prev_storyboard_id(被重生的是哪一镜);含首次人像核验费时另列 kyc_first_time_points。', { storyboard_id: z.number().int().positive().describe('缝右侧那一镜(它的首帧将作为上一镜视频的收尾画面)') }, async ({ storyboard_id }) => jsonResult(await client.producePost(`/storyboards/${storyboard_id}/align-prev/quote`)));
2205
2238
  server.tool('align_prev_shot', '缝裂修复「对齐上一镜」:本镜首帧**不动**,以它为收尾画面、用首尾帧模式重生**上一镜的视频**(上一镜首帧不变),' +
2206
2239
  '让上一镜真实收在本镜的开场画面上。扣一条视频,必须带 quote_align_prev_shot 的 quote_id。' +