@starreel/mcp 0.1.65 → 0.1.67

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.
package/SKILL.md CHANGED
@@ -288,7 +288,16 @@ content that will be rejected.
288
288
 
289
289
  **Soft checkpoints** (not enforced, also free, still expected): `run_precheck`
290
290
  before any image or video generation (it catches shots the vendor will
291
- reject — pure wasted spend otherwise); `get_health_report` after
291
+ reject — pure wasted spend otherwise). **When it flags contradictory
292
+ instructions, don't patch them blind with `update_shot`.** Call
293
+ `plan_precheck_fix` to have the platform work out what to change, walk the
294
+ proposals through with the user shot by shot, and apply the ones they accept
295
+ with `apply_precheck_fix`. That path keeps the optimistic lock and the
296
+ pre-write re-checks; editing by hand skips both. The platform deliberately
297
+ offers no one-click auto-fix — these heuristics carry false positives, and a
298
+ silent rewrite would damage shots that were already correct. Categories the
299
+ platform will *not* touch come back under `blocked`; those are the ones that
300
+ genuinely need a human. `get_health_report` after
292
301
  storyboards; `get_characters` after portraits to confirm every on-screen
293
302
  character has an image and a sheet; `get_storyboards` after frames and after
294
303
  videos to read `frame_status` / `video_status` / `fail_reason` / `fail_hint`
@@ -615,6 +624,37 @@ to close") tells the vendor to fit that entire sequence into each 3-second shot.
615
624
  shots in one call)
616
625
  - **Identity & consistency**: `generate_character_portraits`, `upload_image`,
617
626
  `set_character_portrait`, `generate_character_sheet`, `extract_visual_lock`
627
+ - **Wardrobe**: `list/create/get/update/delete_wardrobe`,
628
+ `regenerate_wardrobe_image`, and the two-step timeline
629
+ `build_wardrobe_timeline` (LLM, **billed** — computes per-shot change
630
+ suggestions, changes nothing) → walk the diff past the user → `apply_wardrobe_timeline`
631
+ (or `apply_wardrobe_appearances` when several characters share a frame).
632
+ **What an outfit actually is here is a structured binding**
633
+ (`active_wardrobe_id` / `appearances[].wardrobeAssetId`), not prose in the
634
+ shot description — editing the text via `update_shot` and leaving the binding
635
+ alone leaves the character in the old outfit.
636
+ - **Continuity ledger**: `get_world_state` (which props / garments / injuries
637
+ are tracked, and where two shots disagree), `rebuild_world_state` (LLM,
638
+ **billed** — rerun it when `stale=true`), `add/delete_world_state_event`
639
+ (explain away a conflict the AI mis-read instead of editing shots),
640
+ `link_world_state_scene_variant`, `derive_pose_chain` (LLM, **billed** — fills
641
+ only the shots whose poses are blank, so cuts connect).
642
+ - **Era contract**: `set_era_contract` / `get_era_contract` — see the project
643
+ settings tier above. Non-contemporary projects must set this before frames.
644
+ - **Lengthen a finished cut**: `quote_extend_final` (free, quote first) →
645
+ `extend_final` (**billed** per round) → `get_extend_final` → `apply_extend_final`.
646
+ The extension is *not* the episode cut until you apply it, and applying
647
+ **replaces** the current cut. A 403 means the drama's video engine isn't
648
+ eligible — don't retry.
649
+ - **Recover an older asset**: `get_asset_recovery` lists cuts and candidate
650
+ frames that were superseded. Reach for it when the user says "the previous
651
+ version was better". Apply them **one at a time, only after the user confirms
652
+ each**: `apply_recovered_final`, or `apply_recovered_candidate` (which
653
+ requires an explicit `target_storyboard_id` — the platform will not guess
654
+ which shot a recovered frame belongs to). `rollback_asset_recovery` undoes one.
655
+ - **Marketing export**: `export_sheet_compare` / `get_sheet_compare` — the
656
+ character-sheet-vs-frame comparison sheet. **Off by default per drama**; a 403
657
+ or `enabled: false` means the user has to switch it on in the drama settings.
618
658
  - **Shots → video**: `quote/generate_storyboards`, `get_storyboards`,
619
659
  `quote/generate_frames`, `chain_frames`, `quote/generate_videos`
620
660
  - **Audio**: `generate_tts` (required before final cut), `clone_voice`,
@@ -76,7 +76,8 @@ const WORKFLOW_HINT = '★三档执行策略(别把三档混着问客户):' +
76
76
  '每次审查返回 review_token,把它随下游收费工具一起传;findings 逐条讲给客户(code=问题类型·shots=命中镜号·action=该调哪个工具修),' +
77
77
  '按 action 修完后**复审**再走。审查后又改了内容 → token 自动失效,复审一次即可(免费)。' +
78
78
  '有 error 时默认拦截,只有客户明确知情并坚持才带 acknowledge_review:true——别替客户做这个决定。' +
79
- '**软引导(不阻断但强烈建议,同样免费)**:出图/出视频前跑 run_precheck(揪出必被厂商拒的镜,防白花钱);' +
79
+ '**软引导(不阻断但强烈建议,同样免费)**:出图/出视频前跑 run_precheck(揪出必被厂商拒的镜,防白花钱)——' +
80
+ '★揪出来之后别自己盲改:plan_precheck_fix 让平台算出提案 → 逐条讲给客户 → 客户点头后 apply_precheck_fix 落库;' +
80
81
  '分镜后跑 get_health_report;定妆图出完用 get_characters 核对每个出场角色都有 image/sheet;' +
81
82
  '出帧后用 get_storyboards 看 frame_status 与 fail_reason/fail_hint(failed 的镜先修再往下,别带着废帧出视频);' +
82
83
  '出视频后同样看 video_status;成片前用 get_pipeline_status 确认没有缺镜。' +
@@ -492,7 +493,40 @@ export function registerProduceTools(server, client) {
492
493
  '④**指令自相矛盾(kind=prompt-conflict)**——同一镜里互斥的要求(如宽景别却标了特写主体、' +
493
494
  '既要站立又要坐姿),这类镜**任何正确的图都满足不了**,不改就会反复被拒并反复扣费,' +
494
495
  '出现时应先按提示改分镜再出图,而不是重试。\n' +
496
+ '★**怎么改不用你猜**:调 plan_precheck_fix 让平台算出提案(哪一镜、把什么改成什么),' +
497
+ '讲给客户、客户点头后用 apply_precheck_fix 落库——比你自己用 update_shot 盲改稳,' +
498
+ '那条路绕开了乐观锁与落库前复核。部分类别系统不替你改(提案里的 blocked),那些才需要人工调。\n' +
495
499
  '⚠️ 它**不**检查首帧是否处在"动作发生前"(平台暂无该契约字段),也不替代 get_health_report。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/precheck`)));
500
+ server.tool('plan_precheck_fix', '让平台**算出**该怎么改 run_precheck 揪出的「指令自相矛盾」类问题(第①步,只算不改)。'
501
+ + '按文本用量计费(很小),不走报价确认。\n'
502
+ + '★两段式,这一步不动任何数据:拿到提案后**逐条讲给客户**(哪一镜、把什么改成什么、为什么),'
503
+ + '客户点头后再把采纳的那几条**原样**传给 apply_precheck_fix 落库。\n'
504
+ + '★**不要替客户决定**:这些判据有假阳,平台刻意不做「一键自动修」——'
505
+ + '曾实测同一批提案里近半是判据误报,静默改会把本来正确的分镜改坏。\n'
506
+ + '★返回的 blocked 列出「系统不替你改」的类别:那些要人工按 run_precheck 的提示调整分镜。'
507
+ + '看到 blocked 不等于没问题,反而是**必须人工处理**的那部分。\n'
508
+ + '不传 shot_id 就算整集;只想修某一镜就传它。', {
509
+ episode_id: z.number().int().positive(),
510
+ shot_id: z.number().int().positive().optional()
511
+ .describe('只算这一镜的提案;不传则整集。注意首帧类判据始终要看上一镜,所以上下文仍取整集'),
512
+ }, async ({ episode_id, shot_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/precheck-fix/plan`, shot_id ? { shot_id } : {})));
513
+ server.tool('apply_precheck_fix', '把客户**确认过**的提案落库(第②步)。免费(纯文本写库)。'
514
+ + '入参就是 plan_precheck_fix 回的提案对象,**原样传回**即可——只传客户勾选采纳的那几条。\n'
515
+ + '★落库前平台还会再过三道闸:乐观锁(库里的值变了就跳过,绝不覆盖别人的改动)、'
516
+ + '准入复判、自净闸(改完判据没转绿也跳过)。所以部分条目进 skipped 是正常的,不是失败。\n'
517
+ + '★返回的 remaining = 本集**还剩几条**预检问题,不是「改了几个字段」。'
518
+ + '改完重跑 run_precheck 确认,再往下出图。', {
519
+ episode_id: z.number().int().positive(),
520
+ proposals: z.array(z.object({
521
+ shot_id: z.number().int().positive(),
522
+ task: z.string().optional(),
523
+ changes: z.array(z.object({
524
+ field: z.string(),
525
+ old_value: z.string().nullable().optional().describe('★必须是 plan 给的原值:落库时做乐观锁,库里已经不是它了就跳过'),
526
+ new_value: z.string().nullable().optional(),
527
+ })),
528
+ })).describe('plan_precheck_fix 回的提案,原样传回;只放客户点头采纳的那几条'),
529
+ }, async ({ episode_id, proposals }) => jsonResult(await client.producePost(`/episodes/${episode_id}/precheck-fix/apply`, { proposals })));
496
530
  server.tool('get_health_report', '(★推荐·免费诊断)读分镜出体检报告:时长超标/母题覆盖不足/问题镜。纯读、免费。出图前查,识别问题先改再出、别出了片才发现。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/health-report`)));
497
531
  // ---------- 分阶段审查(三道硬闸的凭据来源;全部免费) ----------
498
532
  // 每层审查返回 { pass, error_count, warning_count, findings[], review_token }。
@@ -595,6 +629,154 @@ export function registerProduceTools(server, client) {
595
629
  server.tool('get_final_cut', '查某一集成片状态与下载链接。status=completed 时返回 download_url(我方 COS 直链,可直接下载)。免费。' +
596
630
  '★bgm_stale=true 表示配乐在成片之后生成/改动、尚未进成片:重新 compose_episode(免费)即可,别用 re-render。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/final-cut`)));
597
631
  server.tool('get_export', '查某一集导出/母版状态(成片终拼后的可下载母版)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/export`)));
632
+ server.tool('get_asset_recovery', '列本集可恢复的**历史产物**(免费·只读):以前生成过、后来被覆盖掉的成片与候选帧。'
633
+ + '\n用途:客户说「上一版那个好」「前天那张图比现在这张好」时,从这里找回来。'
634
+ + '\n★这一步只看不改。把候选逐条讲给客户,**客户逐条点头**之后再 apply。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/asset-recovery`)));
635
+ server.tool('apply_recovered_final', '把某个**历史终拼**设为本集当前成片(免费,指针切换)。'
636
+ + '\n★这会**覆盖**本集现在对外的那一版成片。先用 get_asset_recovery 列出来、'
637
+ + '把是哪一版(时间/时长)讲清楚,**客户逐条确认**再调——不要凭「看起来更好」自己决定。'
638
+ + '\n★之后重新 compose_episode(终拼)会再把它覆盖回去。', {
639
+ episode_id: z.number().int().positive(),
640
+ merge_id: z.number().int().positive().describe('取自 get_asset_recovery 的 finals[].id'),
641
+ }, async ({ episode_id, merge_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/asset-recovery/finals/${merge_id}/apply`, {})));
642
+ server.tool('apply_recovered_candidate', '把某张**历史候选帧**回填到指定镜头(免费)。'
643
+ + '\n★**必须显式指定 target_storyboard_id**:这张图要回到哪一镜只有客户知道,平台不替你猜——'
644
+ + '猜错目标镜比不做更糟(会把另一镜正在用的帧换掉)。'
645
+ + '\n★同样是**逐条确认**:一次只回填一张,先把「哪张图、回到第几镜」讲给客户听。', {
646
+ episode_id: z.number().int().positive(),
647
+ generation_id: z.number().int().positive().describe('取自 get_asset_recovery 的 candidates[].id'),
648
+ target_storyboard_id: z.number().int().positive().describe('回填到哪一镜(必填,不猜)'),
649
+ }, async ({ episode_id, generation_id, target_storyboard_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/asset-recovery/candidates/${generation_id}/apply`, { target_storyboard_id })));
650
+ server.tool('rollback_asset_recovery', '把一次已应用的恢复**退回去**(免费)。apply 错了就用它撤销,恢复到应用之前的状态。', {
651
+ episode_id: z.number().int().positive(),
652
+ event_id: z.number().int().positive().describe('取自 get_asset_recovery 的恢复事件 id'),
653
+ }, async ({ episode_id, event_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/asset-recovery/events/${event_id}/rollback`, {})));
654
+ server.tool('list_wardrobe', '列本剧的**服装资产库**(免费)。每件服装是一个可被镜头引用的结构化资产,'
655
+ + '不是分镜里的文字描述。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/wardrobe`)));
656
+ server.tool('create_wardrobe', '新建一件服装资产(免费,纯文本写库)。建好之后要**绑到镜头上**才会影响出图——'
657
+ + '用 apply_wardrobe_timeline / apply_wardrobe_appearances,或在官网逐镜挑。', {
658
+ drama_id: z.number().int().positive(),
659
+ name: z.string().describe('这件服装叫什么(角色在剧里的某一身)'),
660
+ character_id: z.number().int().positive().optional().describe('属于哪个角色;通用服装可不填'),
661
+ episode_id: z.number().int().positive().optional(),
662
+ description: z.string().optional().describe('款式/材质/颜色等外观描述'),
663
+ palette_hint: z.any().optional().describe('配色提示'),
664
+ reference_image_urls: z.array(z.string()).optional().describe('参考图直链'),
665
+ notes: z.string().optional(),
666
+ product_id: z.number().int().positive().optional().describe('带货场景:关联商品'),
667
+ sort_order: z.number().int().optional(),
668
+ }, async (args) => jsonResult(await client.producePost('/wardrobe', args)));
669
+ server.tool('get_wardrobe', '读一件服装资产的详情(免费)。', { wardrobe_id: z.number().int().positive() }, async ({ wardrobe_id }) => jsonResult(await client.produceGet(`/wardrobe/${wardrobe_id}`)));
670
+ server.tool('update_wardrobe', '改服装资产(免费)。改完**不会**自动重出已生成的图:要让画面跟上得再调 regenerate_wardrobe_image,'
671
+ + '已出的镜头帧还要逐镜重出。', {
672
+ wardrobe_id: z.number().int().positive(),
673
+ name: z.string().optional(),
674
+ character_id: z.number().int().positive().optional(),
675
+ episode_id: z.number().int().positive().optional(),
676
+ description: z.string().optional(),
677
+ palette_hint: z.any().optional(),
678
+ reference_image_urls: z.array(z.string()).optional(),
679
+ notes: z.string().optional(),
680
+ product_id: z.number().int().positive().optional(),
681
+ sort_order: z.number().int().optional(),
682
+ }, async ({ wardrobe_id, ...fields }) => jsonResult(await client.producePut(`/wardrobe/${wardrobe_id}`, fields)));
683
+ server.tool('regenerate_wardrobe_image', '重出这件服装的资产图(**收费**,图片步按用量后付)。改完 description/参考图之后用它让图跟上。', { wardrobe_id: z.number().int().positive() }, async ({ wardrobe_id }) => jsonResult(await client.producePost(`/wardrobe/${wardrobe_id}/regenerate`, {})));
684
+ server.tool('delete_wardrobe', '软删一件服装资产(免费)。已绑定它的镜头不会被自动改,先确认没有镜头还在用。', { wardrobe_id: z.number().int().positive() }, async ({ wardrobe_id }) => jsonResult(await client.produceDelete(`/wardrobe/${wardrobe_id}`)));
685
+ server.tool('build_wardrobe_timeline', '算**服装时间线**:从剧本与分镜推出每个角色在各镜应该穿哪一身,并给出逐镜建议 diff。'
686
+ + '**要花钱**(走 LLM)。这是第①步,**只算不改**。\n'
687
+ + '★两段式:算完把 diff **逐条讲给客户**(哪一镜、从哪身换成哪身、为什么),'
688
+ + '客户勾选后再用 apply_wardrobe_timeline 落库。别替客户全盘应用。\n'
689
+ + '★**这一步解决的是「文字改了不生效」**:决定出图穿什么的是结构化绑定'
690
+ + '(active_wardrobe_id / appearances[].wardrobeAssetId),不是 update_shot 里的文字描述。'
691
+ + '只改文字、不改绑定,出图照旧穿原来那身。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/wardrobe-timeline`, {})));
692
+ server.tool('apply_wardrobe_timeline', '把客户勾选的换装建议落库(第②步,免费)。改的是每镜的 **active_wardrobe_id 结构化绑定**,'
693
+ + '不是文字描述——这正是「改了 update_shot 的文字却不生效」那个坑的正解。\n'
694
+ + '★只传客户点头的那几条;一次最多 200 条。\n'
695
+ + '★多角色同框的镜头单列装不下,那些走 apply_wardrobe_appearances。', {
696
+ episode_id: z.number().int().positive(),
697
+ items: z.array(z.any()).describe('build_wardrobe_timeline 给的建议,只放客户勾选的那几条'),
698
+ }, async ({ episode_id, items }) => jsonResult(await client.producePost(`/episodes/${episode_id}/wardrobe-timeline/apply`, { items })));
699
+ server.tool('apply_wardrobe_appearances', '逐角色回填 appearances 的服装绑定(第②步的多角色版,免费)。'
700
+ + '用于**多角色同框**的镜头:单个 active_wardrobe_id 装不下几个人各穿各的,'
701
+ + '这条按角色逐个绑。同样只传客户勾选的那几条,一次最多 200 条。', {
702
+ episode_id: z.number().int().positive(),
703
+ items: z.array(z.any()).describe('build_wardrobe_timeline 给的建议,只放客户勾选的那几条'),
704
+ }, async ({ episode_id, items }) => jsonResult(await client.producePost(`/episodes/${episode_id}/wardrobe-timeline/apply-appearances`, { items })));
705
+ server.tool('get_world_state', '读本集的**世界状态账本**:被追踪的实体(道具/服装/伤痕一类)在各镜之间的状态,'
706
+ + '以及对不上的地方(conflicts,带命中的两个镜号)。免费。'
707
+ + '\n★stale=true = 分镜改过但账本没重建,先 rebuild_world_state 再看,否则看的是旧账。'
708
+ + '\n★账本目前只是**报表**:不拦生成、也不自动注入出图。看到 conflicts 要自己决定'
709
+ + '改分镜(update_shot)还是加一条手工事件说明(add_world_state_event)。'
710
+ + '\n逐条事件的文字详情在官网面板看,这里给结构化概览。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/world-state`)));
711
+ server.tool('rebuild_world_state', '重建世界状态账本(**要花钱**:走 LLM 从分镜里抽取实体与状态变化)。'
712
+ + '\n改完分镜、或 get_world_state 回 stale=true 时才需要重建;没改过就别重复跑。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/world-state/rebuild`, {})));
713
+ server.tool('add_world_state_event', '往账本里加一条**手工事件**(免费)。用途:AI 没抽到、或抽错了的状态变化,由你补一条说明。'
714
+ + '\n典型场景:conflicts 报「同一件道具在两镜之间变了」,但剧情里确实发生过交接/损坏——'
715
+ + '补一条手工事件,冲突即被解释掉,不必去改分镜。', {
716
+ episode_id: z.number().int().positive(),
717
+ entity_key: z.string().describe('实体标识(与账本里的 entity_key 对齐)'),
718
+ shot_number: z.number().int().positive().optional().describe('这条事件发生在第几镜'),
719
+ description: z.string().optional().describe('发生了什么(给人看的说明)'),
720
+ }, async ({ episode_id, ...body }) => jsonResult(await client.producePost(`/episodes/${episode_id}/world-state/manual`, body)));
721
+ server.tool('delete_world_state_event', '删掉一条手工事件(免费)。只能删手工加的,AI 抽取出来的要靠 rebuild_world_state 重算。', {
722
+ episode_id: z.number().int().positive(),
723
+ event_id: z.number().int().positive(),
724
+ }, async ({ episode_id, event_id }) => jsonResult(await client.produceDelete(`/episodes/${episode_id}/world-state/manual/${event_id}`)));
725
+ server.tool('link_world_state_scene_variant', '把账本里的某条事件关联到一个**场景变体**(免费)。'
726
+ + '用途:同一场景在事件前后长得不一样(灯亮/灯灭、完好/破损),关联之后出图会取对应那版场景图。', {
727
+ episode_id: z.number().int().positive(),
728
+ event_id: z.number().int().positive(),
729
+ variant_id: z.number().int().positive().nullable().describe('传 null 解除关联'),
730
+ }, async ({ episode_id, event_id, variant_id }) => jsonResult(await client.producePut(`/episodes/${episode_id}/world-state/events/${event_id}/scene-variant`, { variant_id })));
731
+ server.tool('derive_pose_chain', '派生**姿态链**(Action Graph):从相邻镜的动作推出每镜的起止姿态,让镜间衔接不跳。'
732
+ + '**要花钱**(走 LLM)。'
733
+ + '\n★只填**空着**的那些镜,已经写了姿态的不动;黑场镜自动跳过。'
734
+ + '\n出图前做,能减少「上一镜站着下一镜突然坐着」这类接不上的情况。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/derive-pose-chain`, {})));
735
+ server.tool('quote_extend_final', '★成片续写**报价**(免费·无副作用):在已有成片后面再接几轮 AI 生成的片段,把片子拉长。'
736
+ + '回 rounds(轮数)/duration_per_round(每轮秒数)/max_rounds(上限)/estimated_points(预估点数)。'
737
+ + '\n★**必须先报价、把点数讲给客户、客户确认后再调 extend_final**——每轮都真扣厂商的钱。'
738
+ + '\n★回 403 = 本剧没开这个能力(要求剧集视频引擎是 2.5 系)。这不是调用错误,别重试;'
739
+ + '告诉客户换引擎或走别的加长方式。', {
740
+ episode_id: z.number().int().positive(),
741
+ rounds: z.number().int().positive().describe('续写几轮'),
742
+ duration: z.number().positive().describe('每轮秒数'),
743
+ model: z.string().optional().describe('临时覆盖视频模型;不传用剧集设置'),
744
+ }, async ({ episode_id, rounds, duration, model }) => {
745
+ const qs = new URLSearchParams({ rounds: String(rounds), duration: String(duration) });
746
+ if (model)
747
+ qs.set('model', model);
748
+ return jsonResult(await client.produceGet(`/episodes/${episode_id}/extend-final/quote?${qs.toString()}`));
749
+ });
750
+ server.tool('extend_final', '发起成片续写(**收费**,每轮走视频生成扣费链)。后台串行跑,立即返回会话 id,用 get_extend_final 轮询。'
751
+ + '\n★调之前**必须**先 quote_extend_final 报价并让客户确认点数——不要替客户决定花钱。'
752
+ + '\n★续写只是「在成片后面接片段」,不改前面已有的内容;接出来的片段也**不会**自动成为本集成片,'
753
+ + '满意之后要显式调 apply_extend_final 才生效。'
754
+ + '\n★403 = 本剧没开(引擎需 2.5 系),别重试。', {
755
+ episode_id: z.number().int().positive(),
756
+ rounds: z.number().int().positive(),
757
+ duration: z.number().positive(),
758
+ model: z.string().optional(),
759
+ }, async ({ episode_id, ...body }) => jsonResult(await client.producePost(`/episodes/${episode_id}/extend-final`, body)));
760
+ server.tool('get_extend_final', '查本集所有续写会话的进度与产物。免费。'
761
+ + '每条会话回 status(pending/processing/completed/partial/failed)、rounds_completed、'
762
+ + 'final_url(产物直链)、applied(该产物**当前是不是**本集成片)、error_msg。'
763
+ + '\n★status=processing 时耐心轮询,别重复发起 extend_final(那会再扣一次钱)。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/extend-final`)));
764
+ server.tool('apply_extend_final', '把某个续写产物**设为本集成片**(免费,纯指针切换)。'
765
+ + '\n★这一步会**覆盖**本集当前的成片链接:之前的成片不再是对外那一版。'
766
+ + '先用 get_extend_final 看 final_url、确认客户满意再调。'
767
+ + '\n★之后若重新 compose_episode(终拼),成片会被终拼结果覆盖回去,applied 自动变 false。', {
768
+ episode_id: z.number().int().positive(),
769
+ session_id: z.number().int().positive().describe('取自 get_extend_final 返回的 sessions[].id'),
770
+ }, async ({ episode_id, session_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/extend-final/${session_id}/apply`, {})));
771
+ server.tool('export_sheet_compare', '导出**设定图对照版**(角色设定图与成片画面的对照拼版,营销/交付用)。触发后台合成,用 get_sheet_compare 轮询。'
772
+ + '\n★这个能力**默认关**:它是剧集级开关(不是所有片子都允许这样对外输出),'
773
+ + '关着时回 **403**。看到 403 别重试、也别当成失败——告诉客户去官网的**剧集设置**里打开它。'
774
+ + '\n不传 character_id 就导出整集;传了只导这一个角色。', {
775
+ episode_id: z.number().int().positive(),
776
+ character_id: z.number().int().positive().optional().describe('只导这一个角色;不传=整集'),
777
+ }, async ({ episode_id, character_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/export/sheet-compare`, character_id ? { character_id } : {})));
778
+ server.tool('get_sheet_compare', '轮询设定图对照版的导出状态。免费。回 enabled(剧集开关是否打开)/processing/result/error。'
779
+ + '\n★enabled=false 就是那个剧集开关没开——不是出错,去官网剧集设置里打开再导。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/export/sheet-compare`)));
598
780
  // ========== 项目设定(建后可改)==========
599
781
  server.tool('update_project_settings', '建剧后修改项目设定。除画幅/分辨率/世界观Brief/族裔/题材/导演风格外,现覆盖★整剧视觉一致性锚' +
600
782
  '(摄影DNA cinematography_prompt/美术圣经 art_bible/视觉锁定 visual_lock/视频风格正负向词/视觉母题 motifs)、' +