File size: 56,294 Bytes
90647f1
2166671
 
 
 
 
 
 
8c920d4
90647f1
 
 
10d10f3
90647f1
2166671
8c920d4
 
 
 
 
 
 
0cf9806
 
d80fde8
46665f5
7a3a29b
edd35e3
bd24c9a
7a3a29b
d80fde8
90647f1
 
 
 
0cf9806
90647f1
2166671
 
5221da3
d80fde8
90647f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
517e1b0
90647f1
2166671
 
 
90647f1
2166671
 
7a3a29b
 
 
90647f1
 
 
 
 
 
 
 
 
2166671
90647f1
 
2166671
662b21a
2166671
 
662b21a
 
 
 
2166671
 
7a3a29b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226deb2
 
662b21a
 
 
2166671
 
 
90647f1
 
 
 
 
 
 
2166671
8b46bea
 
 
 
2166671
c764057
 
2166671
 
 
 
90647f1
 
 
2166671
 
 
90647f1
 
 
2166671
90647f1
2166671
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76951fa
2166671
 
 
 
 
 
 
76951fa
2166671
76951fa
2166671
 
 
 
 
 
 
 
 
 
 
76951fa
2166671
 
 
 
 
 
 
 
 
 
 
 
 
 
76951fa
2166671
 
 
 
 
 
 
76951fa
2166671
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76951fa
2166671
 
 
 
 
 
 
 
90647f1
2166671
 
 
 
 
 
 
 
90647f1
2166671
 
90647f1
2166671
 
 
90647f1
2166671
 
a9161f7
90647f1
2166671
a9161f7
2166671
a9161f7
90647f1
a9161f7
 
 
 
90647f1
0f4d71a
a9161f7
 
 
2166671
 
 
 
 
0f4d71a
2166671
8c920d4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2166671
662b21a
 
 
 
0f4d71a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c920d4
0f4d71a
 
 
 
8c920d4
c4319e1
 
0f4d71a
 
 
8c920d4
0f4d71a
 
 
8c920d4
0f4d71a
90647f1
0f4d71a
 
90647f1
39440c4
 
 
 
c4319e1
662b21a
76951fa
a9161f7
 
 
2166671
 
 
76951fa
2166671
90647f1
2166671
90647f1
2166671
a9161f7
2166671
3f49c26
0f4d71a
 
 
 
 
 
 
 
2166671
 
 
0f4d71a
 
 
 
 
 
 
 
 
 
 
 
2166671
 
 
 
 
0e98142
2166671
 
90647f1
 
2166671
 
46665f5
 
 
90647f1
 
 
2166671
90647f1
2166671
 
 
 
 
 
 
 
 
90647f1
2166671
 
90647f1
c9393b9
 
 
 
 
 
2166671
 
 
 
 
90647f1
2166671
90647f1
2166671
 
 
90647f1
10d10f3
2166671
 
 
 
3e756b3
2166671
 
 
90647f1
2166671
 
 
 
517e1b0
 
2166671
 
90647f1
d80fde8
 
 
 
 
 
 
2166671
 
 
 
 
 
8b46bea
2166671
517e1b0
 
4b81143
 
b4a7b33
 
4b81143
 
 
 
0eb0531
 
 
b4a7b33
4b81143
2166671
 
0eb0531
 
52bf1ac
 
 
0eb0531
 
52bf1ac
 
 
 
 
edd35e3
0eb0531
 
 
 
52bf1ac
0eb0531
517e1b0
0eb0531
b4a7b33
 
 
0eb0531
b4a7b33
 
0eb0531
b4a7b33
 
0eb0531
 
b4a7b33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39440c4
c6ec73d
39440c4
b4a7b33
 
0eb0531
 
 
 
b4a7b33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0eb0531
 
 
b4a7b33
0eb0531
 
 
b4a7b33
0eb0531
 
 
b4a7b33
 
 
 
 
0eb0531
b4a7b33
 
 
 
 
39440c4
b4a7b33
c6ec73d
39440c4
b4a7b33
992f513
 
 
 
 
 
 
 
 
 
 
0eb0531
 
 
992f513
 
 
 
 
 
 
 
b4a7b33
517e1b0
2166671
5221da3
 
6c1f19d
2e12ebb
6c1f19d
 
 
 
 
 
 
2e12ebb
6c1f19d
2e12ebb
 
 
 
 
 
 
6c1f19d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2e12ebb
6c1f19d
2e12ebb
 
 
 
 
 
 
6c1f19d
 
 
 
 
5221da3
2166671
 
 
c6ec73d
5221da3
4f45ad2
90647f1
2166671
 
 
 
 
 
 
 
 
 
 
 
 
90647f1
517e1b0
 
 
 
2166671
 
 
 
 
 
 
90647f1
2166671
 
a1c4fc6
2166671
 
 
 
 
 
7a3a29b
90647f1
7a3a29b
4f45ad2
aa4ec23
 
 
 
 
 
 
 
 
 
9d69867
 
 
 
 
aa4ec23
 
 
 
 
 
 
9d69867
 
aa4ec23
 
 
c6ec73d
aa4ec23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2166671
 
90647f1
2166671
 
 
90647f1
5221da3
 
6c1f19d
2e12ebb
 
6c1f19d
2e12ebb
 
6c1f19d
 
 
 
 
 
 
 
 
 
2e12ebb
6c1f19d
 
0d74ebd
6c1f19d
 
0d74ebd
6c1f19d
 
0d74ebd
6c1f19d
 
0d74ebd
6c1f19d
 
 
0d74ebd
6c1f19d
 
2e12ebb
6c1f19d
 
2e12ebb
6c1f19d
 
2e12ebb
6c1f19d
 
2e12ebb
6c1f19d
 
2e12ebb
6c1f19d
 
2e12ebb
6c1f19d
 
2e12ebb
 
6c1f19d
2e12ebb
 
6c1f19d
2e12ebb
 
6c1f19d
2e12ebb
 
6c1f19d
2e12ebb
6c1f19d
 
2e12ebb
6c1f19d
2e12ebb
 
 
6c1f19d
2e12ebb
 
6c1f19d
 
 
 
 
 
2e12ebb
6c1f19d
 
2e12ebb
e50ff12
6c1f19d
e50ff12
 
6c1f19d
 
 
 
 
2e12ebb
 
6c1f19d
2e12ebb
 
6c1f19d
 
 
 
 
 
 
2e12ebb
 
 
 
 
 
 
 
6c1f19d
 
e50ff12
6c1f19d
5221da3
517e1b0
 
 
 
 
2166671
 
2e12ebb
 
 
 
 
 
 
 
 
 
517e1b0
2e12ebb
 
 
 
 
c6ec73d
5221da3
2166671
90647f1
2166671
 
 
90647f1
2166671
 
 
 
 
90647f1
2166671
 
 
90647f1
aa4ec23
2166671
04d40e5
2166671
 
 
8b46bea
aa4ec23
 
 
90647f1
2166671
7a3a29b
2166671
 
 
8b46bea
7a3a29b
 
 
 
 
 
2166671
 
 
 
 
 
90647f1
2166671
22b29a3
 
 
 
 
 
2166671
 
22b29a3
c6ec73d
6c1f19d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2166671
 
 
 
 
 
 
22b29a3
 
 
2166671
 
10d10f3
2166671
 
 
 
 
 
10d10f3
90647f1
 
 
2166671
 
 
 
90647f1
2166671
 
 
 
 
3e756b3
2166671
 
 
 
 
 
90647f1
2166671
4f45ad2
7a3a29b
 
 
2166671
 
 
7a3a29b
517e1b0
7a3a29b
2166671
90647f1
 
46665f5
2166671
46665f5
90647f1
 
 
 
 
2166671
90647f1
2166671
8b46bea
 
 
 
3e756b3
 
 
 
 
 
 
 
 
 
 
 
 
90647f1
3e756b3
90647f1
3e756b3
 
 
 
 
 
 
 
 
 
 
 
 
 
90647f1
 
 
 
 
 
 
 
 
 
 
2166671
90647f1
 
 
c764057
 
2166671
c764057
90647f1
 
 
 
 
 
2166671
 
 
 
 
 
d80fde8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10d10f3
d80fde8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46665f5
 
d80fde8
 
 
 
cc7fe60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90647f1
2166671
90647f1
 
 
 
cc7fe60
 
 
 
 
 
 
90647f1
 
cc7fe60
2166671
90647f1
 
 
7dbc5cc
90647f1
cc7fe60
7dbc5cc
90647f1
 
 
 
2166671
90647f1
 
cc7fe60
7dbc5cc
90647f1
2166671
90647f1
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
/**
 * 使命必达执行器 - Mission-Critical Task Executor
 *
 * ★★★ 核心特性 ★★★
 * 1. 智能模型切换:超时自动切换到下一个模型,不等待看门狗
 * 2. 速度优先:记录快速模型,下次优先使用
 * 3. 无限重试:模型池轮询,直到找到响应快的模型
 * 4. 使命必达:绝不放弃,直到任务完成
 * 5. ★★★ 任务 3: 模型感知适配 - 自动折叠 System Role、裁剪 Context ★★★
 */

import pLimit from 'p-limit';
import Redis from 'ioredis';
import { ModelDispatcher } from '../lib/model-dispatcher';
import { getModelMeta, TaskPhase, MODEL_POOL } from '../lib/model-registry';
// ★★★ 任务 3: 导入适配器 ★★★
import {
  adaptPayload,
  sniffErrorAndLearn,
  getEffectiveModelMeta,
  ChatMessage,
} from '../lib/payload-adapter';
import { getDesignPrompt, getStyledPrompt } from '../lib/design-prompt-loader';
import type { StyleProfile } from '../lib/style-profiles';
import { runOpenGamePipeline } from '../lib/opengame-pipeline';
import { sanitizeErrorMessage } from '../lib/error-sanitizer';

console.log('[STARTUP] task-executor loaded, BUILD: 20260523-v9-ANTI-SLOP-V2');

export type DesignTaskType = 'html_prototype' | 'pitch_deck' | 'ui_design_system';
export type OpenGameTaskType = 'opengame_generation';

export interface TaskParams {
  prompt: string;
  sections?: number;
  style?: StyleProfile;
  maxTokens?: number;
  draftContent?: string;
  skipPhase1?: boolean;
  tier?: 'free' | 'starter' | 'pro' | 'enterprise';
  taskType?: 'full_plan' | 'sales_manual' | 'world_setting' | 'competitor_analysis' | DesignTaskType | OpenGameTaskType;
}

export interface TaskOutline {
  title: string;
  sections: Array<{
    title: string;
    points: string[];
  }>;
  metadata?: Record<string, any>;
}

export interface TaskResult {
  content: string;
  outline: TaskOutline;
  sectionsGenerated: number;
  duration: number;
  title?: string; // ★ LLM 生成的文档标题(15字以内)
  phases: {
    planning: { duration: number; model: string; attempts: number };
    expansion: { duration: number; model: string; sections: number; attempts: number };
    review: { duration: number; model: string; attempts: number };
  };
  totalAttempts: number; // 总尝试次数
  fastModelsUsed: string[]; // 使用的快速模型列表
  outputType?: 'docx' | 'zip' | 'pptx';
  fileExtension?: string;
  contentType?: string;
}

export interface ProgressReport {
  taskId: string;
  status: 'planning' | 'writing' | 'reviewing' | 'completed' | 'failed';
  progress: number;
  phase: string;
  detail?: string;
  updatedAt: number;
  modelSwitch?: string; // 模型切换信息
}

// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ★★★ 日本服务器极速跳板模式 ★★★
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

// ★★★ 任务 1: 使用日本跳板替代直连 NVIDIA ★★★
const PROXY_ENDPOINT = process.env.LLM_PROXY_ENDPOINT || 'https://game.5e1.com/api/internal/llm-proxy';
const PROXY_SECRET = process.env.INTERNAL_API_SECRET || '';

const CONCURRENCY_LIMIT = 2;

// ★★★ 设计任务类型检测 ★★★
const DESIGN_TASK_TYPES: DesignTaskType[] = ['html_prototype', 'pitch_deck', 'ui_design_system'];

function isDesignTask(taskType?: string): taskType is DesignTaskType {
  return DESIGN_TASK_TYPES.includes(taskType as DesignTaskType);
}

function getDesignTaskOutputMeta(taskType: DesignTaskType): { outputType: 'zip' | 'pptx'; fileExtension: string; contentType: string } {
  if (taskType === 'pitch_deck') {
    return {
      outputType: 'pptx',
      fileExtension: 'pptx',
      contentType: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
    };
  }
  return {
    outputType: 'zip',
    fileExtension: 'zip',
    contentType: 'application/zip',
  };
}

// ★★★ 超时策略:给模型充足生成时间 ★★★
const MODEL_TIMEOUT_MS = 45000; // 45秒总超时(含 TTFB + token 生成,mimo-v2.5 planning 需要 20-30s)
const MAX_MODEL_ATTEMPTS = 5;
const FAST_MODEL_THRESHOLD = 5000;
const SLOW_MODEL_BLACKLIST_MS = 120000;

// Redis Key 用于存储快速模型优先级
const FAST_MODEL_KEY = 'slow_task:fast_models';

export class TaskExecutor {
  private dispatcher: ModelDispatcher;
  private redis: Redis;
  private abortFlag: boolean = false;
  private progressCallback: ((report: ProgressReport) => void) | null = null;

  // 心跳状态
  private lastProgress: number = 0;
  private lastPhase: string = 'init';
  private lastDetail: string = '';

  // AbortController
  private abortController: AbortController | null = null;

  // ★★★ 新增:快速模型缓存 ★★★
  private fastModelCache: Map<string, { avgTtfb: number; lastSuccess: number }> = new Map();
  private slowModelBlacklist: Map<string, number> = new Map(); // modelId -> blacklistUntil

  constructor(dispatcher: ModelDispatcher, redis: Redis) {
    this.dispatcher = dispatcher;
    this.redis = redis;

    // 加载快速模型缓存
    this.loadFastModelCache();
  }

  /**
   * 从 Redis 加载快速模型缓存
   */
  private async loadFastModelCache(): Promise<void> {
    try {
      const data = await this.redis.hgetall(FAST_MODEL_KEY);
      if (data) {
        for (const [modelId, stats] of Object.entries(data)) {
          const parsed = typeof stats === 'string' ? JSON.parse(stats) : stats;
          this.fastModelCache.set(modelId, {
            avgTtfb: parsed.avgTtfb || 5000,
            lastSuccess: parsed.lastSuccess || Date.now()
          });
        }
        console.log(`[MissionExecutor] 📊 加载快速模型缓存: ${this.fastModelCache.size} 个模型`);
      }
    } catch (err) {
      console.warn('[MissionExecutor] 加载快速模型缓存失败:', err);
    }
  }

  /**
   * 记录快速模型
   */
  private async recordFastModel(modelId: string, ttfb: number): Promise<void> {
    if (ttfb < FAST_MODEL_THRESHOLD) {
      const existing = this.fastModelCache.get(modelId);
      const avgTtfb = existing ? (existing.avgTtfb + ttfb) / 2 : ttfb;

      this.fastModelCache.set(modelId, { avgTtfb, lastSuccess: Date.now() });

      // 写入 Redis 持久化
      try {
        await this.redis.hset(FAST_MODEL_KEY, {
          [modelId]: JSON.stringify({ avgTtfb, lastSuccess: Date.now() })
        });
        console.log(`[MissionExecutor] ⚡ 记录快速模型: ${modelId} (avg TTFB: ${avgTtfb}ms)`);
      } catch (err) {
        console.warn('[MissionExecutor] 记录快速模型失败:', err);
      }
    }
  }

  /**
   * 检查模型是否在黑名单中
   */
  private isModelBlacklisted(modelId: string): boolean {
    const blacklistUntil = this.slowModelBlacklist.get(modelId);
    if (blacklistUntil && Date.now() < blacklistUntil) {
      return true;
    }
    // 黑名单过期,移除
    if (blacklistUntil) {
      this.slowModelBlacklist.delete(modelId);
    }
    return false;
  }

  /**
   * 将慢模型加入临时黑名单
   */
  private blacklistSlowModel(modelId: string): void {
    this.slowModelBlacklist.set(modelId, Date.now() + SLOW_MODEL_BLACKLIST_MS);
    console.log(`[MissionExecutor] 🚫 暂时黑名单慢模型: ${modelId} (${SLOW_MODEL_BLACKLIST_MS / 1000}秒)`);
  }

  /**
   * ★★★ 核心方法:使命必达的模型调用 ★★★
   * 特性:
   * - 单次超时45秒,超时立即切换下一个模型
   * - 每个阶段最多尝试5个模型
   * - 记录快速模型,下次优先使用
   * - 黑名单慢模型2分钟
   */
  private async callNIMMissionCritical(
    taskId: string,
    phase: TaskPhase,
    messages: Array<{ role: string; content: string }>,
    maxTokens: number,
    systemPrompt?: string
  ): Promise<{ content: string; modelId: string; ttfb: number; attempts: number }> {
    const startTime = Date.now();
    let attempts = 0;
    let lastError: Error | null = null;

    this.dispatcher.setPhase(phase);

    // ★★★ 最多尝试 MAX_MODEL_ATTEMPTS 个不同模型 ★★★
    while (attempts < MAX_MODEL_ATTEMPTS) {
      attempts++;

      // 获取下一个可用模型(跳过黑名单)
      let modelId: string | null = null;
      let tryCount = 0;
      const maxTry = 10;

      while (!modelId && tryCount < maxTry) {
        const candidate = this.dispatcher.getNextModel();
        if (!candidate) {
          console.warn(`[MissionExecutor] 无可用模型,重置健康状态`);
          this.dispatcher.resetAllHealth();
          continue;
        }

        // 检查黑名单
        if (!this.isModelBlacklisted(candidate)) {
          modelId = candidate;
        } else {
          console.log(`[MissionExecutor] 跳过黑名单模型: ${candidate}`);
        }
        tryCount++;
      }

      if (!modelId) {
        // 所有模型都在黑名单,清空黑名单强制使用
        this.slowModelBlacklist.clear();
        modelId = this.dispatcher.getNextModel();
        if (!modelId) {
          throw new Error('所有模型池耗尽,无法继续');
        }
      }

      const apiKey = this.dispatcher.getApiKey();
      const meta = getModelMeta(modelId);

      console.log(`[MissionExecutor] 🎯 第 ${attempts} 次尝试: ${modelId} (${phase}阶段)`);
      await this.updateProgress(taskId, this.lastProgress, this.lastPhase,
        `${phase}阶段尝试 ${attempts}/${MAX_MODEL_ATTEMPTS}: ${modelId}`);

      // ★★★ 单次调用超时45秒 ★★★
      const callStartTime = Date.now();
      let heartbeatInterval: ReturnType<typeof setInterval> | null = null; // ★★★ 定义在外层,便于 catch 访问 ★★★

      try {
        // ★★★ 修复:创建 AbortController 用于 fetch 和看门狗 ★★★
        this.abortController = new AbortController();
        this.abortFlag = false;

        // ★★★ 关键修复:使用 AbortSignal.timeout 覆盖整个请求周期 ★★★
        // AbortSignal.timeout 会在 fetch + response.json() 整个过程中超时
        // 这是 Node.js 18+ 特性,比手动 setTimeout 更可靠
        const timeoutSignal = AbortSignal.timeout(MODEL_TIMEOUT_MS);

        // 心跳定时器(每10秒更新进度)
        heartbeatInterval = setInterval(async () => {
          if (this.abortFlag) {
            if (heartbeatInterval) clearInterval(heartbeatInterval);
            return;
          }
          const elapsed = Math.floor((Date.now() - callStartTime) / 1000);
          await this.updateProgress(taskId, this.lastProgress, this.lastPhase,
            `${phase}阶段: ${modelId} 响应中 (${elapsed}秒)`);
        }, 10000);

        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // ★★★ 任务 3: 模型感知适配 ★★★
        // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
        // 1. 获取有效模型元数据(合并静态 + 学习数据)
        const effectiveMeta = await getEffectiveModelMeta(this.redis, modelId);

        // 2. 构建原始消息数组
        const rawMessages: ChatMessage[] = systemPrompt
          ? [{ role: 'system', content: systemPrompt }, ...messages.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }))]
          : messages.map(m => ({ role: m.role as 'user' | 'assistant', content: m.content }));

        // 3. 使用适配器自动适配
        const adapted = adaptPayload(modelId, rawMessages, { maxTokens });

        // 4. 如果有适配行为,打印日志
        if (adapted.adapted) {
          console.log(`[MissionExecutor] 🔧 ${modelId} 适配: ${adapted.adaptations.join('; ')}`);
          await this.updateProgress(taskId, this.lastProgress, this.lastPhase,
            `${phase}阶段: ${modelId} 已适配 (${adapted.adaptations.length}项)`);
        }

        const finalMaxTokens = adapted.maxTokens;
        const finalMessages = adapted.messages;

        // ★★★ 任务 2: 使用跳板模式 ★★★
        // 不再直连 NVIDIA,而是通过日本服务器跳板
        console.log(`[MissionExecutor] 🚀 使用跳板: ${PROXY_ENDPOINT}`);

        // ★★★ 硬超时:Promise.race 覆盖 fetch + response.json() 整个生命周期 ★★★
        // AbortSignal.timeout 只控制连接/TTFB,不控制 token 生成速度
        // Promise.race 确保总时间不超过 MODEL_TIMEOUT_MS
        const fetchAndParse = async () => {
          const response = await fetch(PROXY_ENDPOINT, {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${PROXY_SECRET}`,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              model: modelId,
              messages: finalMessages,
              max_tokens: finalMaxTokens,
              stream: false
            }),
            signal: timeoutSignal
          });

          if (!response.ok) {
            const errText = await response.text();
            throw { status: response.status, body: errText, isHttpError: true };
          }

  return response.json() as Promise<{
            choices?: Array<{ message?: { content?: string; reasoning_content?: string } }>;
            _proxy_meta?: { ttfb: number };
          }>;
        };

        const hardTimeout = new Promise<never>((_, reject) => {
          setTimeout(() => reject(new Error('MODEL_HARD_TIMEOUT')), MODEL_TIMEOUT_MS);
        });

        const data = await Promise.race([fetchAndParse(), hardTimeout]);

        // ★★★ 跳板返回错误处理 + 任务 3: 错误嗅探与学习 ★★★
        // (HTTP 错误已在 fetchAndParse 内部抛出)

        const content = data.choices?.[0]?.message?.content || '';
        if (!content) {
          const rLen = data.choices?.[0]?.message?.reasoning_content?.length || 0;
          console.warn(`[MissionExecutor] ⚠️ ${modelId} content 为空! reasoning_content=${rLen} tokens, 可能 max_tokens 不足或缺少 system prompt`);
        }
        const ttfb = data._proxy_meta?.ttfb || (Date.now() - callStartTime);

        // ★★★ 清除心跳定时器 ★★★
        if (heartbeatInterval) clearInterval(heartbeatInterval);

        // ★★★ 成功!记录快速模型 ★★★
        await this.recordFastModel(modelId, ttfb);
        await this.dispatcher.reportSuccess(modelId, ttfb, content.length / 4, Date.now() - callStartTime);

        console.log(`[MissionExecutor] ✅ 成功: ${modelId} TTFB=${ttfb}ms 尝试次数=${attempts}`);

        return { content, modelId, ttfb, attempts };

      } catch (err: any) {
        if (heartbeatInterval) clearInterval(heartbeatInterval);
        const elapsed = Date.now() - callStartTime;

        // ★★★ 区分超时、HTTP 错误和其他错误 ★★★
        const isTimeout = err.message === 'MODEL_HARD_TIMEOUT'
          || err.name === 'AbortError'
          || err.name === 'TimeoutError'
          || elapsed >= MODEL_TIMEOUT_MS - 1000;

        if (isTimeout) {
          console.warn(`[MissionExecutor] ⏱️ ${modelId} 超时 (${elapsed}ms),加入黑名单,切换下一个`);
          this.blacklistSlowModel(modelId);
          await this.dispatcher.reportFailure(modelId, 504, 'Timeout');
          lastError = new Error(`模型 ${modelId} 超时 (${elapsed}ms)`);
        } else if (err.isHttpError) {
          // ★★★ HTTP 错误:嗅探学习 ★★★
          console.error(`[跳板报错] 模型: ${modelId}, 状态码: ${err.status}, 内容: ${err.body?.slice(0, 200)}`);
          const sniffResult = await sniffErrorAndLearn(this.redis, modelId, err.body || '');
          if (sniffResult.shouldRetry && attempts < MAX_MODEL_ATTEMPTS) {
            console.log(`[MissionExecutor] 🔄 ${modelId} 自愈重试 (已学习适配)`);
            attempts--;
            if (sniffResult.newParams) maxTokens = sniffResult.newParams.maxTokens;
            continue;
          }
          await this.dispatcher.reportFailure(modelId, err.status || 500, err.body?.slice(0, 100) || 'Unknown');
          lastError = new Error(`HTTP ${err.status}: ${(err.body || '').slice(0, 200)}`);
        } else {
          console.warn(`[MissionExecutor] ❌ ${modelId} 错误: ${err.message}`);
          await this.dispatcher.reportFailure(modelId, 500, err.message);
          lastError = err;
        }

        // 继续尝试下一个模型
        console.log(`[MissionExecutor] 🔄 切换到下一个模型...`);
      }
    }

    // 所有模型都失败
    throw new Error(sanitizeErrorMessage(
      `使命必达失败: ${phase}阶段尝试了 ${attempts} 个模型均超时/失败。最后错误: ${lastError?.message}`
    ));
  }

  /**
   * 执行慢任务完整流程
   */
  async execute(taskId: string, params: TaskParams): Promise<TaskResult> {
    const startTime = Date.now();
    const phases = {
      planning: { duration: 0, model: '', attempts: 0 },
      expansion: { duration: 0, model: '', sections: 0, attempts: 0 },
      review: { duration: 0, model: '', attempts: 0 }
    };
    let totalAttempts = 0;
    const fastModelsUsed: string[] = [];

    this.abortController = new AbortController();
    this.abortFlag = false;

    // ★ OPT-02: 兜底 — 当 prompt 为空但 draftContent 存在时,用 draftContent 作为 prompt
    if (!params.prompt && params.draftContent) {
      params.prompt = params.draftContent;
      console.log(`[TaskExecutor] ⚡ Prompt 为空,使用 draftContent 作为 fallback (${params.prompt.length} chars)`);
    }

    // ★★★ DRY_RUN 急速测试模式 ★★★
    if (params.prompt && params.prompt.includes('[DRY_RUN]')) {
      console.log('[MissionExecutor] 🚀 DRY_RUN 模式');
      await this.updateProgress(taskId, 50, 'writing', 'DRY_RUN 测试...');
      await new Promise(resolve => setTimeout(resolve, 3000));

      const fakeMarkdown = `# DRY_RUN 测试文档

任务 ID: ${taskId}
测试时间: ${new Date().toISOString()}
使命必达机制验证成功!`;

      await this.redis.set(`slow_task:content:${taskId}`, fakeMarkdown, 'EX', 86400 * 7);
      await this.redis.hset('slow_task:results', {
        [taskId]: JSON.stringify({
          taskId, status: 'completed', progress: 100, phase: 'completed',
          detail: 'DRY_RUN 完成', updatedAt: Date.now(),
          // downloadUrl 由 processor.ts 上传 COS 后设置
          qualityGrade: 'S', sectionsGenerated: 1, duration: 3000
        })
      });

      return {
        content: fakeMarkdown,
        outline: { title: 'DRY_RUN', sections: [{ title: 'Test', points: ['test'] }] },
        sectionsGenerated: 1, duration: 3000, phases,
        totalAttempts: 1, fastModelsUsed: ['dry_run'],
        title: 'DRY_RUN',
      };
    }

    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    // OpenGame 管线任务
    // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    if (params.taskType === 'opengame_generation') {
      return this.executeOpenGame(taskId, params);
    }

    try {
      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      // Phase 1: 结构规划
      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      await this.updateProgress(taskId, 5, 'planning', '开始规划大纲...');
      const planningStart = Date.now();

      let outline: TaskOutline;
      let llmTitle = ''; // ★ LLM 生成的文档标题

      // ★★★ 修复:draftContent 为空时,fallback 到 prompt(含 chatContext 中的 HTML artifact) ★★★
      const draftSource = params.draftContent || '';
      // 检测是否为草案模式:有 draftContent,或 prompt 包含 HTML/结构化内容
      const hasHtmlContent = params.prompt && (/<h[1-6][^>]*>/i.test(params.prompt) || /<artifact[\s>]/i.test(params.prompt) || /<div[\s>]/i.test(params.prompt));
      const isDraftMode = (draftSource.length > 0 && params.skipPhase1) || hasHtmlContent;
      const effectiveDraft = draftSource.length > 0 ? draftSource : (hasHtmlContent ? params.prompt : '');

      if (isDraftMode && effectiveDraft.length > 0) {
        // ★★★ 关键:先清洗 HTML,移除 style/script/head 内容 ★★★
        const cleanedContent = this.cleanHtmlContent(effectiveDraft);

        // 先尝试结构化提取
        outline = this.extractOutlineFromDraft(effectiveDraft);
        phases.planning.model = 'draft-anchor';
        phases.planning.attempts = 1;
        // ★ 诊断数据
        const draftDebug: Record<string, any> = {
          promptLen: params.prompt?.length || 0,
          draftSourceLen: draftSource.length,
          effectiveDraftLen: effectiveDraft.length,
          cleanedContentLen: cleanedContent.length,
          cleanedContentPreview: cleanedContent.slice(0, 200),
          hasHtmlContent,
          isDraftMode,
          extractedSections: outline.sections.length,
          extractedTitle: outline.title,
          firstSectionTitle: outline.sections[0]?.title || '',
          buildTag: 'v9-anti-slop-v2',
          llmOutlineAttempted: false,
          llmOutlineSuccess: false,
          llmTitleAttempted: false,
          llmTitleSuccess: false,
        };
        (phases.planning as any).draftDebug = draftDebug;

        // ★★★ 如果结构化提取只得到泛化 fallback,用 LLM 从实际内容生成大纲 ★★★
        const isGenericOutline = outline.sections.length === 1 && outline.sections[0].title === '主体内容';
        if (isGenericOutline) {
          console.log('[MissionExecutor] 结构化提取失败,使用 LLM 从实际内容生成大纲...');
          draftDebug.llmOutlineAttempted = true;
          try {
            const isPaid = params.tier && params.tier !== 'free';
            const contentForLLM = cleanedContent.slice(0, 4000);
            const outlinePrompt = `你是一位文档分析专家。请分析以下内容,提取核心主题并生成文档大纲。

内容:
${contentForLLM}

严格输出纯 JSON,禁止包含其他文字或代码块标记:
{
  "title": "文档标题(15字以内,精准有力,必须体现具体内容主题)",
  "sections": [
    { "title": "章节标题", "points": ["要点1", "要点2", "要点3"] }
  ]
}

要求:
- 章节数量:${isPaid ? '5-8' : '4-6'}
- 每章节 3-5 个要点
- 标题必须反映内容的具体主题,禁止使用"主体内容""概述"等泛化标题`;

            const outlineResult = await this.callNIMMissionCritical(
              taskId, 'planning',
              [{ role: 'user', content: outlinePrompt }],
              4000,
              '你是一位文档分析专家。只输出纯 JSON,不要任何多余文字。'
            );

            draftDebug.llmOutlineRaw = outlineResult.content.slice(0, 500);
            draftDebug.llmOutlineModel = outlineResult.modelId;

            // 安全解析 JSON
            const jsonStr = outlineResult.content.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
            let parsed: any = null;
            try { parsed = JSON.parse(jsonStr); } catch {
              const m = jsonStr.match(/\{[\s\S]*\}/);
              if (m) { try { parsed = JSON.parse(m[0]); } catch {} }
            }
            if (parsed && parsed.sections && parsed.sections.length > 0) {
              outline = {
                title: parsed.title || outline.title,
                sections: parsed.sections.map((s: any) => ({
                  title: s.title || '未命名章节',
                  points: Array.isArray(s.points) ? s.points : ['详细阐述'],
                })),
                metadata: { source: 'draft-llm' },
              };
              llmTitle = parsed.title || '';
              draftDebug.llmOutlineSuccess = true;
              draftDebug.llmSections = outline.sections.length;
              draftDebug.llmOutlineTitle = llmTitle;
              console.log('[MissionExecutor] LLM 大纲生成成功:', outline.title, '章节:', outline.sections.length);
            } else {
              draftDebug.llmOutlineParseError = 'JSON parsed but no sections found';
              console.warn('[MissionExecutor] LLM 大纲 JSON 解析成功但无 sections:', JSON.stringify(parsed)?.slice(0, 200));
            }
          } catch (outlineErr: any) {
            draftDebug.llmOutlineError = outlineErr.message?.slice(0, 200);
            console.warn('[MissionExecutor] LLM 大纲生成失败:', outlineErr.message);
          }
        }

        // ★★★ 标题生成:如果 LLM 大纲没有产出标题,调用专门的标题生成 ★★★
        if (!llmTitle) {
          draftDebug.llmTitleAttempted = true;
          try {
            const titleResult = await this.callNIMMissionCritical(
              taskId, 'planning',
              [{
                role: 'user',
                content: `请为以下文档内容生成一个精准有力的标题(15字以内,直接输出标题文字,不要引号或任何标记):\n\n${cleanedContent.slice(0, 1000)}`
              }],
              2000,
              '你是文档标题专家。只输出一个标题,15字以内,精准有力,不要任何多余文字。'
            );
            const rawTitle = titleResult.content.replace(/["'"「」【】\s\n\r]/g, '').slice(0, 15);
            if (rawTitle.length > 0) {
              llmTitle = rawTitle;
              draftDebug.llmTitleSuccess = true;
              draftDebug.llmTitleResult = llmTitle;
              draftDebug.llmTitleModel = titleResult.modelId;
              console.log('[MissionExecutor] LLM 生成标题(草案模式):', llmTitle);
            } else {
              draftDebug.llmTitleEmpty = true;
              console.warn('[MissionExecutor] LLM 标题返回空');
            }
          } catch (titleErr: any) {
            draftDebug.llmTitleError = titleErr.message?.slice(0, 200);
            console.warn('[MissionExecutor] 标题生成失败:', titleErr.message);
          }

          // ★★★ 最终兜底:从内容首行提取标题 ★★★
          if (!llmTitle) {
            const firstLine = cleanedContent.split(/[。\n]/).find(l => l.trim().length > 3) || '';
            llmTitle = firstLine.replace(/[^a-zA-Z0-9一-鿿·\-| ]/g, '').trim().slice(0, 15);
            draftDebug.heuristicTitle = llmTitle;
            console.log('[MissionExecutor] 启发式标题:', llmTitle);
          }
        }
      } else {
        const isPaid = params.tier && params.tier !== 'free';
        const planningSystemPrompt = isPaid
          ? `你是腾讯/网易级资深系统策划,正在为「大师全案」生成文档大纲。

【反 AI 废话协议】
- 禁止学术名词堆砌(沉没成本/损失厌恶等术语全篇最多出现1次)
- 禁止名人名言、煽情愿景
- 章节标题必须具体有力,禁止"概述""简介""背景"等虚词
- 每个要点必须是可执行的具体动作或可量化的数据指标

大纲格式(严格输出纯 JSON):
{
  "title": "文档标题(15字以内,体现具体主题,禁止'完整策划案''大师全案'等泛化词)",
  "subtitle": "副标题(20字以内,说明核心价值)",
  "sections": [
    { "title": "章节标题", "points": ["要点1", "要点2", "要点3", "要点4", "要点5"] }
  ]
}

结构要求:
- 章节数量:6-8 个,逻辑递进
- 每章节 4-6 个要点,每个要点必须具体到可执行
- 必须包含的模块(按顺序):
  1. 现状诊断(含具体数据指标和问题定位)
  2. 核心机制设计(含数据结构、接口定义、边界情况)
  3. 用户流程与交互(含客户端-服务端通信流程)
  4. 数值与经济系统(含数值表、平衡公式、付费转化节点)
  5. 风险与异常处理(含 TOP3 风险 + 具体规避方案)
  6. 验收标准(含可测量 KPI 和测试用例)
- 要点示例(必须达到此粒度):
  ✓ "用户数据表含 userId/giftId/status/purchasedAt 五个核心字段"
  ✓ "支付回调超时 30s 触发定时补偿任务,24h 内自动对账"
  ✓ "首充转化率目标 42%,基于沉没成本机制行业基准推导"
  ✗ "分析用户需求"(太抽象)
  ✗ "制定运营策略"(太泛化)`
          : `你是资深系统策划,正在为「灵感草案」生成文档大纲。

【反 AI 废话协议】禁止学术名词堆砌、名人名言、模糊词。章节标题必须具体。

大纲格式(严格输出纯 JSON):
{
  "title": "文档标题(10字以内,体现具体主题,禁止'灵感草案''草案'等泛化词)",
  "subtitle": "副标题(简短描述核心方向)",
  "sections": [
    { "title": "章节标题", "points": ["要点1", "要点2", "要点3"] }
  ]
}

结构要求:
- 章节数量:4-5 个,抓住核心
- 每章节 3-4 个要点,每个要点具体可执行
- 必须覆盖:核心机制 → 关键数据/数值 → 执行路径 → 风险与注意事项
- 要点必须有具体数字或可执行动作,禁止泛化罗列
- 标题示例:"独立游戏首充系统设计"(好) vs "变现策略分析"(差)`;

        const planningResult = await this.callNIMMissionCritical(
          taskId, 'planning',
          [{ role: 'user', content: params.prompt }],
          4000,
          planningSystemPrompt
        );

        // 解析大纲
        try {
          const jsonMatch = planningResult.content.match(/\{[\s\S]*\}/);
          outline = jsonMatch ? JSON.parse(jsonMatch[0]) : {
            title: '生成文档', sections: [
              { title: '概述', points: ['背景介绍', '核心概念'] },
              { title: '主体', points: ['详细分析', '关键要素'] },
              { title: '总结', points: ['核心结论', '建议方案'] }
            ]
          };
        } catch {
          outline = { title: '生成文档', sections: [{ title: '主体', points: ['内容生成'] }] };
        }

        // ★★★ 从 LLM 输出的 JSON 中提取 title ★★★
        llmTitle = outline.title || '';
        console.log('[MissionExecutor] LLM 生成标题(规划模式):', llmTitle);

        phases.planning.model = planningResult.modelId;
        phases.planning.attempts = planningResult.attempts;
        totalAttempts += planningResult.attempts;
        if (planningResult.ttfb < FAST_MODEL_THRESHOLD) {
          fastModelsUsed.push(planningResult.modelId);
        }
      }

      phases.planning.duration = Date.now() - planningStart;
      await this.updateProgress(taskId, 20, 'planning', '大纲生成完成');

      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      // Phase 2: 分章扩写
      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      const sections = outline.sections || [];
      const expansionStart = Date.now();
      const limit = pLimit(CONCURRENCY_LIMIT);
      const designTask = isDesignTask(params.taskType);

      console.log(`[MissionExecutor] Phase 2: 扩写 ${sections.length} 个章节${designTask ? ' (设计任务)' : ''}`);

      // ★★★ 设计任务:一次性完整生成(不拆章节) ★★★
      let allSectionContent: string[] = [];

      if (designTask) {
        const designPrompt = params.style
          ? getStyledPrompt(params.taskType!, params.style)
          : getDesignPrompt(params.taskType!);

        await this.updateProgress(taskId, 30, 'writing', '设计任务:一次性生成完整内容...');

        // ★★★ 在 user message 中强制重复输出格式要求 ★★★
        const formatHint = params.taskType === 'pitch_deck'
          ? `【关键】你必须使用 <slide title="..."> 标签输出,不要使用 <file> 标签。每个 slide 标签包含一个幻灯片。`
          : `【关键】你必须使用 <file name="..."> 标签输出。每个 file 标签包含一个文件。`;

        const designResult = await this.callNIMMissionCritical(
          taskId, 'expansion',
          [{ role: 'user', content: [
            `## 任务需求`,
            ``,
            params.prompt,
            ``,
            formatHint,
            ``,
            `请严格按照 system prompt 中的输出格式要求,生成完整的内容。`,
            `所有内容必须包含在单个输出中,不要分多次输出。`,
          ].join('\n') }],
          16000,
          designPrompt
        );

        phases.expansion.attempts = designResult.attempts;
        totalAttempts += designResult.attempts;
        if (designResult.ttfb < FAST_MODEL_THRESHOLD) {
          fastModelsUsed.push(designResult.modelId);
        }

        allSectionContent = [designResult.content];
        phases.expansion.sections = 1;

        await this.updateProgress(taskId, 80, 'writing', '设计内容生成完成');
      }

      // ★★★ 非设计任务:分章节扩写 ★★★
      const sectionTasks = designTask ? [] : sections.map((section, index) =>
        limit(async () => {
          if (this.abortFlag) return '';

          const startProgress = 20 + Math.floor(index / sections.length * 60);
          await this.updateProgress(taskId, startProgress, 'writing',
            `章节 ${index + 1}/${sections.length}: ${section.title}`);

          const isPaid = params.tier && params.tier !== 'free';
          const expansionSystemPrompt = isPaid
            ? `你是腾讯/网易级资深系统策划,正在撰写「大师全案」。请深度扩写以下章节。

═══════════════════════════════════════
【反 AI 虚空废话协议 — 违反即不合格】
═══════════════════════════════════════

1. 禁止在线打草稿:不得出现「错误!应为」「修正:」「更正:」「实际上应该是」等自我纠错痕迹。一次成型,不回头。
2. 禁止学术名词复读:沉没成本/损失厌恶/认知失调/心理锚点等术语,全章最多提及1次(1句解释即止),此后用「该机制」「此设计」指代。
3. 禁止名人名言:不得引用彼得·德鲁克、乔布斯等人名言,不得使用「我们的使命是」「最危险的不是」等煽情句式。
4. 禁止 QA 自问自答:不得使用「你可能会问」「答案是」等伪对话。
5. 禁止模糊词:「很多」「大量」「显著」「一定」「若干」全部替换为具体数字或百分比。
6. 禁止泛化罗列:不得出现「首先...其次...最后...」八股文结构,不得用「概述」「简介」等虚词作章节标题。

═══════════════════════════════════════
【强制输出 — 涉及系统/功能/机制设计时必须包含】
═══════════════════════════════════════

A. 数据结构表 — 每个核心系统必须给出字段定义:
| 字段名 | 类型 | 说明 | 示例值 |

B. 客户端-服务端流程 — 涉及网络请求时必须给出:
请求: POST /api/xxx → 请求体: {} → 响应: {} → 错误码: 列表

C. 边界情况表 — 每个系统必须列出 3-5 个异常场景:
| 场景 | 触发条件 | 处理方式 |

D. 核心循环 — 用箭头链描述主流程:
步骤A → 步骤B → [条件判断] → 步骤C

═══════════════════════════════════════
输出格式标签(按场景选用,禁止使用未定义标签)
═══════════════════════════════════════

1. 高管摘要(开篇必须使用,结论先行):
<exec_summary>2-3句话概括本章最核心的结论或建议</exec_summary>

2. 数据网格(核心数据/财务指标,最多4个):
<metric_grid>指标 → 数值 | 指标 → 数值 | 指标 → 数值</metric_grid>

3. KPI指标横排(每章最多1次):
<kpi_row>指标 → 数值 | 指标 → 数值</kpi_row>

4. 核心结论框(每章最多1次):
<highlight_box title="核心结论">明确判断 + 数据依据</highlight_box>

5. 操作步骤(执行类章节):
<step_box>1. 步骤名 | 具体操作 + 预期结果 + 时间节点</step_box>

6. 阶段进度:
<phase_bar>第一阶段:xxx | 第二阶段:xxx | 第三阶段:xxx</phase_bar>

7. 左右对比:
<comparison_box left_title="方案A" right_title="方案B">左侧内容 | 右侧内容</comparison_box>

8. 话术演示:
<script_box scene="场景">对话脚本</script_box>

9. 提示框:
<tip_box title="执行建议">具体建议 + 时间 + 责任角色</tip_box>

10. 警告框:
<warn_box>风险描述 + 规避方案</warn_box>

11. 检查清单:
<checklist title="交付标准">[x] 已完成 [ ] 待完成</checklist>

12. 大数字突出:
<number_card label="指标名" value="数值" note="说明">

13. 数据表格(Markdown):
| 维度 | 现状 | 目标 | 差距 |

═══════════════════════════════════════
内容质量标准
═══════════════════════════════════════

✓ 每章 800-1500 字,必须包含至少 1 个数据表格 + 1 个流程描述
✓ 所有数字必须有单位和来源,禁止「显著提升」「一定效果」
✓ 竞品案例必须具名(《原神》《王者荣耀》等),不用「某游戏」「某公司」
✓ 执行建议必须有时间节点(「第1周」「上线后3天」)和责任主体
✓ 涉及系统设计时,必须给出数据结构、接口定义、边界情况
✓ 必须使用 3-5 种不同格式标签

风格:${params.style || '专业严谨,数据驱动,直击要害,像腾讯/网易内部策划案'}`
            : `你是资深系统策划,正在撰写「灵感草案」。帮用户快速看清核心问题和可行方向。

═══════════════════════════════════════
【反 AI 虚空废话协议 — 违反即不合格】
═══════════════════════════════════════

1. 禁止自我纠错:不得出现「错误」「修正」「更正」等痕迹。一次成型。
2. 禁止学术名词复读:术语全章最多1次,此后用「该机制」指代。
3. 禁止名人名言和煽情句式。
4. 禁止 QA 自问自答。
5. 禁止模糊词:用具体数字替代「很多」「大量」「显著」。

═══════════════════════════════════════
输出格式标签(精简版,按需选用)
═══════════════════════════════════════

1. 高管摘要(开篇使用):<exec_summary>1-2句核心结论</exec_summary>
2. 核心数据:<metric_grid>指标 → 数值 | 指标 → 数值</metric_grid>
3. 核心结论(每章最多1个):<highlight_box title="核心洞察">关键判断</highlight_box>
4. 注意事项:<warn_box>风险 + 规避方案</warn_box>
5. 行动建议:<step_box>1. 步骤 | 说明</step_box>
6. 对比框:<comparison_box left_title="A" right_title="B">左 | 右</comparison_box>
7. 检查清单:<checklist title="行动">[ ] 待完成</checklist>

═══════════════════════════════════════
内容质量要求
═══════════════════════════════════════

✓ 每章 400-800 字,精炼不啰嗦
✓ 每个要点 2-3 句话说清楚
✓ 至少用 2 种格式标签
✓ 涉及系统设计时给出核心数据结构和边界情况
✓ 给方向的同时给出关键数字(转化率、成本、时间估算)

风格:${params.style || '简洁直接,专业务实,像资深同事给的建议'}`;

          // ★★★ 构建扩写用户消息:有草案时作为背景参考 ★★★
          const draftReference = params.draftContent
            ? `\n## 背景参考(草案素材)\n\n⚠️ 以下内容仅作为背景参考和结构依据,不得照搬原文。你的任务是在此基础上重新创作一份专业全案,深度、数据、案例均需大幅扩充。\n\n${params.draftContent.slice(0, 500)}\n`
            : '';

          const expansionResult = await this.callNIMMissionCritical(
            taskId, 'expansion',
            [{ role: 'user', content: [
              `## 章节任务`,
              ``,
              `章节标题:**${section.title}**`,
              ``,
              `核心要点(需要深度覆盖):`,
              section.points.map((p, i) => `${i + 1}. ${p}`).join('\n'),
              ``,
              `原始用户需求(保持相关性):`,
              params.prompt.slice(0, 300),
              draftReference,
              `要求:`,
              isPaid
                ? `- 深度扩写,字数 800-1500字\n- 每个要点都要有数据支撑和具体案例\n- 必须使用至少 3 种格式标签\n- 禁止模糊表达,全部量化`
                : `- 精简扩写,字数 400-800字\n- 每个要点 2-3 句话,激发思路\n- 使用 2-3 种格式标签\n- 简洁直接`,
            ].join('\n') }],
            isPaid ? 4000 : 2000,
            expansionSystemPrompt
          );

          const endProgress = 20 + Math.floor((index + 1) / sections.length * 60);
          await this.updateProgress(taskId, endProgress, 'writing',
            `章节 ${index + 1}/${sections.length}: ${section.title} (完成)`);

          phases.expansion.attempts += expansionResult.attempts;
          totalAttempts += expansionResult.attempts;
          if (expansionResult.ttfb < FAST_MODEL_THRESHOLD) {
            fastModelsUsed.push(expansionResult.modelId);
          }

          return expansionResult.content;
        })
      );

      const sectionResults = designTask ? allSectionContent : await Promise.all(sectionTasks);
      const validSections = sectionResults.filter(r => r.length > 0);

      phases.expansion.duration = Date.now() - expansionStart;
      phases.expansion.sections = validSections.length;
      phases.expansion.model = this.dispatcher.getNextModel() || 'multi';

      if (!designTask) {
        await this.updateProgress(taskId, 80, 'writing', `生成 ${validSections.length} 个章节`);
      }

      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      // Phase 3: 质量自检(可选,设计任务跳过)
      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      const fullContent = validSections.join('\n\n---\n\n');
      const REVIEW_SKIP_THRESHOLD = 10000;

      // ★★★ 设计任务跳过 review(XML 标签会被 review 破坏)★★★
      if (designTask) {
        console.log(`[MissionExecutor] ⚠️ 设计任务,跳过 Review`);
        phases.review.model = 'skipped-design-task';
        phases.review.attempts = 0;
      } else if (fullContent.length > REVIEW_SKIP_THRESHOLD) {
        console.log(`[MissionExecutor] ⚠️ 内容过长(${fullContent.length}),跳过 Review`);
        phases.review.model = 'skipped';
        phases.review.attempts = 0;
      } else {
        await this.updateProgress(taskId, 85, 'reviewing', '开始质量审查...');
        const reviewStart = Date.now();

        try {
          // ★★★ 修复:裁剪content到3000字符,maxTokens降到512 ★★★
          // 模型maxContext=4096,需要给prompt留足够空间
          const reviewContent = fullContent.length > 3000
            ? fullContent.slice(0, 3000) + '\n...[内容已裁剪,仅审查前半部分]'
            : fullContent;

          const reviewResult = await this.callNIMMissionCritical(
            taskId, 'review',
            [{ role: 'user', content: reviewContent }],
            1000,
            `你是腾讯/网易级文档质量审核编辑。执行以下审查:

【反 AI 废话审查 — 发现即删除/修正】
1. 删除所有自我纠错痕迹(「错误!应为」「修正:」「更正:」「实际上应该是」)
2. 删除名人名言和煽情愿景句式(「我们的使命是」「最危险的不是」)
3. 删除 QA 自问自答(「你可能会问」「答案是」)
4. 学术名词(沉没成本/损失厌恶/认知失调等)若出现超过1次,保留首次,删除后续重复
5. 模糊词(「很多」「大量」「显著」「一定」)替换为具体数字或删除

【内容质量审查】
6. 修正逻辑矛盾或前后不一致的表述
7. 补充遗漏的关键数据占位符(格式:[待补充:XXX数据])
8. 优化过于口语化的表达为专业文档风格

【格式保护】
9. 严禁删除或修改 <tag> 标签(exec_summary, metric_grid, kpi_row, highlight_box, info_box, step_box, phase_bar, comparison_box, script_box, warn_box, tip_box, checklist, number_card)
10. 严禁修改数据表格的 | 格式
11. 输出修正后的完整内容,不加任何说明文字`
          );

          phases.review.model = reviewResult.modelId;
          phases.review.attempts = reviewResult.attempts;
          phases.review.duration = Date.now() - reviewStart;
          totalAttempts += reviewResult.attempts;

          // ★★★ Review结果仅替换审查部分 ★★★
          const reviewedContent = reviewResult.content.length > reviewContent.length * 0.5
            ? reviewResult.content + (fullContent.length > 3000 ? '\n\n' + fullContent.slice(3000) : '')
            : fullContent;

          await this.redis.set(`slow_task:content:${taskId}`, reviewedContent, 'EX', 86400 * 7);
          await this.updateProgress(taskId, 95, 'reviewing', '审查完成');

        } catch (reviewErr) {
          console.warn(`[MissionExecutor] Review 失败,使用原文:`, reviewErr);
          phases.review.model = 'fallback';
          phases.review.attempts = 0;
          await this.redis.set(`slow_task:content:${taskId}`, fullContent, 'EX', 86400 * 7);
        }
      }

      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      // 完成!写入最终状态
      // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      const finalContent = await this.redis.get(`slow_task:content:${taskId}`) || fullContent;

      await this.redis.hset('slow_task:results', {
        [taskId]: JSON.stringify({
          taskId, status: 'completed', progress: 100, phase: 'completed',
          detail: `任务完成 (尝试 ${totalAttempts} 次,快速模型: ${fastModelsUsed.length})`,
          updatedAt: Date.now(),
          // downloadUrl 由 processor.ts 上传 COS 后设置(真实 URL)
          qualityGrade: totalAttempts <= 2 ? 'S' : totalAttempts <= 4 ? 'A' : 'B',
          sectionsGenerated: validSections.length,
          duration: Date.now() - startTime,
          phases, totalAttempts, fastModelsUsed
        })
      });

      console.log(`[MissionExecutor] ✅ 任务完成: ${taskId}, 总尝试: ${totalAttempts}, 快速模型: ${fastModelsUsed.join(',')}`);

      // ★★★ 设计任务附加输出元数据 ★★★
      const designMeta = designTask ? getDesignTaskOutputMeta(params.taskType as DesignTaskType) : {};

      return {
        content: typeof finalContent === 'string' ? finalContent : JSON.stringify(finalContent),
        outline, sectionsGenerated: validSections.length,
        duration: Date.now() - startTime, phases, totalAttempts, fastModelsUsed,
        title: llmTitle || outline.title || '', // ★ 优先使用 LLM 生成的标题
        ...designMeta,
      };

    } catch (err: any) {
      const safeMessage = sanitizeErrorMessage(err?.message);
      console.error(`[MissionExecutor] ❌ 任务失败: ${err.message}`);
      await this.updateProgress(taskId, 0, 'failed', safeMessage);
      throw err;
    }
  }

  /**
   * 更新进度
   */
  private async updateProgress(taskId: string, progress: number, phase: string, detail?: string): Promise<void> {
    this.lastProgress = progress;
    this.lastPhase = phase;
    this.lastDetail = detail || '';

    const status = progress === 100 ? 'completed' : progress === 0 ? 'failed' : 'writing';

    // ★★★ 合并现有状态,避免覆盖 processor.ts 设置的 downloadUrl 等字段 ★★★
    try {
      const existing = await this.redis.hget('slow_task:results', taskId);
      const prevState = existing ? (typeof existing === 'string' ? JSON.parse(existing) : existing) : {};

      const report: ProgressReport = {
        ...prevState,
        taskId,
        status,
        progress, phase, detail: detail || '', updatedAt: Date.now()
      };

      await this.redis.hset('slow_task:results', { [taskId]: JSON.stringify(report) });

      if (this.progressCallback) {
        this.progressCallback(report);
      }
    } catch {
      // fallback: 直接写入(不合并)
      const report: ProgressReport = {
        taskId,
        status,
        progress, phase, detail: detail || '', updatedAt: Date.now()
      };
      await this.redis.hset('slow_task:results', { [taskId]: JSON.stringify(report) });
      if (this.progressCallback) {
        this.progressCallback(report);
      }
    }
  }

  /**
   * 注册进度回调
   */
  onProgress(callback: (report: ProgressReport) => void): void {
    this.progressCallback = callback;
  }

  /**
   * 中止任务
   */
  abort(): void {
    this.abortFlag = true;
    if (this.abortController) {
      this.abortController.abort();
      console.warn('[MissionExecutor] 🛑 AbortController.abort() 已调用');
    }
  }

  isAborted(): boolean {
    return this.abortFlag;
  }

  reset(): void {
    this.abortFlag = false;
    this.progressCallback = null;
    this.slowModelBlacklist.clear();
  }

  /**
   * 执行 OpenGame 管线任务
   */
  private async executeOpenGame(taskId: string, params: TaskParams): Promise<TaskResult> {
    const startTime = Date.now();
    const outputDir = `/tmp/opengame/${taskId}`;

    try {
      const result = await runOpenGamePipeline({
        prompt: params.prompt,
        style: typeof params.style === 'string' ? params.style : params.style?.name,
        complexity: 'medium',
        tier: params.tier as 'free' | 'pro' | 'enterprise' | undefined,
        outputDir,
        onProgress: (report) => {
          this.updateProgress(taskId, report.progress, report.stage, report.detail).catch(() => {});
        },
      });

      const duration = Date.now() - startTime;
      const content = result.code.files.map(f => `// === ${f.path} ===\n${f.content}`).join('\n\n');

      // 写入 Redis
      await this.redis.set(`slow_task:content:${taskId}`, content, 'EX', 86400 * 7);
      await this.redis.hset('slow_task:results', {
        [taskId]: JSON.stringify({
          taskId,
          status: 'completed',
          progress: 100,
          phase: 'completed',
          detail: `OpenGame 完成: ${result.filesWritten.length} 个文件, 验证 ${result.verification.passed ? '通过' : '未通过'}`,
          updatedAt: Date.now(),
          qualityGrade: result.verification.passed ? 'A' : 'B',
        }),
      });

      return {
        content,
        outline: {
          title: result.gdd.metadata.title,
          sections: result.gdd.entities.map(e => ({ title: e.name, points: e.behaviors })),
        },
        sectionsGenerated: result.code.files.length,
        duration,
        phases: {
          planning: { duration: 0, model: 'pipeline', attempts: 1 },
          expansion: { duration: 0, model: 'pipeline', sections: result.code.files.length, attempts: 1 },
          review: { duration: 0, model: 'pipeline', attempts: result.verification.rounds },
        },
        totalAttempts: result.verification.rounds,
        fastModelsUsed: ['opengame-pipeline'],
        outputType: 'zip',
        fileExtension: 'zip',
        contentType: 'application/zip',
      };
    } catch (err: any) {
      const safeMessage = sanitizeErrorMessage(err?.message);
      await this.updateProgress(taskId, 0, 'failed', safeMessage);
      throw err;
    }
  }

  /**
   * 清洗 HTML 内容:移除 style/script/head 标签及其内容,再剥离剩余标签
   */
  private cleanHtmlContent(html: string): string {
    return html
      .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
      .replace(/<head[^>]*>[\s\S]*<\/head>/gi, '')
      .replace(/<!--[\s\S]*?-->/g, '')
      .replace(/<[^>]*>/g, ' ')
      .replace(/&[a-z]+;/gi, ' ')
      .replace(/\s+/g, ' ')
      .trim();
  }

  /**
   * 从草案提取大纲
   */
  private extractOutlineFromDraft(draft: string): TaskOutline {
    const sections: Array<{ title: string; points: string[] }> = [];

    // ★★★ 关键修复:先移除 style/script/head 标签内容,再提取标题 ★★★
    const cleanedForStructure = draft
      .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
      .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
      .replace(/<head[^>]*>[\s\S]*<\/head>/gi, '')
      .replace(/<!--[\s\S]*?-->/g, '');

    const mdHeadingRegex = /^##\s+(.+)$/gm;
    let mdMatch;
    while ((mdMatch = mdHeadingRegex.exec(cleanedForStructure)) !== null) {
      sections.push({ title: mdMatch[1].trim(), points: ['延续草案风格'] });
    }

    if (sections.length === 0) {
      const htmlHeadingRegex = /<h2[^>]*>([\s\S]*?)<\/h2>/gi;
      let htmlMatch;
      while ((htmlMatch = htmlHeadingRegex.exec(cleanedForStructure)) !== null) {
        sections.push({ title: htmlMatch[1].replace(/<[^>]*>/g, '').trim(), points: ['延续草案风格'] });
      }
    }

    if (sections.length === 0) {
      sections.push({ title: '主体内容', points: ['详细阐述'] });
    }

    const titleMatch = cleanedForStructure.match(/^#\s+(.+)$/m) || cleanedForStructure.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
    const docTitle = titleMatch ? titleMatch[1].replace(/<[^>]*>/g, '').trim() : '策划方案';

    return { title: docTitle, sections, metadata: { source: 'draft' } };
  }
}

export function createTaskExecutor(dispatcher: ModelDispatcher, redis: Redis): TaskExecutor {
  return new TaskExecutor(dispatcher, redis);
}