@tfdesign/b-end 1.0.14 → 1.0.15

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.
@@ -10,6 +10,7 @@ import ChatMessage, {
10
10
  DEFAULT_CHAT_TASK_GROUPS,
11
11
  DEFAULT_CHAT_PLAN,
12
12
  DEFAULT_CHAT_THINKING,
13
+ DEFAULT_CHAT_FORM_CONFIRM,
13
14
  useStreamingTaskGroups,
14
15
  } from '../components/ChatMessage';
15
16
 
@@ -41,64 +42,20 @@ import ChatMessage, {
41
42
  */
42
43
 
43
44
  const STREAM_INTERVAL = 600;
45
+ const AUTO_FLOW_TIMINGS = {
46
+ thinkingDone: 900,
47
+ confirmShow: 1400,
48
+ confirmDone: 2400,
49
+ flowStart: 3000,
50
+ };
44
51
 
45
- /* ── 端到端 mock 对话 ── */
46
- function buildInitMessages() {
47
- return [
48
- /* m1 用户:富文本 + tag + 附件 */
49
- {
50
- id: 'm1',
51
- kind: 'user',
52
- timestamp: '14:02',
53
- userContent: [
54
- { type: 'text', value: '帮我整理 ' },
55
- {
56
- type: 'entity',
57
- icon: 'message-chat-square-stroked',
58
- label: '智能会话:社交私信',
59
- showChevron: true,
60
- },
61
- { type: 'text', value: ' 场景近 7 天的人工解决率,做一份分析报告' },
62
- ],
63
- userAttachments: [
64
- { id: 'att-1', name: '抖音电商售后政策汇编.pdf', size: 327680 },
65
- ],
66
- },
67
-
68
- /* m2 AI(合并:深度思考 + 引导文本 + 任务规划卡,已被用户处理 → 禁用置灰态) */
69
- {
70
- id: 'm2',
71
- kind: 'ai-task-plan',
72
- timestamp: '14:02',
73
- thinking: {
74
- ...DEFAULT_CHAT_THINKING,
75
- state: 'completed',
76
- defaultExpanded: false,
77
- },
78
- leadText: '好的,我先做一份任务规划,请稍后...',
79
- planConfirmed: true,
80
- },
81
-
82
- /* m3 用户:开始执行任务 */
83
- {
84
- id: 'm3',
85
- kind: 'user',
86
- timestamp: '14:03',
87
- userContent: [{ type: 'text', value: '开始执行任务' }],
88
- },
89
-
90
- /* m4 AI:完整执行流消息(执行流 + 总结文本 + 产物组 + 追问) */
91
- {
92
- id: 'm4',
93
- kind: 'ai-flow',
94
- timestamp: '14:08',
95
- taskGroups: DEFAULT_CHAT_TASK_GROUPS.map((g) => ({ ...g, status: 'completed' })),
96
- resultText: DEFAULT_CHAT_RESULT,
97
- resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS,
98
- followUps: DEFAULT_CHAT_FOLLOW_UPS,
99
- },
100
- ];
101
- }
52
+ const DEFAULT_CONVERSATION_CONTEXT = {
53
+ account: '抖音电商客服账号 · after_sales_prod',
54
+ environment: '生产环境 / OLA Workflow v13',
55
+ mock: '近 7 天售后会话样本 240 条',
56
+ conclusion: '上一轮已完成售后政策口径梳理,并沉淀可继续追问的结论摘要。',
57
+ artifacts: DEFAULT_CHAT_RESULT_ARTIFACTS.map((item) => item.title),
58
+ };
102
59
 
103
60
  /* ── 关键词 → 回复路由 ── */
104
61
  const TASK_KEYWORDS = ['整理', '分析', '生成', '梳理', '输出', '汇总'];
@@ -194,6 +151,108 @@ function routeReply(text) {
194
151
  return { type: 'short', text: GENERIC_REPLIES[Math.floor(Math.random() * GENERIC_REPLIES.length)] };
195
152
  }
196
153
 
154
+ function buildThinkingContent(text, context) {
155
+ const artifacts = context.artifacts?.length ? context.artifacts.join('、') : '暂无历史产物';
156
+ return [
157
+ `用户本轮输入:${text}`,
158
+ `系统自动带入上一轮账号:${context.account}`,
159
+ `系统自动带入上一轮环境:${context.environment}`,
160
+ `系统自动带入上一轮 Mock:${context.mock}`,
161
+ `系统自动带入上一轮结论:${context.conclusion || '暂无历史结论'}`,
162
+ `系统自动带入上一轮产物:${artifacts}`,
163
+ '基于上述连续会话上下文,先确认关键执行口径,再进入自动执行流。',
164
+ ].join('\n');
165
+ }
166
+
167
+ function buildClarificationConfirms(context) {
168
+ return DEFAULT_CHAT_FORM_CONFIRM.map((confirm) => ({
169
+ ...confirm,
170
+ id: 'auto-clarify-confirm',
171
+ title: '人工澄清确认',
172
+ primaryActionLabel: '确认并继续',
173
+ secondaryActionLabel: '',
174
+ formItems: [
175
+ {
176
+ id: 'scene',
177
+ label: '业务场景',
178
+ type: 'select',
179
+ placeholder: '请选择业务场景',
180
+ defaultValue: 'after-sales-policy',
181
+ options: [
182
+ { value: 'after-sales-policy', label: '售后政策整理' },
183
+ { value: 'conversation-analysis', label: '会话场景分析' },
184
+ { value: 'ab-experiment', label: 'AB 实验方案' },
185
+ ],
186
+ fullWidth: true,
187
+ },
188
+ {
189
+ id: 'channel',
190
+ label: '处理渠道',
191
+ type: 'select',
192
+ placeholder: '请选择处理渠道',
193
+ defaultValue: 'current-context',
194
+ options: [
195
+ {
196
+ value: 'current-context',
197
+ label: `使用当前账号、${context.environment} 与 ${context.mock}`,
198
+ },
199
+ { value: 'refresh-mock', label: '刷新 Mock 数据后再执行' },
200
+ { value: 'manual-review', label: '先转人工复核上下文再执行' },
201
+ ],
202
+ fullWidth: true,
203
+ },
204
+ {
205
+ id: 'remark',
206
+ label: '补充说明',
207
+ type: 'input',
208
+ placeholder: '请输入补充说明(可选)',
209
+ fullWidth: true,
210
+ },
211
+ ],
212
+ }));
213
+ }
214
+
215
+ function buildAutoFlowPayload(text, reply, context) {
216
+ if (reply.type === 'follow-up-result') {
217
+ return {
218
+ resultText: `${reply.resultText}\n\n本轮追问已自动继承上一轮账号、环境、Mock、结论和产物上下文,未新开会话。`,
219
+ resultArtifacts: reply.resultArtifacts,
220
+ followUps: reply.followUps,
221
+ };
222
+ }
223
+
224
+ if (reply.type === 'task') {
225
+ return {
226
+ resultText: DEFAULT_CHAT_RESULT,
227
+ resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS,
228
+ followUps: DEFAULT_CHAT_FOLLOW_UPS,
229
+ };
230
+ }
231
+
232
+ return {
233
+ resultText:
234
+ `${reply.text} 已结合当前会话上下文继续处理「${text}」:账号为 ${context.account},环境为 ${context.environment},Mock 使用 ${context.mock}。如需深入推进,可继续选择下方追问或直接在底部输入补充要求。`,
235
+ resultArtifacts: DEFAULT_CHAT_RESULT_ARTIFACTS.slice(0, 2),
236
+ followUps: ['继续细化结论', '补充一轮 Mock 验证', '生成可交付文档'],
237
+ };
238
+ }
239
+
240
+ function getFlowTotalMs() {
241
+ const totalSteps = DEFAULT_CHAT_TASK_GROUPS.reduce(
242
+ (sum, group) => sum + (group.steps?.length ?? 0),
243
+ 0,
244
+ );
245
+ return (totalSteps + 1) * STREAM_INTERVAL;
246
+ }
247
+
248
+ function hasActionablePrimaryButton(label) {
249
+ return typeof label === 'string' && label.trim().length > 0;
250
+ }
251
+
252
+ function confirmsRequireHumanGate(confirms) {
253
+ return Array.isArray(confirms) && confirms.some((confirm) => hasActionablePrimaryButton(confirm?.primaryActionLabel));
254
+ }
255
+
197
256
  /* ── 单条消息 → ChatMessage props 映射 ──
198
257
  * isLatest:是否为列表中最后一条(决定 historyMode 是否启用)
199
258
  * handlers:所有交互回调集合
@@ -202,7 +261,15 @@ function routeReply(text) {
202
261
  * · onFollowUpSelect(text) — 追问 chip 点击(默认作为新一轮对话发出)
203
262
  */
204
263
  function messageToChatProps(msg, isLatest, handlers = {}) {
205
- const { onPlanConfirm, onPlanCancel, onFollowUpSelect } = handlers;
264
+ const {
265
+ onPlanConfirm,
266
+ onPlanCancel,
267
+ onFollowUpSelect,
268
+ onConfirmPrimary,
269
+ onConfirmSecondary,
270
+ onConfirmOptionChange,
271
+ onConfirmFormChange,
272
+ } = handlers;
206
273
  const baseProps = {
207
274
  className: msg.className || '',
208
275
  };
@@ -262,6 +329,41 @@ function messageToChatProps(msg, isLatest, handlers = {}) {
262
329
  thinking: msg.thinking,
263
330
  resultText: msg.resultText,
264
331
  timestamp: msg.timestamp,
332
+ actions: msg.thinking?.state === 'thinking' ? null : baseActions,
333
+ };
334
+ }
335
+
336
+ if (msg.kind === 'ai-confirm') {
337
+ return {
338
+ ...baseProps,
339
+ role: 'ai',
340
+ header: true,
341
+ title: '',
342
+ steps: null,
343
+ leadText: msg.leadText,
344
+ confirms: Array.isArray(msg.confirms)
345
+ ? msg.confirms.map((confirm) => ({
346
+ ...confirm,
347
+ onOptionChange: (value, option) => onConfirmOptionChange && onConfirmOptionChange(
348
+ msg.id,
349
+ confirm.id,
350
+ value,
351
+ option,
352
+ ),
353
+ onFormChange: (formValues, meta) => onConfirmFormChange && onConfirmFormChange(
354
+ msg.id,
355
+ confirm.id,
356
+ formValues,
357
+ meta,
358
+ ),
359
+ onPrimaryAction: (payload) => onConfirmPrimary && onConfirmPrimary(msg.id, confirm.id, {
360
+ ...payload,
361
+ primaryActionLabel: confirm.primaryActionLabel,
362
+ }),
363
+ onSecondaryAction: () => onConfirmSecondary && onConfirmSecondary(msg.id, confirm.id),
364
+ }))
365
+ : msg.confirms,
366
+ timestamp: msg.timestamp,
265
367
  actions: baseActions,
266
368
  };
267
369
  }
@@ -329,7 +431,7 @@ export default function ChatConversationPattern({
329
431
  initialMessages,
330
432
  }) {
331
433
  const resolvedInitialMessagesRef = useRef(
332
- Array.isArray(initialMessages) ? initialMessages : buildInitMessages(),
434
+ Array.isArray(initialMessages) ? initialMessages : [],
333
435
  );
334
436
  const [messages, setMessages] = useState(() => resolvedInitialMessagesRef.current);
335
437
  const [phase, setPhase] = useState(() => (
@@ -346,10 +448,62 @@ export default function ChatConversationPattern({
346
448
  const [prefillSeed, setPrefillSeed] = useState(0);
347
449
  const replyTimerRef = useRef(null);
348
450
  const busyTimerRef = useRef(null);
451
+ const flowTimersRef = useRef([]);
452
+ const pendingFlowResumeRef = useRef(null);
349
453
  const scrollRef = useRef(null);
454
+ const latestAnchorRef = useRef(null);
455
+ const conversationContextRef = useRef({ ...DEFAULT_CONVERSATION_CONTEXT });
350
456
  const idSeedRef = useRef(100);
351
457
  const nextId = useCallback((prefix = 'm') => `${prefix}${++idSeedRef.current}`, []);
352
458
 
459
+ const scrollToLatest = useCallback((behavior = 'smooth') => {
460
+ if (latestAnchorRef.current?.scrollIntoView) {
461
+ latestAnchorRef.current.scrollIntoView({ block: 'end', behavior });
462
+ return;
463
+ }
464
+ const el = scrollRef.current;
465
+ if (el) el.scrollTo({ top: el.scrollHeight, behavior });
466
+ }, []);
467
+
468
+ const scheduleScrollToLatest = useCallback((behavior = 'smooth') => {
469
+ if (typeof window === 'undefined') return;
470
+ window.requestAnimationFrame(() => scrollToLatest(behavior));
471
+ }, [scrollToLatest]);
472
+
473
+ const clearFlowTimers = useCallback(() => {
474
+ flowTimersRef.current.forEach((timer) => clearTimeout(timer));
475
+ flowTimersRef.current = [];
476
+ }, []);
477
+
478
+ const clearPendingFlowResume = useCallback(() => {
479
+ pendingFlowResumeRef.current = null;
480
+ }, []);
481
+
482
+ const pauseForHumanGate = useCallback((resume) => {
483
+ pendingFlowResumeRef.current = resume;
484
+ setInputView('default');
485
+ scheduleScrollToLatest();
486
+ }, [scheduleScrollToLatest]);
487
+
488
+ const scheduleFlowTimer = useCallback((callback, delay) => {
489
+ const timer = setTimeout(() => {
490
+ flowTimersRef.current = flowTimersRef.current.filter((item) => item !== timer);
491
+ callback();
492
+ scheduleScrollToLatest();
493
+ }, delay);
494
+ flowTimersRef.current.push(timer);
495
+ return timer;
496
+ }, [scheduleScrollToLatest]);
497
+
498
+ const resumePendingFlow = useCallback((delay = 240) => {
499
+ const resume = pendingFlowResumeRef.current;
500
+ pendingFlowResumeRef.current = null;
501
+ if (typeof resume !== 'function') return;
502
+ scheduleFlowTimer(() => {
503
+ resume();
504
+ }, delay);
505
+ }, [scheduleFlowTimer]);
506
+
353
507
  /* chip 回填到输入框(覆盖当前草稿 + 自动聚焦) */
354
508
  const prefillInput = useCallback((text) => {
355
509
  setPrefillText(text || '');
@@ -361,8 +515,10 @@ export default function ChatConversationPattern({
361
515
  () => () => {
362
516
  if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
363
517
  if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
518
+ clearFlowTimers();
519
+ clearPendingFlowResume();
364
520
  },
365
- [],
521
+ [clearFlowTimers, clearPendingFlowResume],
366
522
  );
367
523
 
368
524
  /* 自动滚到底:用 ResizeObserver 监听消息容器内容高度变化
@@ -374,11 +530,11 @@ export default function ChatConversationPattern({
374
530
  const inner = el.firstElementChild;
375
531
  if (!inner) return undefined;
376
532
  const ro = new ResizeObserver(() => {
377
- el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' });
533
+ scrollToLatest();
378
534
  });
379
535
  ro.observe(inner);
380
536
  return () => ro.disconnect();
381
- }, [phase, messages.length]);
537
+ }, [phase, messages.length, scrollToLatest]);
382
538
 
383
539
  /* 重置回欢迎状态 */
384
540
  const handleNewSession = useCallback(() => {
@@ -388,12 +544,11 @@ export default function ChatConversationPattern({
388
544
  setPrefillSeed((s) => s + 1);
389
545
  }, []);
390
546
 
391
- /* ── 取消链路:用户在任务规划卡里点「取消」 ──
392
- * 1. 标记该任务规划卡为「已确认」(卡片置灰)
393
- * 2. 追加用户消息「取消」
394
- * 3. ChatInput 进 replying(短答态)+ 600ms 后追加 AI 寒暄 + 回 default */
547
+ /* ── 取消链路:任务规划卡次按钮 ──
548
+ * 在自动编排有人为闸门等待时,取消会清空 pending resume 并暂停后续链路。 */
395
549
  const handlePlanCancel = useCallback(
396
550
  (planMsgId) => {
551
+ clearPendingFlowResume();
397
552
  setMessages((prev) =>
398
553
  prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
399
554
  );
@@ -421,17 +576,137 @@ export default function ChatConversationPattern({
421
576
  setInputView('default');
422
577
  }, 600);
423
578
  },
424
- [nextId],
579
+ [nextId, clearPendingFlowResume],
580
+ );
581
+
582
+ const handleConfirmPrimary = useCallback(
583
+ (messageId, confirmId, payload = {}) => {
584
+ const isActionable = hasActionablePrimaryButton(payload.primaryActionLabel);
585
+ const selectedLabel = payload.option?.label ?? '';
586
+ setMessages((prev) =>
587
+ prev.map((message) =>
588
+ message.id === messageId
589
+ ? {
590
+ ...message,
591
+ confirms: Array.isArray(message.confirms)
592
+ ? message.confirms.map((confirm) => (
593
+ confirm.id === confirmId
594
+ ? {
595
+ ...confirm,
596
+ defaultConfirmed: true,
597
+ selectedValue: payload.value ?? confirm.selectedValue,
598
+ defaultSelectedValue: payload.value ?? confirm.defaultSelectedValue,
599
+ selectedLabel: selectedLabel || confirm.selectedLabel,
600
+ formValues: payload.formValues ?? confirm.formValues,
601
+ }
602
+ : confirm
603
+ ))
604
+ : message.confirms,
605
+ leadText: isActionable ? '已确认,继续执行后续流程。' : message.leadText,
606
+ timestamp: nowHHmm(),
607
+ }
608
+ : message,
609
+ ),
610
+ );
611
+ if (isActionable) resumePendingFlow();
612
+ },
613
+ [resumePendingFlow],
425
614
  );
426
615
 
427
- /* ── 任务规划链路:用户在任务规划卡里点「开始执行任务」 ── */
616
+ const handleConfirmOptionChange = useCallback((messageId, confirmId, value, option) => {
617
+ setMessages((prev) =>
618
+ prev.map((message) =>
619
+ message.id === messageId
620
+ ? {
621
+ ...message,
622
+ confirms: Array.isArray(message.confirms)
623
+ ? message.confirms.map((confirm) => (
624
+ confirm.id === confirmId
625
+ ? {
626
+ ...confirm,
627
+ selectedValue: value,
628
+ defaultSelectedValue: value,
629
+ selectedLabel: option?.label ?? confirm.selectedLabel,
630
+ }
631
+ : confirm
632
+ ))
633
+ : message.confirms,
634
+ }
635
+ : message,
636
+ ),
637
+ );
638
+ }, []);
639
+
640
+ const handleConfirmFormChange = useCallback((messageId, confirmId, formValues, meta = {}) => {
641
+ setMessages((prev) =>
642
+ prev.map((message) =>
643
+ message.id === messageId
644
+ ? {
645
+ ...message,
646
+ confirms: Array.isArray(message.confirms)
647
+ ? message.confirms.map((confirm) => (
648
+ confirm.id === confirmId
649
+ ? {
650
+ ...confirm,
651
+ formValues,
652
+ lastChangedFieldId: meta.fieldId,
653
+ }
654
+ : confirm
655
+ ))
656
+ : message.confirms,
657
+ }
658
+ : message,
659
+ ),
660
+ );
661
+ }, []);
662
+
663
+ const handleConfirmSecondary = useCallback(
664
+ (messageId, confirmId) => {
665
+ setMessages((prev) =>
666
+ prev.map((message) =>
667
+ message.id === messageId
668
+ ? {
669
+ ...message,
670
+ confirms: Array.isArray(message.confirms)
671
+ ? message.confirms.map((confirm) => (
672
+ confirm.id === confirmId ? { ...confirm, defaultConfirmed: true } : confirm
673
+ ))
674
+ : message.confirms,
675
+ leadText: '已取消当前确认,流程暂停。',
676
+ timestamp: nowHHmm(),
677
+ }
678
+ : message,
679
+ ),
680
+ );
681
+ clearPendingFlowResume();
682
+ setInputView('default');
683
+ },
684
+ [clearPendingFlowResume],
685
+ );
686
+
687
+ /* ── 任务规划卡主按钮:有人为闸门时用于恢复自动链路;无闸门时保留旧兜底链路 ── */
428
688
  const handlePlanConfirm = useCallback(
429
689
  (planMsgId) => {
690
+ const hasPendingResume = typeof pendingFlowResumeRef.current === 'function';
430
691
  /* 1) 标记该任务规划卡为「已确认」(持久化到消息数据,重渲染时也是禁用态) */
431
692
  setMessages((prev) =>
432
693
  prev.map((m) => (m.id === planMsgId ? { ...m, planConfirmed: true } : m)),
433
694
  );
434
695
 
696
+ if (hasPendingResume) {
697
+ setMessages((prev) => [
698
+ ...prev,
699
+ {
700
+ id: nextId('u'),
701
+ kind: 'user',
702
+ timestamp: nowHHmm(),
703
+ userContent: [{ type: 'text', value: '开始执行任务' }],
704
+ },
705
+ ]);
706
+ resumePendingFlow();
707
+ return;
708
+ }
709
+
435
710
  const flowMsgId = nextId('flow');
436
711
 
437
712
  /* 2) 立即追加:用户消息「开始执行任务」+ AI 流式执行流(带 stream 标记,由 StreamingChatMessage 接管) */
@@ -486,12 +761,145 @@ export default function ChatConversationPattern({
486
761
  setInputView('default');
487
762
  }, totalMs);
488
763
  },
489
- [nextId],
764
+ [nextId, resumePendingFlow],
765
+ );
766
+
767
+ const startAutoOrchestratedFlow = useCallback(
768
+ (text, reply, ctx = {}) => {
769
+ const contextSnapshot = { ...conversationContextRef.current };
770
+ const thinkingMsgId = nextId('think');
771
+ const confirmMsgId = nextId('confirm');
772
+ const flowMsgId = nextId('flow');
773
+ const payload = buildAutoFlowPayload(text, reply, contextSnapshot);
774
+ const clarificationConfirms = buildClarificationConfirms(contextSnapshot);
775
+
776
+ const finishExecutionFlow = () => {
777
+ setMessages((prev) =>
778
+ prev.map((message) =>
779
+ message.id === flowMsgId
780
+ ? {
781
+ ...message,
782
+ stream: false,
783
+ taskGroups: DEFAULT_CHAT_TASK_GROUPS.map((group) => ({
784
+ ...group,
785
+ status: 'completed',
786
+ })),
787
+ resultText: payload.resultText,
788
+ resultArtifacts: payload.resultArtifacts,
789
+ followUps: payload.followUps,
790
+ timestamp: nowHHmm(),
791
+ }
792
+ : message,
793
+ ),
794
+ );
795
+ conversationContextRef.current = {
796
+ ...contextSnapshot,
797
+ conclusion: payload.resultText,
798
+ artifacts: payload.resultArtifacts.map((artifact) => artifact.title),
799
+ lastUserIntent: text,
800
+ };
801
+ setInputView('default');
802
+ };
803
+
804
+ const startExecutionFlow = () => {
805
+ setInputView('busy');
806
+ setMessages((prev) => [
807
+ ...prev,
808
+ {
809
+ id: flowMsgId,
810
+ kind: 'ai-flow',
811
+ timestamp: nowHHmm(),
812
+ taskGroups: DEFAULT_CHAT_TASK_GROUPS,
813
+ stream: true,
814
+ },
815
+ ]);
816
+ scheduleFlowTimer(finishExecutionFlow, getFlowTotalMs());
817
+ };
818
+
819
+ const continueAfterConfirm = () => {
820
+ setMessages((prev) =>
821
+ prev.map((message) =>
822
+ message.id === confirmMsgId
823
+ ? {
824
+ ...message,
825
+ confirms: Array.isArray(message.confirms)
826
+ ? message.confirms.map((confirm) => ({
827
+ ...confirm,
828
+ defaultConfirmed: true,
829
+ }))
830
+ : message.confirms,
831
+ leadText: '已确认澄清信息,开始自动执行。',
832
+ timestamp: nowHHmm(),
833
+ }
834
+ : message,
835
+ ),
836
+ );
837
+ scheduleFlowTimer(startExecutionFlow, 320);
838
+ };
839
+
840
+ setMessages((prev) => [
841
+ ...prev,
842
+ {
843
+ id: thinkingMsgId,
844
+ kind: 'ai-thinking',
845
+ timestamp: nowHHmm(),
846
+ thinking: {
847
+ ...DEFAULT_CHAT_THINKING,
848
+ state: 'thinking',
849
+ inProgressLabel: '深度思考中 ...',
850
+ defaultExpanded: false,
851
+ },
852
+ className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
853
+ },
854
+ ]);
855
+
856
+ scheduleFlowTimer(() => {
857
+ setMessages((prev) =>
858
+ prev.map((message) =>
859
+ message.id === thinkingMsgId
860
+ ? {
861
+ ...message,
862
+ thinking: {
863
+ ...DEFAULT_CHAT_THINKING,
864
+ state: 'completed',
865
+ durationLabel: '深度思考(用时 3.20 秒)',
866
+ content: buildThinkingContent(text, contextSnapshot),
867
+ defaultExpanded: false,
868
+ },
869
+ resultText: '已完成上下文理解,接下来需要先确认本轮执行口径。',
870
+ timestamp: nowHHmm(),
871
+ }
872
+ : message,
873
+ ),
874
+ );
875
+ }, AUTO_FLOW_TIMINGS.thinkingDone);
876
+
877
+ scheduleFlowTimer(() => {
878
+ setMessages((prev) => [
879
+ ...prev,
880
+ {
881
+ id: confirmMsgId,
882
+ kind: 'ai-confirm',
883
+ timestamp: nowHHmm(),
884
+ leadText: '请确认以下人工澄清信息;若卡片右下角存在可操作主按钮,则点击后再继续执行。',
885
+ confirms: clarificationConfirms,
886
+ },
887
+ ]);
888
+
889
+ if (confirmsRequireHumanGate(clarificationConfirms)) {
890
+ pauseForHumanGate(continueAfterConfirm);
891
+ return;
892
+ }
893
+
894
+ scheduleFlowTimer(continueAfterConfirm, 320);
895
+ }, AUTO_FLOW_TIMINGS.confirmShow);
896
+ },
897
+ [nextId, pauseForHumanGate, scheduleFlowTimer],
490
898
  );
491
899
 
492
900
  /* ── 用户输入框发送 ──
493
- * 状态机:default → replying → 600ms 后追加 AI 回复 → default
494
- * 任务关键词分支也走 replying 态(正在规划中),规划卡推出后回 default 等用户处理卡片 */
901
+ * 状态机:default → replying → AI 深度思考 人工澄清确认busy 执行流 → 结论/产物/追问 → default
902
+ * 底部输入与 followUps 点击都进入同一条会话流,并自动继承上一轮上下文。 */
495
903
  const handleSend = useCallback(
496
904
  (text, ctx = {}) => {
497
905
  const trimmed = (text || '').trim();
@@ -520,64 +928,35 @@ export default function ChatConversationPattern({
520
928
  },
521
929
  ]);
522
930
  setInputView('replying');
931
+ scheduleScrollToLatest();
523
932
 
524
933
  const reply = routeReply(trimmed);
525
934
  if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
935
+ if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
936
+ clearFlowTimers();
937
+ clearPendingFlowResume();
526
938
 
527
- /* 2) 追问结果:点击 follow-up chip 等价于立即发送,并模拟生成对应的新答案 */
528
- if (reply.type === 'follow-up-result') {
939
+ if (reply.type === 'short' && STOP_KEYWORDS.some((keyword) => trimmed.includes(keyword))) {
529
940
  replyTimerRef.current = setTimeout(() => {
530
941
  setMessages((prev) => [
531
942
  ...prev,
532
943
  {
533
944
  id: nextId('a'),
534
- kind: 'ai-result',
945
+ kind: 'ai-text',
535
946
  timestamp: nowHHmm(),
536
- resultText: reply.resultText,
537
- resultArtifacts: reply.resultArtifacts,
538
- followUps: reply.followUps,
947
+ resultText: reply.text,
539
948
  className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
540
949
  },
541
950
  ]);
542
951
  setInputView('default');
952
+ scheduleScrollToLatest();
543
953
  }, 600);
544
954
  return;
545
955
  }
546
956
 
547
- /* 3) 任务流:AI 推规划卡(待用户处理);规划卡出现后输入框回 default 让用户操作卡片 */
548
- if (reply.type === 'task') {
549
- replyTimerRef.current = setTimeout(() => {
550
- setMessages((prev) => [
551
- ...prev,
552
- {
553
- id: nextId('plan'),
554
- kind: 'ai-task-plan',
555
- timestamp: nowHHmm(),
556
- leadText: '我开始规划啦,请稍后...',
557
- planConfirmed: false,
558
- },
559
- ]);
560
- setInputView('default');
561
- }, 600);
562
- return;
563
- }
564
-
565
- /* 4) 短答:600ms 后追加 AI 短答 + 输入框回 default */
566
- replyTimerRef.current = setTimeout(() => {
567
- setMessages((prev) => [
568
- ...prev,
569
- {
570
- id: nextId('a'),
571
- kind: 'ai-text',
572
- timestamp: nowHHmm(),
573
- resultText: reply.text,
574
- className: ctx.source === 'follow-up' ? 'tfds-followup-ai-pop' : '',
575
- },
576
- ]);
577
- setInputView('default');
578
- }, 600);
957
+ startAutoOrchestratedFlow(trimmed, reply, ctx);
579
958
  },
580
- [phase, nextId],
959
+ [phase, nextId, clearFlowTimers, clearPendingFlowResume, scheduleScrollToLatest, startAutoOrchestratedFlow],
581
960
  );
582
961
 
583
962
  const handleFollowUpSend = useCallback(
@@ -598,6 +977,8 @@ export default function ChatConversationPattern({
598
977
  clearTimeout(busyTimerRef.current);
599
978
  busyTimerRef.current = null;
600
979
  }
980
+ clearFlowTimers();
981
+ clearPendingFlowResume();
601
982
  setInputView('default');
602
983
  setMessages((prev) => [
603
984
  ...prev,
@@ -608,15 +989,18 @@ export default function ChatConversationPattern({
608
989
  resultText: '已停止当前任务,需要时再叫我。',
609
990
  },
610
991
  ]);
611
- }, [nextId]);
992
+ }, [nextId, clearFlowTimers, clearPendingFlowResume]);
612
993
 
613
994
  /* 新会话:清空消息 + 切欢迎屏 + 重置输入框状态 */
614
995
  const handleNewSessionFull = useCallback(() => {
615
996
  if (replyTimerRef.current) clearTimeout(replyTimerRef.current);
616
997
  if (busyTimerRef.current) clearTimeout(busyTimerRef.current);
998
+ clearFlowTimers();
999
+ clearPendingFlowResume();
1000
+ conversationContextRef.current = { ...DEFAULT_CONVERSATION_CONTEXT };
617
1001
  setInputView('default');
618
1002
  handleNewSession();
619
- }, [handleNewSession]);
1003
+ }, [handleNewSession, clearFlowTimers, clearPendingFlowResume]);
620
1004
 
621
1005
  const lastIdx = messages.length - 1;
622
1006
  const isNewConversationState = phase === 'welcome' || messages.length === 0;
@@ -649,11 +1033,16 @@ export default function ChatConversationPattern({
649
1033
  ) : (
650
1034
  <ChatPhase
651
1035
  scrollRef={scrollRef}
1036
+ latestAnchorRef={latestAnchorRef}
652
1037
  messages={messages}
653
1038
  lastIdx={lastIdx}
654
1039
  handlers={{
655
1040
  onPlanConfirm: handlePlanConfirm,
656
1041
  onPlanCancel: handlePlanCancel,
1042
+ onConfirmPrimary: handleConfirmPrimary,
1043
+ onConfirmSecondary: handleConfirmSecondary,
1044
+ onConfirmOptionChange: handleConfirmOptionChange,
1045
+ onConfirmFormChange: handleConfirmFormChange,
657
1046
  onFollowUpSelect: handleFollowUpSend,
658
1047
  }}
659
1048
  onSend={handleSend}
@@ -670,7 +1059,18 @@ export default function ChatConversationPattern({
670
1059
  /* ============================================================
671
1060
  * ChatPhase — 对话阶段:消息流 + 底部 ChatInput
672
1061
  * ============================================================ */
673
- function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inputView, prefillText, prefillSeed }) {
1062
+ function ChatPhase({
1063
+ scrollRef,
1064
+ latestAnchorRef,
1065
+ messages,
1066
+ lastIdx,
1067
+ handlers,
1068
+ onSend,
1069
+ onStop,
1070
+ inputView,
1071
+ prefillText,
1072
+ prefillSeed,
1073
+ }) {
674
1074
  /* 上下 40px 渐隐遮罩:让消息进入 / 离开滚动视口时柔和过渡,不硬切
675
1075
  * 顶部渐隐仅在「可向上滚」时启用,回到第一条时取消,避免首条被遮淡 */
676
1076
  const [atTop, setAtTop] = useState(true);
@@ -725,8 +1125,8 @@ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inp
725
1125
  >
726
1126
  <div
727
1127
  className="mx-auto flex w-full flex-col gap-2"
728
- /* 底部 60px padding:避免最后一条消息贴住底部 ChatInput */
729
- style={{ width: '800px', maxWidth: '100%', padding: '0 20px 60px' }}
1128
+ /* 底部 80px padding:让最后一条会话卡片再上移 20px,避免被底部渐隐遮挡 */
1129
+ style={{ width: '800px', maxWidth: '100%', padding: '0 20px 80px' }}
730
1130
  >
731
1131
  {messages.map((m, idx) => {
732
1132
  const isLatest = idx === lastIdx;
@@ -737,6 +1137,7 @@ function ChatPhase({ scrollRef, messages, lastIdx, handlers, onSend, onStop, inp
737
1137
  }
738
1138
  return <ChatMessage key={m.id} {...props} />;
739
1139
  })}
1140
+ <div ref={latestAnchorRef} aria-hidden="true" style={{ height: '1px' }} />
740
1141
  </div>
741
1142
  </div>
742
1143