jialh
2025-06-09 d9734706cbd1b6fbff2c4af440f43bd7e433a8c7
备份
3个文件已修改
1261 ■■■■■ 已修改文件
src/components/AiProChat/AiProChat.tsx 465 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/ExternalBot.tsx 790 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/ai/Bots.tsx 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/AiProChat/AiProChat.tsx
@@ -1,29 +1,41 @@
import React, { useLayoutEffect, useRef, useState, useEffect } from 'react';
import { Bubble, Sender, Welcome } from '@ant-design/x';
import { Button, message, Space, Spin } from 'antd';
import { CopyOutlined, OpenAIOutlined, SyncOutlined } from '@ant-design/icons';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize from 'rehype-sanitize';
import rehypeHighlight from 'rehype-highlight';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { ghcolors } from 'react-syntax-highlighter/dist/esm/styles/prism';
import logo from '/favicon.png';
import { UserOutlined } from '@ant-design/icons';
import './markdown-styles.css'; // 自定义Markdown样式
import FileUploader from '../FileUploader'; // 根据实际路径调整
// 动态加载mermaid(流程图支持)
/* eslint-disable no-constant-condition */
import React, { useLayoutEffect, useRef, useState, useEffect } from "react";
import { Bubble, Sender, Welcome } from "@ant-design/x";
import { App, Button, message, Space, Spin, Upload, UploadFile } from "antd";
import {
  CopyOutlined,
  OpenAIOutlined,
  SyncOutlined,
  UserOutlined,
  UploadOutlined,
  DeleteOutlined,
} from "@ant-design/icons";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import remarkBreaks from "remark-breaks";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import rehypeHighlight from "rehype-highlight";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { ghcolors } from "react-syntax-highlighter/dist/esm/styles/prism";
import logo from "/favicon.png";
import "./markdown-styles.css";
const codeStyle: Record<string, React.CSSProperties> = {
  ...(Object.fromEntries(
    Object.entries(ghcolors).filter(([key]) => typeof key === "string")
  ) as Record<string, React.CSSProperties>),
};
const loadMermaid = () => {
  if (typeof window !== 'undefined' && !(window as any).mermaid) {
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js';
  if (typeof window !== "undefined" && !(window as any).mermaid) {
    const script = document.createElement("script");
    script.src = "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js";
    script.onload = () => {
      (window as any).mermaid.initialize({
      (window as any).mermaid.initialize({
        startOnLoad: true,
        theme: 'default',
        securityLevel: 'loose'
        theme: "default",
        securityLevel: "loose",
      });
    };
    document.body.appendChild(script);
@@ -33,7 +45,7 @@
export type ChatMessage = {
  id: string;
  content: string;
  role: 'user' | 'assistant' | 'aiLoading';
  role: "user" | "assistant" | "aiLoading";
  created: number;
  updateAt: number;
  loading?: boolean;
@@ -42,13 +54,19 @@
export type AiProChatProps = {
  loading?: boolean;
  chats?: ChatMessage[];
  onChatsChange?: (value: ((prevState: ChatMessage[]) => ChatMessage[]) | ChatMessage[]) => void;
  onChatsChange?: (
    value: ((prevState: ChatMessage[]) => ChatMessage[]) | ChatMessage[]
  ) => void;
  style?: React.CSSProperties;
  appStyle?: React.CSSProperties;
  helloMessage?: string;
  botAvatar?: string;
  request: (messages: ChatMessage[]) => Promise<Response>;
  clearMessage?: () => void;
  botId?: string; // 新增 botId 参数
  botTypeId?: number;
  onFileUploaded?: (fileId: string) => void; // 新增文件上传回调
  onFileRemoved?: (removedFileId?: string) => void; // 新增文件删除回调
};
export const AiProChat = ({
@@ -57,34 +75,124 @@
  onChatsChange: parentOnChatsChange,
  style = {},
  appStyle = {},
  helloMessage = '欢迎使用AI助手',
  helloMessage = "欢迎使用AI助手",
  botAvatar = `${logo}`,
  request,
  clearMessage
  clearMessage,
  botId, // 新增
  botTypeId,
  onFileUploaded, // 新增
  onFileRemoved, // 新增
}: AiProChatProps) => {
  const isControlled = parentChats !== undefined && parentOnChatsChange !== undefined;
  const isControlled =
    parentChats !== undefined && parentOnChatsChange !== undefined;
  const [internalChats, setInternalChats] = useState<ChatMessage[]>([]);
  const chats = isControlled ? parentChats : internalChats;
  const setChats = isControlled ? parentOnChatsChange : setInternalChats;
  const [content, setContent] = useState('');
  const [content, setContent] = useState("");
  const [sendLoading, setSendLoading] = useState(false);
  const [isStreaming, setIsStreaming] = useState(false);
  const [uploadedFiles, setUploadedFiles] = useState<string[]>([]);
  const [fileList, setFileList] = useState<UploadFile[]>([]);
  const messagesContainerRef = useRef<HTMLDivElement>(null);
  const messagesEndRef = useRef<HTMLDivElement>(null);
  const autoScrollEnabled = useRef(true);
  const isUserScrolledUp = useRef(false);
  const { message: messageApi } = App.useApp();
  // 初始化mermaid
  useEffect(() => {
    loadMermaid();
  }, []);
  // 渲染后重新初始化mermaid流程图
  useEffect(() => {
    if (typeof window !== 'undefined' && (window as any).mermaid) {
      (window as any).mermaid.init(undefined, '.mermaid');
    if (typeof window !== "undefined" && (window as any).mermaid) {
      (window as any).mermaid.init(undefined, ".mermaid");
    }
  }, [chats]);
  const handleRemoveFile = (name: string) => {
    const fileToRemove = fileList.find(f => f.name === name);
    console.log("handleRemoveFile", fileToRemove);
    if (!fileToRemove) return;
    const newUploadedFiles = uploadedFiles.filter(f => f !== name);
    const newFileList = fileList.filter(f => f.name !== name);
    setUploadedFiles(newUploadedFiles);
    setFileList(newFileList);
    // 确保传递的是文件的 url (即文件ID)
    if (fileToRemove.url) {
      onFileRemoved?.(fileToRemove.url);
    }
  };
  const handleUpload = async (file: File) => {
    if (botTypeId === 2 && uploadedFiles.length >= 1) {
      messageApi.warning("只能上传一个文件,请删除已上传文件");
      return false;
    }
    // 多文件模式检查(假设限制最多5个文件)
    if (botTypeId === 3 && uploadedFiles.length >= 5) {
      messageApi.warning("最多只能上传5个文件");
      return false;
    }
    setFileList(prev => [
      ...prev,
      {
        uid: file.name,
        name: file.name,
        status: "uploading",
      },
    ]);
    const formData = new FormData();
    const tokenKey = `${import.meta.env.VITE_APP_TOKEN_KEY}`;
    formData.append("file", file);
    formData.append("botId", botId || "");
    try {
      const res = await fetch("/api/v1/aiBot/files/upload", {
        method: "POST",
        headers: {
          [tokenKey]: localStorage.getItem("authKey") || "",
          Accept: "application/json",
        },
        body: formData,
      });
      if (!res.ok) throw new Error(`上传失败: ${res.statusText}`);
      const result = await res.json();
      const fileData = JSON.parse(result.data.trim());
      setFileList(prev =>
        prev.map(f =>
          f.name === file.name
            ? { ...f, uid: fileData.name, status: "done", url: fileData.id }
            : f
        )
      );
      setUploadedFiles((prev) => [...prev, fileData.name]);
      messageApi.success(`${fileData.name} 上传成功`);
      // 调用回调函数传递文件ID
      onFileUploaded?.(fileData.id);
      return fileData;
    } catch (error) {
      setFileList(prev =>
        prev.map(f =>
          f.name === file.name
            ? { ...f, status: "error" }
            : f
        )
      );
      messageApi.error((error as Error).message || "上传失败");
      throw error;
    }
  };
  const scrollToBottom = () => {
    const container = messagesContainerRef.current;
@@ -120,53 +228,69 @@
      }
    };
    container.addEventListener('scroll', handleScroll);
    container.addEventListener("scroll", handleScroll);
    return () => {
      container.removeEventListener('scroll', handleScroll);
      container.removeEventListener("scroll", handleScroll);
    };
  }, []);
  const handleSubmit = async () => {
    if (!content.trim()) return;
    if (!content.trim() && uploadedFiles.length === 0) return;
    setSendLoading(true);
    setIsStreaming(true);
    let finalContent = content;
    if (uploadedFiles.length > 0) {
      finalContent =
        uploadedFiles.map((name) => `📄 ${name}`).join("\n") +
        (content ? "\n" + content : "");
    }
    const userMessage: ChatMessage = {
      role: 'user',
      role: "user",
      id: Date.now().toString(),
      content,
      content: finalContent,
      created: Date.now(),
      updateAt: Date.now(),
    };
    const aiMessage: ChatMessage = {
      role: 'assistant',
      role: "assistant",
      id: Date.now().toString(),
      content: '',
      content: "",
      loading: true,
      created: Date.now(),
      updateAt: Date.now(),
    };
    const temp = [userMessage, aiMessage];
    setChats?.((prev: ChatMessage[]) => [...(prev || []), ...temp]);
    setChats?.((prev: ChatMessage[]) => [
      ...(prev || []),
      userMessage,
      aiMessage,
    ]);
    setUploadedFiles([]);
    setContent("");
    setTimeout(scrollToBottom, 50);
    setContent('');
    try {
      const response = await request([...(chats || []), userMessage]);
      if (!response?.body) return;
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let partial = '';
      let currentContent = '';
      let partial = "";
      let currentContent = "";
      while (true) {
        const {done, value} = await reader.read();
        const { done, value } = await reader.read();
        if (done) break;
        partial += decoder.decode(value, {stream: true});
        partial += decoder.decode(value, { stream: true });
        const id = setInterval(() => {
          currentContent = partial.slice(0, currentContent.length + 2);
          setChats?.((prev: ChatMessage[]) => {
            const newChats = [...(prev || [])];
            const lastMsg = newChats[newChats.length - 1];
            if (lastMsg?.role === 'assistant') {
            if (lastMsg?.role === "assistant") {
              lastMsg.loading = false;
              lastMsg.content = currentContent;
              lastMsg.updateAt = Date.now();
@@ -182,8 +306,8 @@
        }, 50);
      }
    } catch (error) {
      console.error('Error:', error);
      message.error('请求失败,请重试');
      console.error("Error:", error);
      message.error("请求失败,请重试");
    } finally {
      setIsStreaming(false);
      setSendLoading(false);
@@ -192,7 +316,7 @@
  const handleRegenerate = async (index: number) => {
    const prevMessage: ChatMessage = {
      role: 'user',
      role: "user",
      id: Date.now().toString(),
      content: chats[index - 1].content,
      loading: false,
@@ -200,9 +324,9 @@
      updateAt: Date.now(),
    };
    const aiMessage: ChatMessage = {
      role: 'assistant',
      role: "assistant",
      id: Date.now().toString(),
      content: '',
      content: "",
      loading: true,
      created: Date.now(),
      updateAt: Date.now(),
@@ -215,19 +339,19 @@
      if (!response?.body) return;
      const reader = response.body.getReader();
      const decoder = new TextDecoder();
      let partial = '';
      let currentContent = '';
      let partial = "";
      let currentContent = "";
      while (true) {
        const {done, value} = await reader.read();
        const { done, value } = await reader.read();
        if (done) break;
        partial += decoder.decode(value, {stream: true});
        partial += decoder.decode(value, { stream: true });
        const id = setInterval(() => {
          currentContent = partial.slice(0, currentContent.length + 2);
          setChats?.((prev: ChatMessage[]) => {
            const newChats = [...(prev || [])];
            const lastMsg = newChats[newChats.length - 1];
            if (lastMsg.role === 'assistant') {
            if (lastMsg.role === "assistant") {
              lastMsg.loading = false;
              lastMsg.content = currentContent;
              lastMsg.updateAt = Date.now();
@@ -240,7 +364,7 @@
        }, 50);
      }
    } catch (error) {
      console.error('Error:', error);
      console.error("Error:", error);
    }
  };
@@ -249,9 +373,15 @@
      return (
        <Welcome
          variant="borderless"
          icon={<img src={botAvatar} style={{ width: 32, height: 32, borderRadius: '50%' }} alt="AI Avatar" />}
          icon={
            <img
              src={botAvatar}
              style={{ width: 32, height: 32, borderRadius: "50%" }}
              alt="AI Avatar"
            />
          }
          description={helloMessage}
          styles={{icon: {width: 40, height: 40}}}
          styles={{ icon: { width: 40, height: 40 } }}
        />
      );
    }
@@ -260,27 +390,23 @@
        autoScroll={true}
        items={chats.map((chat, index) => ({
          key: chat.id + Math.random().toString(),
          typing: {suffix: <>💗</>},
          header: (
            <Space>
              {new Date(chat.created).toLocaleString()}
            </Space>
          ),
          typing: { suffix: <>💗</> },
          header: <Space>{new Date(chat.created).toLocaleString()}</Space>,
          loading: chat.loading,
          loadingRender: () => (
            <Space>
              <Spin size="small"/>
              <Spin size="small" />
              AI正在思考中...
            </Space>
          ),
          footer: (
            <Space>
              {(chat.role === 'assistant') && (
              {chat.role === "assistant" && (
                <Button
                  color="default"
                  variant="text"
                  size="small"
                  icon={<SyncOutlined/>}
                  icon={<SyncOutlined />}
                  onClick={() => handleRegenerate(index)}
                />
              )}
@@ -288,40 +414,42 @@
                color="default"
                variant="text"
                size="small"
                icon={<CopyOutlined/>}
                icon={<CopyOutlined />}
                onClick={async () => {
                  try {
                    await navigator.clipboard.writeText(chat.content);
                    message.success('复制成功');
                    message.success("复制成功");
                  } catch (error) {
                    message.error('复制失败');
                    message.error("复制失败");
                  }
                }}
              />
            </Space>
          ),
          role: chat.role === 'user' ? 'local' : 'ai',
          role: chat.role === "user" ? "local" : "ai",
          content: (
            <div className="markdown-body">
              {chat.role === 'assistant' ? (
              {chat.role === "assistant" ? (
                <ReactMarkdown
                  remarkPlugins={[remarkGfm, remarkBreaks]}
                  rehypePlugins={[
                    rehypeRaw,
                    rehypeSanitize,
                    rehypeHighlight,
                  ]}
                  rehypePlugins={[rehypeRaw, rehypeSanitize, rehypeHighlight]}
                  components={{
                    code({ node, inline, className, children, ...props }) {
                      const match = /language-(\w+)/.exec(className || '');
                      return !inline ? (
                    code({ node, className, children, ...props }) {
                      const match = /language-(\w+)/.exec(className || "");
                      const isInline = !!(
                        node?.position?.start &&
                        node.position.end &&
                        node.position.start.line === node.position.end.line
                      );
                      return !isInline ? (
                        <SyntaxHighlighter
                          language={match?.[1] || 'text'}
                          style={ghcolors}
                          language={match?.[1] || "text"}
                          style={codeStyle as { [key: string]: React.CSSProperties }}
                          PreTag="div"
                          {...props}
                        >
                          {String(children).replace(/\n$/, '')}
                          {String(children).replace(/\n$/, "")}
                        </SyntaxHighlighter>
                      ) : (
                        <code className={className} {...props}>
@@ -338,15 +466,21 @@
              )}
            </div>
          ),
          avatar: chat.role === 'assistant' ? (
            <img
              src={botAvatar}
              style={{ width: 32, height: 32, borderRadius: '50%' }}
              alt="AI Avatar"
            />
          ) : { icon: <UserOutlined />, style: { color: '#fff', backgroundColor: '#87d068' } },
          avatar:
            chat.role === "assistant" ? (
              <img
                src={botAvatar}
                style={{ width: 32, height: 32, borderRadius: "50%" }}
                alt="AI Avatar"
              />
            ) : (
              {
                icon: <UserOutlined />,
                style: { color: "#fff", backgroundColor: "#87d068" },
              }
            ),
        }))}
        roles={{ai: {placement: 'start'}, local: {placement: 'end'}}}
        roles={{ ai: { placement: "start" }, local: { placement: "end" } }}
      />
    );
  };
@@ -354,12 +488,12 @@
  return (
    <div
      style={{
        width: '100%',
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        background: '#fff',
        border: '1px solid #f3f3f3',
        width: "100%",
        height: "100%",
        display: "flex",
        flexDirection: "column",
        background: "#fff",
        border: "1px solid #f3f3f3",
        ...appStyle,
        ...style,
      }}
@@ -368,35 +502,96 @@
        ref={messagesContainerRef}
        style={{
          flex: 1,
          overflowY: 'auto',
          padding: '16px',
          scrollbarWidth: 'none',
          overflowY: "auto",
          padding: "16px",
          scrollbarWidth: "none",
        }}
      >
        {loading ? (
          <Spin tip="加载中..."/>
          <Spin tip="加载中..." />
        ) : (
          <>
            {renderMessages()}
            <div ref={messagesEndRef}/>
            <div ref={messagesEndRef} />
          </>
        )}
      </div>
      <div
        style={{
          borderTop: '1px solid #eee',
          padding: '12px',
          display: 'flex',
          gap: '8px',
        }}
      >
        {/* <Sender
      <div style={{ borderTop: "1px solid #eee", padding: "12px" }}>
        {uploadedFiles.length > 0 && (
          <div
            style={{
              marginBottom: 8,
              padding: 8,
              background: "#f5f5f5",
              borderRadius: 4,
              display: "flex",
              flexWrap: "wrap",
              gap: 8,
            }}
          >
            {uploadedFiles.map((name) => (
              <div
                key={name}
                style={{
                  display: "flex",
                  alignItems: "center",
                  padding: "4px 8px",
                  background: "#fff",
                  borderRadius: 4,
                  border: "1px solid #d9d9d9",
                }}
              >
                <span style={{ marginRight: 4 }}>📄</span>
                <span
                  style={{
                    maxWidth: 200,
                    overflow: "hidden",
                    textOverflow: "ellipsis",
                    whiteSpace: "nowrap",
                  }}
                >
                  {name}
                </span>
                {/* 添加删除按钮 */}
                <Button
                  type="text"
                  size="small"
                  icon={<DeleteOutlined />}
                  onClick={() => {
                    handleRemoveFile(name);
                    // setUploadedFiles(uploadedFiles.filter((f) => f !== name));
                    // setFileList(fileList.filter((f) => f.name !== name));
                  }}
                />
              </div>
            ))}
          </div>
        )}
        <Sender
          value={content}
          onChange={setContent}
          onSubmit={handleSubmit}
          loading={sendLoading || isStreaming}
          actions={(_, info) => (
            <Space size="small">
              {(botTypeId === 2 || botTypeId === 3) && (
                <Upload
                  showUploadList={false}
                  beforeUpload={(file) => {
                    handleUpload(file);
                    return false;
                  }}
                  fileList={fileList}
                >
                  <Button
                    icon={<UploadOutlined />}
                    loading={fileList.some((f) => f.status === "uploading")}
                  >
                    上传文件
                  </Button>
                </Upload>
              )}
              <info.components.ClearButton
                disabled={false}
                title="删除对话记录"
@@ -408,50 +603,12 @@
              />
              <info.components.SendButton
                type="primary"
                icon={<OpenAIOutlined/>}
                icon={<OpenAIOutlined />}
                loading={sendLoading || isStreaming}
              />
            </Space>
          )}
        /> */}
<Sender
  value={content}
  onChange={setContent}
  onSubmit={handleSubmit}
  loading={sendLoading || isStreaming}
  actions={(_, info) => (
    <Space size="small">
      {/* 使用项目中的上传组件 */}
      <FileUploader
        onChange={(fileUrl) => {
          if (fileUrl) {
            // 将文件链接添加到输入框(或直接发送)
            setContent(prev => `${prev}\n[文件] ${fileUrl}`);
            message.success('文件上传成功');
          }
        }}
      />
      {/* 原有按钮 */}
      <info.components.ClearButton
        disabled={false}
        title="删除对话记录"
        style={{ fontSize: 20 }}
        onClick={(e) => {
          e.preventDefault();
          clearMessage?.();
        }}
      />
      <info.components.SendButton
        type="primary"
        icon={<OpenAIOutlined />}
        loading={sendLoading || isStreaming}
      />
    </Space>
  )}
/>
        />
      </div>
    </div>
  );
src/pages/ExternalBot.tsx
@@ -1,385 +1,457 @@
import { Conversations, ConversationsProps } from "@ant-design/x";
import { createStyles } from "antd-style";
import React, { useEffect, useRef, useState } from "react";
import {
    Conversations, ConversationsProps,
} from '@ant-design/x';
import { createStyles } from 'antd-style';
import React, {useEffect, useRef, useState} from 'react';
import {
    DeleteOutlined,
    EditOutlined, ExclamationCircleFilled,
    PlusOutlined,
} from '@ant-design/icons';
import {Button, type GetProp, Modal, Input, message} from 'antd';
  DeleteOutlined,
  EditOutlined,
  ExclamationCircleFilled,
  PlusOutlined,
} from "@ant-design/icons";
import { Button, type GetProp, Modal, Input, message } from "antd";
import { AiProChat, ChatMessage } from "../components/AiProChat/AiProChat.tsx";
import {getExternalSessionId, setNewExternalSessionId, updateExternalSessionId} from "../libs/getExternalSessionId.ts";
import {
  getExternalSessionId,
  setNewExternalSessionId,
  updateExternalSessionId,
} from "../libs/getExternalSessionId.ts";
import { useSse } from "../hooks/useSse.ts";
import { useParams } from "react-router-dom";
import { useGetManual } from "../hooks/useApis.ts";
import {uuid} from "../libs/uuid.ts";
import { uuid } from "../libs/uuid.ts";
const useStyle = createStyles(({ token, css }) => {
    return {
        layout: css`
            width: 100%;
            min-width: 1000px;
            height: 100vh;
            border-radius: ${token.borderRadius}px;
            display: flex;
            background: ${token.colorBgContainer};
            font-family: AlibabaPuHuiTi, ${token.fontFamily}, sans-serif;
            .ant-prompts {
                color: ${token.colorText};
            }
        `,
        menu: css`
            background: ${token.colorBgLayout}80;
            width: 300px;
            min-width: 300px;
            height: 100%;
            display: flex;
            flex-direction: column;
        `,
        conversations: css`
            padding: 0 12px;
            flex: 1;
            overflow-y: auto;
        `,
        chat: css`
            height: 100%;
            width: 100%;
            margin: 0 auto;
            box-sizing: border-box;
            display: flex;
            flex-direction: column;
            padding: ${token.paddingLG}px;
            gap: 16px;
        `,
        messages: css`
            flex: 1;
        `,
        placeholder: css`
            padding-top: 32px;
        `,
        sender: css`
            box-shadow: ${token.boxShadow};
        `,
        logo: css`
            display: flex;
            height: 72px;
            align-items: center;
            justify-content: start;
            padding: 0 24px;
            box-sizing: border-box;
            img {
                width: 45px;
                height: 40px;
                display: inline-block;
            }
            span {
                display: inline-block;
                margin: 0 8px;
                font-weight: bold;
                color: ${token.colorText};
                font-size: 16px;
            }
        `,
        addBtn: css`
            background: #1677ff0f;
            border: 1px solid #1677ff34;
            width: calc(100% - 24px);
            margin: 0 12px 24px 12px;
        `,
    };
  return {
    layout: css`
      width: 100%;
      min-width: 1000px;
      height: 100vh;
      border-radius: ${token.borderRadius}px;
      display: flex;
      background: ${token.colorBgContainer};
      font-family: AlibabaPuHuiTi, ${token.fontFamily}, sans-serif;
      .ant-prompts {
        color: ${token.colorText};
      }
    `,
    menu: css`
      background: ${token.colorBgLayout}80;
      width: 300px;
      min-width: 300px;
      height: 100%;
      display: flex;
      flex-direction: column;
    `,
    conversations: css`
      padding: 0 12px;
      flex: 1;
      overflow-y: auto;
    `,
    chat: css`
      height: 100%;
      width: 100%;
      margin: 0 auto;
      box-sizing: border-box;
      display: flex;
      flex-direction: column;
      padding: ${token.paddingLG}px;
      gap: 16px;
    `,
    messages: css`
      flex: 1;
    `,
    placeholder: css`
      padding-top: 32px;
    `,
    sender: css`
      box-shadow: ${token.boxShadow};
    `,
    logo: css`
      display: flex;
      height: 72px;
      align-items: center;
      justify-content: start;
      padding: 0 24px;
      box-sizing: border-box;
      img {
        width: 45px;
        height: 40px;
        display: inline-block;
      }
      span {
        display: inline-block;
        margin: 0 8px;
        font-weight: bold;
        color: ${token.colorText};
        font-size: 16px;
      }
    `,
    addBtn: css`
      background: #1677ff0f;
      border: 1px solid #1677ff34;
      width: calc(100% - 24px);
      margin: 0 12px 24px 12px;
    `,
  };
});
export const ExternalBot: React.FC = () => {
    const urlParams = new URLSearchParams(location.search);
    const [isExternalIFrame, setIsExternalIFrame] = useState<boolean>(false);
    const isExternalIFrameRef = useRef(isExternalIFrame);
   const {doGet: doGetCreateToken} =  useGetManual('/api/temp-token/create')
    useEffect(() => {
        isExternalIFrameRef.current = isExternalIFrame;
    }, [isExternalIFrame]);
    useEffect(() => {
        const isFrame = urlParams.get('isIframe');
        const token = urlParams.get('authKey');
        if (isFrame) {
            const newValue = true;
            setIsExternalIFrame(newValue);
            isExternalIFrameRef.current = newValue; // 手动同步 ref
            doGetCreateToken().then((res: any) => {
                if (res.data.errorCode === 0){
                    localStorage.setItem('authKey', res.data.data);
                    const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement;
                    doGetBotInfo({params: {id: params?.id}}).then((r: any) => {
                        if (link) {
                            link.href = r?.data?.data?.icon || '/favicon.png';
                        }
                        document.title = r?.data?.data?.title;
                    });
                }
            });
        } else if (token) {
                localStorage.setItem('authKey', token);
        }
    }, []); // 空依赖数组表示只在组件挂载时执行一次
    const [newTitle, setNewTitle] = useState<string>('');
    // ==================== Style ====================
    const { styles } = useStyle();
    // ==================== State ====================
    const [conversationsItems, setConversationsItems] = React.useState<{ key: string; label: string }[]>([]);
    const [activeKey, setActiveKey] = React.useState('');
    const [open, setOpen] = useState(false);
    const params = useParams();
    const { doGet: doGetBotInfo, result: botInfo} =useGetManual("/api/v1/aiBot/detail")
    const { start: startChat } = useSse("/api/v1/aiBot/chat");
    // 查询会话列表的数据
    const { doGet: getConversationManualGet } = useGetManual('/api/v1/conversation/externalList');
    const { doGet: doGetManual } = useGetManual("/api/v1/aiBotMessage/messageList");
    const { doGet: doGetConverManualDelete } = useGetManual("/api/v1/conversation/deleteConversation");
    const { doGet: doGetConverManualUpdate } = useGetManual("/api/v1/conversation/updateConversation");
    const menuConfig: ConversationsProps['menu'] = () => ({
        items: [
            {
                label: '重命名',
                key: 'update',
                icon: <EditOutlined />,
            },
            {
                label: '删除',
                key: 'delete',
                icon: <DeleteOutlined />,
                danger: true,
            },
        ],
        onClick: (menuInfo) => {
            if (menuInfo.key === 'delete') {
                Modal.confirm({
                    title: '删除对话',
                    icon: <ExclamationCircleFilled />,
                    content: '删除后,该对话将不可恢复。确认删除吗?',
                    onOk() {
                        doGetConverManualDelete({
                            params: {
                                sessionId: getExternalSessionId(),
                                botId: params?.id,
                            },
                        }).then((res: any) => {
                            if (res.data.errorCode === 0){
                                message.success('删除成功');
                                setChats([])
                                getConversationManualGet({params: { "botId": params?.id }})
                            }
                        });
                    },
                    onCancel() {
                    },
                });
            } else if (menuInfo.key === 'update') {
                showModal()
            }
        },
    });
    const [chats, setChats] = useState<ChatMessage[]>([]);
    const getConversations = (options: { sessionId: any; title: any }[]): { key: any; label: any }[] => {
        if (options) {
            return options.map((item) => ({
                key: item.sessionId,
                label: item.title,
            }));
        }
        return [];
    };
     useEffect(() => {
         if (isExternalIFrameRef.current) {
             return;
         }
            if (chats.length === 2 && chats[1].content.length < 1){
                getConversationManualGet({
                    params:  { "botId": params?.id }
                }).then((r: any) => {
                    setConversationsItems(getConversations(r?.data?.data?.cons));
                });
            }
        }, [chats])
    useEffect(() => {
        if (isExternalIFrameRef.current) {
            return;
        }
        const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement;
        doGetBotInfo({params: {id: params?.id}}).then((r: any) => {
  const urlParams = new URLSearchParams(location.search);
  const [isExternalIFrame, setIsExternalIFrame] = useState<boolean>(false);
  const isExternalIFrameRef = useRef(isExternalIFrame);
  const { doGet: doGetCreateToken } = useGetManual("/api/temp-token/create");
  useEffect(() => {
    isExternalIFrameRef.current = isExternalIFrame;
  }, [isExternalIFrame]);
  useEffect(() => {
    const isFrame = urlParams.get("isIframe");
    const token = urlParams.get("authKey");
    if (isFrame) {
      const newValue = true;
      setIsExternalIFrame(newValue);
      isExternalIFrameRef.current = newValue; // 手动同步 ref
      doGetCreateToken().then((res: any) => {
        if (res.data.errorCode === 0) {
          localStorage.setItem("authKey", res.data.data);
          const link = document.querySelector(
            "link[rel*='icon']"
          ) as HTMLLinkElement;
          doGetBotInfo({ params: { id: params?.id } }).then((r: any) => {
            if (link) {
                link.href = r?.data?.data?.icon || '/favicon.png';
              link.href = r?.data?.data?.icon || "/favicon.png";
            }
            document.title = r?.data?.data?.title;
        });
          });
        }
      });
    } else if (token) {
      localStorage.setItem("authKey", token);
    }
  }, []); // 空依赖数组表示只在组件挂载时执行一次
  const [newTitle, setNewTitle] = useState<string>("");
        updateExternalSessionId(uuid())
        getConversationManualGet(
            {
                params:  { "botId": params?.id }
            }
        ).then((r: any) => {
            setActiveKey(getExternalSessionId());
            setConversationsItems(getConversations(r?.data?.data?.cons));
        });
    }, [])
  // ==================== Style ====================
  const { styles } = useStyle();
    const onAddConversation = () => {
        setNewExternalSessionId();
        // setConversationsItems(prev => [ { key: getExternalSessionId(), label: '新建会话' }, ...prev]);
        setActiveKey(getExternalSessionId());
        setChats([])
    };
    const onConversationClick: GetProp<typeof Conversations, 'onActiveChange'> = (key) => {
        setActiveKey(key);
        updateExternalSessionId(key);
        doGetManual({
            params: {
                sessionId: key,
  // ==================== State ====================
  const [conversationsItems, setConversationsItems] = React.useState<
    { key: string; label: string }[]
  >([]);
  const [activeKey, setActiveKey] = React.useState("");
  const [open, setOpen] = useState(false);
  const params = useParams();
  const { doGet: doGetBotInfo, result: botInfo } = useGetManual(
    "/api/v1/aiBot/detail"
  );
  const { start: startChat } = useSse("/api/v1/aiBot/chat");
  // 查询会话列表的数据
  const { doGet: getConversationManualGet } = useGetManual(
    "/api/v1/conversation/externalList"
  );
  const { doGet: doGetManual } = useGetManual(
    "/api/v1/aiBotMessage/messageList"
  );
  const { doGet: doGetConverManualDelete } = useGetManual(
    "/api/v1/conversation/deleteConversation"
  );
  const { doGet: doGetConverManualUpdate } = useGetManual(
    "/api/v1/conversation/updateConversation"
  );
  const menuConfig: ConversationsProps["menu"] = () => ({
    items: [
      {
        label: "重命名",
        key: "update",
        icon: <EditOutlined />,
      },
      {
        label: "删除",
        key: "delete",
        icon: <DeleteOutlined />,
        danger: true,
      },
    ],
    onClick: (menuInfo) => {
      if (menuInfo.key === "delete") {
        Modal.confirm({
          title: "删除对话",
          icon: <ExclamationCircleFilled />,
          content: "删除后,该对话将不可恢复。确认删除吗?",
          onOk() {
            doGetConverManualDelete({
              params: {
                sessionId: getExternalSessionId(),
                botId: params?.id,
                // 是externalBot页面提交的消息记录
                isExternalMsg: 1
            },
        }).then((r: any) => {
            setChats(r?.data.data);
              },
            }).then((res: any) => {
              if (res.data.errorCode === 0) {
                message.success("删除成功");
                setChats([]);
                getConversationManualGet({ params: { botId: params?.id } });
              }
            });
          },
          onCancel() {},
        });
      } else if (menuInfo.key === "update") {
        showModal();
      }
    },
  });
    };
  const [chats, setChats] = useState<ChatMessage[]>([]);
    const logoNode = (
  const getConversations = (
    options: { sessionId: any; title: any }[]
  ): { key: any; label: any }[] => {
    if (options) {
      return options.map((item) => ({
        key: item.sessionId,
        label: item.title,
      }));
    }
    return [];
  };
  useEffect(() => {
    if (isExternalIFrameRef.current) {
      return;
    }
    if (chats.length === 2 && chats[1].content.length < 1) {
      getConversationManualGet({
        params: { botId: params?.id },
      }).then((r: any) => {
        setConversationsItems(getConversations(r?.data?.data?.cons));
      });
    }
  }, [chats]);
        <div className={styles.logo}>
            <img
                src={botInfo?.data?.icon || "/favicon.png"}
                style={{ width: 32, height: 32, borderRadius: '50%' }}
                draggable={false}
                alt="logo"
            />
            <span>{botInfo?.data?.title}</span>
        </div>
  useEffect(() => {
    if (isExternalIFrameRef.current) {
      return;
    }
    const link = document.querySelector("link[rel*='icon']") as HTMLLinkElement;
    doGetBotInfo({ params: { id: params?.id } }).then((r: any) => {
      if (link) {
        link.href = r?.data?.data?.icon || "/favicon.png";
      }
      document.title = r?.data?.data?.title;
    });
    updateExternalSessionId(uuid());
    getConversationManualGet({
      params: { botId: params?.id },
    }).then((r: any) => {
      setActiveKey(getExternalSessionId());
      setConversationsItems(getConversations(r?.data?.data?.cons));
    });
  }, []);
  const onAddConversation = () => {
    setNewExternalSessionId();
    // setConversationsItems(prev => [ { key: getExternalSessionId(), label: '新建会话' }, ...prev]);
    setActiveKey(getExternalSessionId());
    setChats([]);
  };
  const onConversationClick: GetProp<typeof Conversations, "onActiveChange"> = (
    key
  ) => {
    setActiveKey(key);
    updateExternalSessionId(key);
    doGetManual({
      params: {
        sessionId: key,
        botId: params?.id,
        // 是externalBot页面提交的消息记录
        isExternalMsg: 1,
      },
    }).then((r: any) => {
      setChats(r?.data.data);
    });
  };
  const logoNode = (
    <div className={styles.logo}>
      <img
        src={botInfo?.data?.icon || "/favicon.png"}
        style={{ width: 32, height: 32, borderRadius: "50%" }}
        draggable={false}
        alt="logo"
      />
      <span>{botInfo?.data?.title}</span>
    </div>
  );
  // 更新会话标题的辅助函数
  const updateConversationTitle = (sessionId: string, newTitle: string) => {
    setConversationsItems((prevItems) =>
      prevItems.map((item) =>
        item.key === sessionId ? { ...item, label: newTitle } : item
      )
    );
    // 更新会话标题的辅助函数
    const updateConversationTitle = (sessionId: string, newTitle: string) => {
        setConversationsItems((prevItems) =>
            prevItems.map((item) =>
                item.key === sessionId ? { ...item, label: newTitle } : item
            )
        );
    };
    const showModal = () => {
        setOpen(true);
    };
    const updateTitle = () => {
        doGetConverManualUpdate({
            params: {
                sessionId: activeKey,
                botId: params?.id,
                title: newTitle,
            },
        }).then((res: any) => {
            if (res.data.errorCode === 0){
                // 更新本地状态
                updateConversationTitle(activeKey, newTitle)
                message.success('更新成功');
                setOpen(false);
            }
        });
    };
    const hideModal = () => {
  };
  const showModal = () => {
    setOpen(true);
  };
  const updateTitle = () => {
    doGetConverManualUpdate({
      params: {
        sessionId: activeKey,
        botId: params?.id,
        title: newTitle,
      },
    }).then((res: any) => {
      if (res.data.errorCode === 0) {
        // 更新本地状态
        updateConversationTitle(activeKey, newTitle);
        message.success("更新成功");
        setOpen(false);
    };
    // ==================== Render ====================
    return (
       <div className={styles.layout}>
            <Modal
                title="修改会话名称"
                open={open}
                onOk={updateTitle}
                onCancel={hideModal}
                okText="确认"
                cancelText="取消"
            >
                <Input placeholder="请输入新的会话标题"
                            defaultValue={newTitle}
                            onChange={(e) => {
                                setNewTitle(e.target.value)
                            }}
                        />
            </Modal>
            <div className={styles.menu}>
                {/* 🌟 Logo */}
                {logoNode}
                {/* 🌟 添加会话 */}
                <Button
                    onClick={onAddConversation}
                    type="link"
                    className={styles.addBtn}
                    icon={<PlusOutlined />}
                >
                    新建会话
                </Button>
                {/* 🌟 会话管理 */}
                {conversationsItems && (
                    <Conversations
                        items={conversationsItems}
                        className={styles.conversations}
                        activeKey={activeKey}
                        menu={menuConfig}
                        onActiveChange={onConversationClick}
                    />
                )}
            </div>
            <div className={styles.chat}>
                <AiProChat
                    chats={chats}
                    onChatsChange={setChats} // 确保正确传递 onChatsChange
                    helloMessage="欢迎使用仁智企 ,我是你的专属机器人,有什么问题可以随时问我。"
                    botAvatar={botInfo?.data?.icon}
                    request={async (messages) => {
                        const readableStream = new ReadableStream({
                            async start(controller) {
                                const encoder = new TextEncoder();
                                startChat({
                                    data: {
                                        botId: params.id,
                                        sessionId: getExternalSessionId(),
                                        prompt: messages[messages.length - 1].content as string,
                                        isExternalMsg: 1
                                    },
                                    onMessage: (msg) => {
                                        controller.enqueue(encoder.encode(msg));
                                    },
                                    onFinished: () => {
                                        controller.close();
                                    },
                                })
                            },
                        });
                        return new Response(readableStream);
                    }}
                />
            </div>
        </div>
    );
      }
    });
  };
  const hideModal = () => {
    setOpen(false);
  };
  const [uploadedFileId, setUploadedFileId] = useState<string>("");
  const [uploadedFileIds, setUploadedFileIds] = useState<string[]>([]); // 改为数组存储
  const [botTypeId, setBotTypeId] = useState<number>(0); // 新增 botTypeId 状态
  // 获取机器人信息时同时获取 botTypeId
  useEffect(() => {
    doGetBotInfo({ params: { id: params?.id } }).then((r: any) => {
      setBotTypeId(r?.data?.data?.botTypeId); // 假设返回数据中包含 botTypeId
      // ...其他原有逻辑...
    });
  }, []);
// 处理文件上传回调
const handleFileUploaded = (fileId: string) => {
    if (botTypeId === 2) {
      setUploadedFileIds([fileId]);
    } else if (botTypeId === 3) {
      setUploadedFileIds(prev =>
        prev.includes(fileId) ? prev : [...prev, fileId]
      );
    }
  };
 // 处理文件删除回调
const handleFileRemoved = (removedFileId?: string) => {
    if (!removedFileId) return;
    console.log("handleFileRemoved called with fileId:", removedFileId);
    if (botTypeId === 2) {
      setUploadedFileIds([]);
    } else if (botTypeId === 3) {
      setUploadedFileIds(prev =>
        prev.filter(id => id && id !== removedFileId)
      );
    }
  };
  // 新增:重置文件状态的函数
  const resetFileState = () => {
    setUploadedFileIds([]);
    // 如果需要同时清空文件列表显示,可以在这里添加
  };
  // ==================== Render ====================
  return (
    <div className={styles.layout}>
      <Modal
        title="修改会话名称"
        open={open}
        onOk={updateTitle}
        onCancel={hideModal}
        okText="确认"
        cancelText="取消"
      >
        <Input
          placeholder="请输入新的会话标题"
          defaultValue={newTitle}
          onChange={(e) => {
            setNewTitle(e.target.value);
          }}
        />
      </Modal>
      <div className={styles.menu}>
        {/* 🌟 Logo */}
        {logoNode}
        {/* 🌟 添加会话 */}
        <Button
          onClick={onAddConversation}
          type="link"
          className={styles.addBtn}
          icon={<PlusOutlined />}
        >
          新建会话
        </Button>
        {/* 🌟 会话管理 */}
        {conversationsItems && (
          <Conversations
            items={conversationsItems}
            className={styles.conversations}
            activeKey={activeKey}
            menu={menuConfig}
            onActiveChange={onConversationClick}
          />
        )}
      </div>
      <div className={styles.chat}>
        <AiProChat
          botTypeId={botTypeId} // 传递 botTypeId
          botId={params.id} // 传递 botId
          onFileUploaded={handleFileUploaded}
          onFileRemoved={handleFileRemoved}
          chats={chats}
          onChatsChange={setChats} // 确保正确传递 onChatsChange
          helloMessage="欢迎使用仁智企 ,我是你的专属机器人,有什么问题可以随时问我。"
          botAvatar={botInfo?.data?.icon}
          request={async (messages) => {
            const readableStream = new ReadableStream({
              async start(controller) {
                const encoder = new TextEncoder();
                const requestData: any = {
                  botId: params.id,
                  sessionId: getExternalSessionId(),
                  prompt: messages[messages.length - 1].content as string,
                  isExternalMsg: 1,
                };
                // 根据 botTypeId 添加不同的文件参数
                if (botTypeId === 2 && uploadedFileIds.length > 0) {
                  requestData.file = uploadedFileIds[0];
                } else if (botTypeId === 3 && uploadedFileIds.length > 0) {
                  requestData.files = Array.from(new Set(uploadedFileIds)).filter(id => id);
                }
                startChat({
                  data: requestData,
                  onMessage: (msg) => {
                    controller.enqueue(encoder.encode(msg));
                  },
                  onFinished: () => {
                    controller.close();
                    resetFileState();
                  },
                  onError: () => {
                    resetFileState();
                  },
                });
              },
            });
            return new Response(readableStream);
          }}
        />
      </div>
    </div>
  );
};
export default {
    path: "/ai/externalBot/:id",
    element: ExternalBot,
    frontEnable: true,
  path: "/ai/externalBot/:id",
  element: ExternalBot,
  frontEnable: true,
};
src/pages/ai/Bots.tsx
@@ -124,7 +124,8 @@
            const map: Record<string, string> = {
              "-1": "无",
              "1": "对话模式",
              "2": "文件工作流模式"
              "2": "单文件工作流模式",
              "3": "多文件工作流模式"
            };
            return map[valueStr] || valueStr; // 未知值原样显示
          },
@@ -135,7 +136,8 @@
              options: [
                { value: -1, label: "无" },  // 表单中建议用数字(与接口一致)
                { value: 1, label: "对话模式" },
                { value: 2, label: "文件工作流模式" },
                { value: 2, label: "单文件工作流模式" },
                { value: 3, label: "多文件工作流模式" },
              ],
            },
          },