SOURCE

import React, { useEffect, useState, useRef } from 'react';
import { isEmpty, isFunction, isNotEmptyArr } from 'cn-lib';
import { Button, Form, Input, Modal, Row } from 'antd';
import { marked } from 'marked';
import { ReactComponent as Robot } from '@/assets/robot.svg';

const AIBoss = ({
  visible,
  setVisible,
  cmdForm,
  question,
  getRequestBody,
  apiKey,
  currentStage,
  savePlanCommand,
}) => {
  const [form] = Form.useForm();
  const answerRef = useRef(null);
  const [isAnswering, setAnswering] = useState(false);
  const [currentAnswer, setAnswer] = useState('');
  const decoder = new TextDecoder();

  const aiStreamTalk = () => {
    if (isEmpty(apiKey)) {
      return;
    }
    setAnswering(true);
    let buffer = ''; // 用于累积流数据

    // 设置请求体
    const body = isFunction(getRequestBody) ? getRequestBody(question) : { question };
    answerRef.current = '';

    fetch(`${origin}/agent/v1/chat-messages`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: `Bearer ${apiKey}`,
      },
      body: JSON.stringify({
        response_mode: 'streaming',
        conversation_id: '',
        files: [],
        user: 'abc-123',
        query: question,
        ...(body || {}),
      }),
    })
      .then(response => {
        if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
        return response.body.getReader();
      })
      .then(reader => {
        const readChunk = async () => {
          try {
            const { done, value } = await reader.read();
            if (done) {
              setAnswering(false);
              return;
            }

            buffer += decoder.decode(value, { stream: true }); // 累积到缓冲区

            // 处理所有完整的消息
            let boundary;
            // eslint-disable-next-line no-cond-assign
            while ((boundary = buffer.indexOf('\n')) !== -1) {
              const chunk = buffer.slice(0, boundary);
              buffer = buffer.slice(boundary + 1);

              if (chunk.startsWith('data: ')) {
                const dataStr = chunk.slice(6); // 移除'data: '
                try {
                  const { answer } = JSON.parse(dataStr);
                  if (typeof answer === 'string') {
                    answerRef.current += answer;
                    setAnswer(answerRef.current);
                  }
                } catch (e) {
                  console.error('解析JSON失败:', e, '数据:', dataStr);
                }
              }
            }

            readChunk(); // 继续读取下一块
          } catch (e) {
            console.error('流读取错误:', e);
            setAnswering(false);
          }
        };

        readChunk();
      })
      .catch(() => {
        setAnswering(false);
      });
  };

  useEffect(() => {
    if (question) {
      setAnswer('');
      aiStreamTalk();
    }
  }, [question]);

  useEffect(() => {
    if (currentAnswer) {
      const node = document.getElementById('cnt-ai-footer');
      if (node) {
        node.scrollIntoView({ behavior: 'smooth', block: 'end', inline: 'nearest' });
      }
    }
  }, [currentAnswer]);

  const extractJsonPart = str => {
    const regex = /```json([\s\S]*?)```/;
    const match = regex.exec(str);

    if (match) {
      const jsonContent = match[1].trim().replace(/},\s*\]$/, '}]');
      try {
        return JSON.parse(jsonContent);
      } catch (e) {
        return [];
      }
    } else {
      return [];
    }
  };

  useEffect(() => {
    if (!isAnswering && currentAnswer) {
      const aa = extractJsonPart(currentAnswer);
      let ss = '';
      if (isNotEmptyArr(aa)) {
        aa.forEach(({ cmd }, index) => {
          ss += `${cmd}${index !== aa.length - 1 ? `\n` : ''}`;
        });
      }
      if (ss) {
        form.setFieldsValue({ content: ss });
      }
    }
  }, [isAnswering]);

  return (
    <Modal
      destroyOnClose
      maskClosable={false}
      title={
        <div style={{ display: 'flex', alignContent: 'center' }}>
          <Robot />
          <span style={{ marginLeft: 8, marginTop: 10 }}>{`AI${
            isAnswering ? '深度思考中' : currentAnswer ? '已完成深度思考' : ''
          }`}</span>
        </div>
      }
      open={visible}
      footer={
        <Row justify="end">
          <Button
            onClick={() => {
              setVisible(false);
            }}
          >
            取消
          </Button>
          <Button
            type="primary"
            disabled={isAnswering}
            onClick={() => {
              if (currentStage === 1) {
                const content = form.getFieldValue('content');
                if (content) {
                  cmdForm.setFieldsValue({ planContent: content });
                  savePlanCommand({
                    planContent: content,
                    planDesc: cmdForm.getFieldValue('planDesc') || '',
                  });
                }
              }
              setVisible(false);
            }}
          >
            {currentStage === 1 ? '确认使用' : '确认'}
          </Button>
        </Row>
      }
      onCancel={() => {
        setVisible(false);
      }}
      width={720}
    >
      {currentStage === 1 ? (
        <>
          <div
            className="cnt-ai cnt-default-border"
            style={{ height: 300, overflow: 'auto', padding: '4px 11px' }}
          >
            <div
              dangerouslySetInnerHTML={{ __html: marked.parse(currentAnswer) }}
              className="markdown-body"
            />
            <div id="cnt-ai-footer" style={{ height: 1 }} />
          </div>
          <Form form={form} layout="vertical" style={{ marginTop: 24 }}>
            <Form.Item name="content" label="AI辅助生成配置" style={{ marginBottom: 0 }}>
              <Input.TextArea style={{ height: 200 }} />
            </Form.Item>
          </Form>
        </>
      ) : (
        <div
          className="cnt-ai cnt-default-border"
          style={{ height: 600, overflow: 'auto', padding: '4px 11px' }}
        >
          <div
            dangerouslySetInnerHTML={{ __html: marked.parse(currentAnswer) }}
            className="markdown-body"
          />
          <div id="cnt-ai-footer" style={{ height: 1 }} />
        </div>
      )}
    </Modal>
  );
};

export default AIBoss;
console 命令行工具 X clear

                    
>
console