为了账号安全,请及时绑定邮箱和手机立即绑定

用LangGraph、CopilotKit和Tavily构建属于你的Perplexity搜索引擎

AI驱动的应用程序已经不仅仅局限于自主代理执行任务。一种新的方法涉及人在回路中,允许用户提供反馈、审查结果并指导AI的下一步行动。这类代理被称为CoAgents。

TL;DR(长文概要)

在本教程中,你将学习如何利用LangGraphCopilotKitTavily来构建一个Perplexity克隆。

开始建吧!

什么是代理助手?

代理型副驾(AG)是CopilotKit如何将LangGraph代理集成到你的应用程序中的方式。

CoAgents,是 CopilotKit 构建代理本机应用的方式!

简单来说,它将通过执行多个搜索来响应用户请求,并实时将搜索状态和结果流回给客户端。

可以看一下 CopilotKit ⭐️

此处省略内容

先决条件

要完全理解本教程,你需要了解一些 React 或 Next.js 的基础知识。

我们还将使用以下内容:

  • Python - 一种流行的编程语言,用于使用LangGraph构建AI代理;请确保已安装在您的计算机上。
  • LangGraph - 一个用于创建和部署AI代理的框架。它还有助于定义代理执行的控制流程和操作。
  • OpenAI API Key - 用于使用GPT模型执行各种任务,如生成文本和回答问题;对于本教程,请确保您能够使用GPT-4模型。
  • Tavily AI - 一个搜索引擎,帮助AI代理在应用程序内进行研究并访问实时知识。
  • CopilotKit - 一个开源的副驾驶框架,用于构建定制的AI聊天机器人、应用程序内的AI代理和文本区域。
  • Shad Cn UI - 提供一组可在应用程序内使用的可重用UI组件。

或者
三个星号 (sān gè xīnghào)

(此处省略部分内容) (chǔcǐ shěngjué bù chǎn fēn yùn)

如何使用LangGraph和CopilotKit打造AI智能体

接下来,你将学习如何使用LangGraph和CopilotKit创建一个AI助手。

首先,克隆 CopilotKit CoAgents starter 仓库(点击这里查看:https://github.com/CopilotKit/CopilotKit/tree/main/examples/coagents-starter)。ui 目录包含 Next.js 应用程序的前端部分,而 agent 目录则包含应用程序的 CoAgent。

在名为 agent 的目录中,使用 Poetry 安装项目的依赖项。

    cd agent
    poetry install

点击进入全屏,点击退出全屏

在代理文件夹内创建一个.env文件,并将你的OpenAITavily AI API密钥复制到该文件中:

OPENAI_API_KEY= # 开放AI的API密钥
TAVILY_API_KEY= # TAVILY的API密钥

全屏模式(按此进入/退出)

点此获取OpenAI API密钥

将下面这段代码复制到 agent.py 文件中:

    """
    这是AI的主要入口点。
    它定义了工作流图和代理的入口点。
    """
    # pylint: disable=line-too-long, unused-import
    from langgraph.graph import StateGraph, END
    from langgraph.checkpoint.memory import MemorySaver

    from ai_researcher.state import AgentState
    from ai_researcher.steps import steps_node
    from ai_researcher.search import search_node
    from ai_researcher.summarize import summarize_node
    from ai_researcher.extract import extract_node

    def route(state):
        """路由到研究步骤。"""
        if not state.get("steps", None):
            return END

        current_step = next((step for step in state["steps"] if step["status"] == "pending"), None)

        if not current_step:
            return "summarize_node"

        if current_step["type"] == "search":
            return "search_node"

        raise ValueError(f"未知步骤类型{current_step['type']}:")

    # 定义一个新的工作流图
    workflow = StateGraph(AgentState)
    workflow.add_node("steps_node", steps_node)
    workflow.add_node("search_node", search_node)
    workflow.add_node("summarize_node", summarize_node)
    workflow.add_node("extract_node", extract_node)
    # 对话流程
    workflow.set_entry_point("steps_node")

    workflow.add_conditional_edges(
        "steps_node", 
        route,
        ["summarize_node", "search_node", END]
    )

    workflow.add_edge("search_node", "extract_node")

    workflow.add_conditional_edges(
        "extract_node",
        route,
        ["summarize_node", "search_node"]
    )

    workflow.add_edge("summarize_node", END)

    memory = MemorySaver()
    graph = workflow.compile(checkpointer=memory)

切换到全屏模式,退出全屏

上述代码片段定义了LangGraph代理的工作流程。它从steps_node开始,搜索结果,然后总结它们,并提取关键信息。

Agent Workflow

接下来创建一个demo.py文件,并包含以下代码片段。

    """示例代码"""

    import os
    from dotenv import load_dotenv
    load_dotenv()

    from fastapi import FastAPI
    import uvicorn
    from copilotkit.integrations.fastapi import add_fastapi_endpoint
    from copilotkit import CopilotKitSDK, LangGraphAgent
    from ai_researcher.agent import graph

    app = FastAPI()
    sdk = CopilotKitSDK(
        agents=[
            LangGraphAgent(
                name="ai_researcher",
                description="研究代理。",
                graph=graph,
            )
        ],
    )

    add_fastapi_endpoint(app, sdk, "/copilotkit")

    # 新增健康检查路由
    @app.get("/health")
    def status_check():
        """状态检查。"""
        return {"status": "ok"}

    def main():
        """启动uvicorn服务器。"""
        port = int(os.getenv("PORT", "8000"))
        uvicorn.run("ai_researcher.demo:app", host="0.0.0.0", port=port, reload=True)

全屏 。
退出全屏 。

上面的代码创建了一个FastAPI端点来托管LangGraph代理,并将它连接到CopilotKit SDK。

你可以从 GitHub 仓库 复制剩余的代码以创建 CoAgent。在接下来的部分里,你将学习如何为 Perplexity 的克隆版构建用户界面并使用 CopilotKit 处理搜索请求的过程。

……

使用 Next.js 构建应用界面

在这个部分,我将一步步引导您完成构建应用程序用户界面的步骤。

首先,通过运行如下代码片段来建立一个Next.js的TypeScript项目:

    # 👉🏻 进入 ui 文件夹
    npx create-next-app ./

点击全屏,点击退出全屏

查看 Next.js 安装

在新创建的项目中安装 ShadCn UI 库,运行以下代码片段即可。

    npx shadcn@latest init
使用 npx 初始化 shadcn 项目

进入全屏,退出全屏

接下来,在 Next.js 项目的根目录下创建一个名为 components 的文件夹,然后将此 GitHub 仓库中的 ui 文件夹 复制到该文件夹中。Shadcn 可以让您通过命令行轻松安装各种 组件(如折叠面板),以方便地将它们添加到您的应用程序中。

除了 Shadcn 提供的组件之外,你还需要创建一些代表应用程序界面不同部分的几个组件。在 components 文件夹内运行以下代码片段,以添加这些组件到 Next.js 项目。

执行 touch ResearchWrapper.tsx ResultsView.tsx HomeView.tsx  # 更新文件时间戳或创建空文件
执行 touch AnswerMarkdown.tsx Progress.tsx SkeletonLoader.tsx  # 更新文件时间戳或创建空文件

进入全屏,退出全屏

将下面的代码复制到 app/page.tsx 文件中。

    "use client";

    import { ResearchWrapper } from "@/components/ResearchWrapper";
    import { ModelSelectorProvider, useModelSelectorContext } from "@/lib/model-selector-provider";
    import { ResearchProvider } from "@/lib/research-provider";
    import { CopilotKit } from "@copilotkit/react-core";
    import "@copilotkit/react-ui/styles.css";

    export default function ModelSelectorWrapper() {

      return (
          <CopilotKit runtimeUrl={useLgc ? "/api/copilotkit-lgc" : "/api/copilotkit"} agent="ai_researcher">
            <ResearchProvider>
              <ResearchWrapper />
            </ResearchProvider>
          </CopilotKit>
      );
    }

进入全屏 退出全屏

在上面的代码片段中,ResearchProvider 是一个自定义的 React 上下文提供器,它共享用户的搜索查询及结果,使得它们对应用程序中的所有组件可用。ResearchWrapper 组件包含了应用的核心元素并管理用户界面的显示。

在项目根目录创建一个名为lib的文件夹,并将research-provider.tsx文件放入该文件夹中,然后将以下代码粘贴到该文件中:

    import { createContext, useContext, useState, ReactNode, useEffect } from "react";

    type ResearchContextType = {
      researchQuery: string;
      setResearchQuery: (query: string) => void;
      researchInput: string;
      setResearchInput: (input: string) => void;
      isLoading: boolean;
      setIsLoading: (loading: boolean) => void;
      researchResult: ResearchResult | null;
      setResearchResult: (result: ResearchResult) => void;
    };

    type ResearchResult = {
      answer: string;
      sources: string[];
    }

    const ResearchContext = createContext<ResearchContextType | undefined>(undefined);

    export const ResearchProvider = ({ children }: { children: ReactNode }) => {
      const [researchQuery, setResearchQuery] = useState<string>("");
      const [researchInput, setResearchInput] = useState<string>("");
      const [researchResult, setResearchResult] = useState<ResearchResult | null>(null);
      const [isLoading, setIsLoading] = useState<boolean>(false);

      useEffect(() => {
        if (!researchQuery) {
          setResearchResult(null);
          setResearchInput("");
        }
      }, [researchQuery, researchResult]);

      return (
        <ResearchContext.Provider
          value={{
            researchQuery,
            setResearchQuery,
            researchInput,
            setResearchInput,
            isLoading,
            setIsLoading,
            researchResult,
            setResearchResult,
          }}
        >
          {children}
        </ResearchContext.Provider>
      );
    };

    export const useResearchContext = () => {
      const context = useContext(ResearchContext);
      if (context === undefined) {
        throw new Error("useResearchContext 必须在 ResearchProvider 组件内使用");
      }
      return context;
    };

进入全屏,退出全屏

这些状态会被声明和保存到ResearchContext中,以确保它们在应用程序中多个组件之间能够被妥善管理。

创建一个 ResearchWrapper 组件,如下:

    import { HomeView } from "./HomeView";
    import { ResultsView } from "./ResultsView";
    import { AnimatePresence } from "framer-motion";
    import { useResearchContext } from "@/lib/research-provider";

    export function ResearchWrapper() {
      const { researchQuery, setResearchInput } = useResearchContext();

      return (
        <>
          <div className="flex flex-col items-center justify-center relative z-10">
            <div className="flex-1">
              {researchQuery ? (
                <AnimatePresence
                  key="results"
                  onExitComplete={() => {
                    setResearchInput("");
                  }}
                  mode="wait"
                >
                  <ResultsView key="results" />
                </AnimatePresence>
              ) : (
                <AnimatePresence key="home" mode="wait">
                  <HomeView key="home" />
                </AnimatePresence>
              )}
            </div>
            <footer className="text-xs p-2">
              <a
                href="https://copilotkit.ai"
                target="_blank"
                rel="noopener noreferrer"
                className="text-slate-600 font-medium hover:underline"
              >
                本页面由 CopilotKit 🪁 提供支持
              </a>
            </footer>
          </div>
        </>
      );
    }

全屏/退出全屏

The ResearchWrapper 组件初始化渲染 HomeView 组件作为默认视图,并当提供搜索查询时显示 ResultView。使用 useResearchContext 钩子使我们能够获取 researchQuery 状态并根据需要更新视图。

最后,创建HomeView组件来展示应用程序的主页界面。

    "use client";

    import { useEffect, useState } from "react";
    import { Textarea } from "./ui/textarea";
    import { cn } from "@/lib/utils";
    import { Button } from "./ui/button";
    import { CornerDownLeftIcon } from "lucide-react";
    import { useResearchContext } from "@/lib/research-provider";
    import { motion } from "framer-motion";
    import { useCoAgent } from "@copilotkit/react-core";
    import { TextMessage, MessageRole } from "@copilotkit/runtime-client-gql";
    import type { AgentState } from "../lib/types";
    import { useModelSelectorContext } from "@/lib/model-selector-provider";

    const MAX_INPUT_LENGTH = 250;

    export function HomeView() {
      const { setResearchQuery, researchInput, setResearchInput } =
        useResearchContext();
      const { model } = useModelSelectorContext();
      const [isInputFocused, setIsInputFocused] = useState(false);
      const {
        run: runResearchAgent,
      } = useCoAgent<AgentState>({
        name: "ai_researcher",
        initialState: {
          model,
        },
      });

      const handleResearch = (query: string) => {
        setResearchQuery(query);
        runResearchAgent(() => {
          return new TextMessage({
            role: MessageRole.User,
            content: query,
          });
        });
      };

      const suggestions = [
        { label: "2024年与2023年电动汽车的销量对比", icon: "🚙" },
        { label: "世界最富有的人", icon: "💰" },
        { label: "世界人口", icon: "🌍 " },
        { label: "西雅图与纽约的天气对比", icon: "⛅️" },
      ];

      return (
        <motion.div
          initial={{ opacity: 0, y: -50 }}
          animate={{ opacity: 1, y: 0 }}
          exit={{ opacity: 0 }}
          transition={{ duration: 0.4 }}
          className="h-screen w-full flex flex-col gap-y-2 justify-center items-center p-4 lg:p-0"
        >
          <h1 className="text-4xl font-extralight mb-6">
            您想了解什么?
          </h1>

          <div
            className={cn(
              "w-full bg-slate-100/50 border shadow-sm rounded-md transition-all",
              {
                "ring-1 ring-slate-300": isInputFocused,
              }
            )}
          >
            <Textarea
              placeholder="有什么想了解的?"
              className="bg-transparent p-4 resize-none focus-visible:ring-0 focus-visible:ring-offset-0 border-0 w-full"
              onFocus={() => setIsInputFocused(true)}
              onBlur={() => setIsInputFocused(false)}
              value={researchInput}
              onChange={(e) => setResearchInput(e.target.value)}
              onKeyDown={(e) => {
                if (e.key === "Enter" && !e.shiftKey) {
                  e.preventDefault();
                  handleResearch(researchInput);
                }
              }}
              maxLength={MAX_INPUT_LENGTH}
            />
            <div className="text-xs p-4 flex items-center justify-between">
              <div
                className={cn("transition-all duration-300 mt-4 text-slate-500", {
                  "opacity-0": !researchInput,
                  "opacity-100": researchInput,
                })}
              >
                {researchInput.length} / {MAX_INPUT_LENGTH}
              </div>
              <Button
                size="sm"
                className={cn("rounded-full transition-all duration-300", {
                  "opacity-0 pointer-events-none": !researchInput,
                  "opacity-100": researchInput,
                })}
                onClick={() => handleResearch(researchInput)}
              >
                研究
                <CornerDownLeftIcon className="w-4 h-4 ml-2" />
              </Button>
            </div>
          </div>
          <div className="grid grid-cols-2 w-full gap-2 text-sm">
            {suggestions.map((suggestion) => (
              <div
                key={suggestion.label}
                onClick={() => handleResearch(suggestion.label)}
                className="p-2 bg-slate-100/50 rounded-md border col-span-2 lg:col-span-1 flex cursor-pointer items-center space-x-2 hover:bg-slate-100 transition-all duration-300"
              >
                <span className="text-base">{suggestion.icon}</span>
                <span className="flex-1">{suggestion.label}</span>
              </div>
            ))}
          </div>
        </motion.div>
      );
    }

全屏模式(点击进入/退出)

首页


如何将你的CoAgent连接到Next.js应用

在这个部分里,你将学习如何将CopilotKit CoAgent连接到你的Next.js应用,以让用户在应用内进行搜索。

安装以下 CopilotKit 包以及 OpenAI Node.js SDK。这些包让辅助代理能够与 React 状态值交互并作出应用程序内的决策,以实现这一功能。

    npm install @copilotkit/react-core @copilotkit/react-ui @copilotkit/runtime @copilotkit/runtime-client-gql openai

切换到全屏 退出全屏

在 Next.js 的 app 文件夹里新建一个 api 文件夹。然后,在 api 文件夹里创建一个叫 copilotkit 的目录,并在其中添加一个 route.ts 文件。这会生成一个 API 端点(/api/copilotkit),用来连接前端应用到 CopilotKit CoAgent。

首先,我们进入app目录
cd app
创建api目录并进入
mkdir api && cd api
创建copilotkit目录并进入
mkdir copilotkit && cd copilotkit
创建一个名为route.ts的新文件
touch route.ts

全屏 全屏退出

将下面的代码段复制到 api/copilotkit/route.ts 文件:

import { NextRequest } from "next/server";
import {
  CopilotRuntime,
  OpenAIAdapter,
  copilotRuntimeNextJSAppRouterEndpoint,
} from "@copilotkit/runtime";
import OpenAI from "openai";

//👇🏻 将 OpenAI 初始化为适配器
const openai = new OpenAI();
const serviceAdapter = new OpenAIAdapter({ openai } as any);

//👇🏻 将 CopilotKit 运行时与 CoAgent 连接
const runtime = new CopilotRuntime({
  remoteEndpoints: [
    {
      url: process.env.REMOTE_ACTION_URL || "http://localhost:8000/copilotkit",
    },
  ],
});

export const POST = async (req: NextRequest) => {
  // 返回处理请求的结果
  const { handleRequest } = copilotRuntimeNextJSAppRouterEndpoint({
    runtime,
    serviceAdapter,
    endpoint: "/api/copilotkit",
  });

  return handleRequest(req);
};

进入全屏 退出全屏

下面的代码片段在/api/copilotkit API接口配置了CopilotKit运行环境,使CopilotKit通过AI助手处理用户请求。

最后,更新app/page.tsx文件,通过在应用程序中使用CopilotKit组件包裹整个代码,为所有应用程序组件提供助手环境。

"use client";
// 引入模型选择器组件
import { ModelSelector } from "@/components/ModelSelector";
// 引入研究包装器组件
import { ResearchWrapper } from "@/components/ResearchWrapper";
// 引入模型选择器提供者
import { ModelSelectorProvider, useModelSelectorContext } from "@/lib/model-selector-provider";
// 引入研究提供者
import { ResearchProvider } from "@/lib/research-provider";
// 引入CopilotKit组件
import { CopilotKit } from "@copilotkit/react-core";
// 引入CopilotKit样式
import "@copilotkit/react-ui/styles.css";

// 导出默认函数ModelSelectorWrapper
export default function ModelSelectorWrapper() {
  return (
      <main className="flex flex-col items-center justify-between">
        // 创建主区域
        <ModelSelectorProvider>
          // 展示Home组件
          <Home/>
          // 展示模型选择器组件
          <ModelSelector />
        </ModelSelectorProvider>
      </main>
  );
}

// 创建Home函数
function Home() {
  const { useLgc } = useModelSelectorContext();

  return (
      <CopilotKit runtimeUrl={useLgc ? "/api/copilotkit-lgc" : "/api/copilotkit"} agent="ai_researcher">
        <ResearchProvider>
          // 使用研究提供者
          <ResearchWrapper />
        </ResearchProvider>
      </CopilotKit>
  );
}

全屏模式,退出全屏

CopilotKit 组件覆盖整个应用程序,并接受两个参数 - runtimeUrlagent。其中,runtimeUrl 是托管 AI 代理的后端 API 路由,而 agent 是执行操作的代理名称。

接受请求并实时传输响应给前端

让 CopilotKit 可以访问和处理用户输入,它提供了一个 [useCoAgent 钩子],允许从应用程序中的任何位置访问代理的状态信息。

例如,下面的代码片段展示了如何使用 useCoAgent 钩子。state 变量允许访问代理的当前状态,setState 用于修改代理的状态,而 run 函数用于执行代理的指令。startstop 函数分别用来启动和停止代理的运行。

    const { state, setState, run, start, stop } = useCoAgent({
        name: "搜索代理",
    });

切换到全屏模式 切换回正常模式

更新 HomeView 组件,当提供搜索查询时执行代理操作。

    //👇🏻 从 CopilotKit 导入 useCoAgent 模块
    import { useCoAgent } from "@copilotkit/react-core";

    const { run: runResearchAgent } = useCoAgent({
        name: "search_agent", // 设置名为 'search_agent' 的代理
    });

    const handleResearch = (query: string) => {
        setResearchQuery(query); // 设置搜索查询
        runResearchAgent(query); // 运行搜索代理
    };

进入全屏 退出全屏

接下来,你可以通过访问 useCoAgent 钩子中的状态变量来将搜索结果流式传输显示到 ResultsView。然后,把下面这段代码粘贴到 ResultsView 组件里。

    "use client";

    import { useResearchContext } from "@/lib/research-provider";
    import { motion } from "framer-motion";
    import { BookOpenIcon, LoaderCircleIcon, SparkleIcon } from "lucide-react";
    import { SkeletonLoader } from "./SkeletonLoader";
    import { useCoAgent } from "@copilotkit/react-core";
    import { Progress } from "./Progress";
    import { AnswerMarkdown } from "./AnswerMarkdown";

    export function ResultsView() {
        const { researchQuery } = useResearchContext();
        //👇🏻 代理状态
        const { state: agentState } = useCoAgent({
            name: "search_agent",
        });

        console.log("AGENT_STATE", agentState);

        //👇🏻 跟踪当前代理处理的步骤
        const steps =
            agentState?.steps?.map((step: any) => {
                return {
                    description: step.description || "",
                    status: step.status || "pending",
                    updates: step.updates || [],
                };
            }) || [];

        const isLoading = !agentState?.answer?.markdown;

        return (
            <motion.div
                initial={{ opacity: 0, y: -50 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0, y: -50 }}
                transition={{ duration: 0.5, ease: "easeOut" }}
            >
                <div className='max-w-[1000px] p-8 lg:p-4 flex flex-col gap-y-8 mt-4 lg:mt-6 text-sm lg:text-base'>
                    <div className='space-y-4'>
                        <h1 className='text-3xl lg:text-4xl font-extralight'>
                            {researchQuery}
                        </h1>
                    </div>

                    <Progress steps={steps} />

                    <div className='grid grid-cols-12 gap-8'>
                        <div className='col-span-12 lg:col-span-8 flex flex-col'>
                            <h2 className='flex items-center gap-x-2'>
                                {isLoading ? (
                                    <LoaderCircleIcon className='animate-spin w-4 h-4 text-slate-500' />
                                ) : (
                                    <SparkleIcon className='w-4 h-4 text-slate-500' />
                                )}
                                答案
                            </h2>

                            <div className='text-slate-500 font-light'>
                                {isLoading ? (
                                    <SkeletonLoader />
                                ) : (
                                    <AnswerMarkdown markdown={agentState?.answer?.markdown} /> //👈🏼 注释:显示搜索的答案
                                )}
                            </div>
                        </div>

                        {agentState?.answer?.references?.length && (
                            <div className='flex col-span-12 lg:col-span-4 flex-col gap-y-4 w-[200px]'>
                                <h2 className='flex items-center gap-x-2'>
                                    <BookOpenIcon className='w-4 h-4 text-slate-500' />
                                    参考资料
                                </h2>
                                <ul className='text-slate-900 font-light text-sm flex flex-col gap-y-2'>
                                    {agentState?.answer?.references?.map(
                                        (ref: any, idx: number) => (
                                            <li key={idx}>
                                                <a
                                                    href={ref.url}
                                                    target='_blank'
                                                    rel='noopener noreferrer'
                                                >
                                                    {idx + 1}. {ref.title}
                                                </a>
                                            </li>
                                        )
                                    )}
                                </ul>
                            </div>
                        )}
                    </div>
                </div>
            </motion.div>
        );
    }

全屏,退出全屏

上述代码片段从代理的状态中获取搜索结果,并使用 useCoAgent 钩子将其流式传输到前端。搜索结果以 Markdown 格式返回,并传递给该 AnswerMarkdown 组件,该组件会在页面上渲染内容。

最后,将代码片段复制到 AnswerMarkdown 组件中。这会利用 React Markdown 库 把 markdown 内容转换成格式化的文本。

import Markdown from "react-markdown";

// 导入Markdown模块
export function AnswerMarkdown({ markdown }: { markdown: string }) {
    // 返回一个包含markdown内容的div
    return (
        <div className='markdown-wrapper'>
            <Markdown>{markdown}</Markdown>
        </div>
    );
}

全屏模式 退出全屏

ResultsView Component

恭喜你完成了本教程里的项目。你也可以在这里观看视频回放。

观看完整的网络研讨会录像


总结

LLM智能最有效的是与人类智能一起工作,而CopilotKit CoAgents只需几分钟就能让您将AI代理、副驾和其他类型的助手集成到您的软件应用中。

如果你需要开发一个AI产品或将AI代理嵌入到你的应用中,你可以考虑使用CopilotKit。

你可以在 GitHub 上找到本教程的源代码文件。

在 GitHub 上的 CopilotKit 示例页面:https://github.com/CopilotKit/CopilotKit/tree/main/examples/coagents-ai-researcher

感谢您的阅读!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消