从零开始的AI聊天:如何制作一个AI聊天网页(附代码)

效果:

一、服务端

① 启动ollama serve(当然需要先下载安装ollama)

 

② 启动后端服务

后端连接ollama中的模型进行问答。

pip install fastapi uvicorn
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import litellm
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 配置 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许所有来源,您可以根据需要修改为特定的来源
    allow_credentials=True,
    allow_methods=["*"],  # 允许所有方法
    allow_headers=["*"],  # 允许所有头部
)

# 定义请求体模型
class QuestionRequest(BaseModel):
    question: str

async def generate_ollama_response(question):
    """ Generate a streaming response using litellm """
    response = await litellm.acompletion(
        model="ollama/model_name", # 可以先在ollama中pull任意模型,然后修改model_name即可
        messages=[
            {"role": "system", "content": "扮演一名中国的法律考试助手,根据用户的问题给出恰当的答案,在必要的时候给出参考法条。"},
            {"role": "user", "content": question}
        ],
        api_base="http://localhost:11434", 
        stream=True  # Enable streaming
    )

    async for chunk in response:
        content = chunk.choices[0].delta.content
        if content:
            yield content  # Yield text incrementally to simulate streaming

@app.post("/QA")
async def qa(request: QuestionRequest):
    return StreamingResponse(generate_ollama_response(request.question), media_type="text/plain")

使用 uvicorn main:app --reload 运行程序。 (程序命名为main.py)

 

③ 代码详解

1)导入依赖模块

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import litellm
from fastapi.middleware.cors import CORSMiddleware

FastAPI:用于创建 Web API 的框架,提供了高效、易用的异步支持。
StreamingResponse:FastAPI 内置的响应类,用于返回流式数据,让客户端可以边接收边处理返回的内容。
BaseModel (Pydantic):用于定义数据模型,帮助验证和解析请求体数据,确保输入数据符合预期的格式。
litellm:用于调用 AI 模型完成任务的库,这里用来生成文本回答。
CORSMiddleware:FastAPI 中间件,用于处理跨域请求(CORS),允许不同来源的前端访问后端接口。

2)创建 FastAPI 应用实例

app = FastAPI()

创建一个 FastAPI 应用实例 app,后续所有 API 接口和中间件的配置都基于这个实例进行设置。

3)配置 CORS 中间件

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  # 允许所有来源访问
    allow_credentials=True, # 允许携带认证信息(如 Cookies)
    allow_methods=["*"],  # 允许所有 HTTP 方法(如 GET, POST 等)
    allow_headers=["*"],  # 允许所有头部信息
)

解决跨域问题,使得前端(可能部署在其他域名或端口)能正常请求这个 API。

4)定义请求体模型

class QuestionRequest(BaseModel):
    question: str

作用:使用 Pydantic 定义一个数据模型 QuestionRequest,用来描述客户端请求的 JSON 数据格式。
详细说明:模型中只有一个字段 question,类型为字符串。
                  当客户端发送请求时,FastAPI 会自动将请求体解析为这个数据模型,并验证其中的数据是否满足定义的格式和类型要求。如果数据格式不符合,会自动返回错误响应。 

5)定义生成流式响应的异步函数

async def generate_ollama_response(question):
    """ Generate a streaming response using litellm """
    response = await litellm.acompletion(
        model="ollama/model_name",  # 指定要使用的 AI 模型名称(需替换为实际模型名)
        messages=[
            {
                "role": "system",
                "content": "扮演一名中国的法律考试助手,根据用户的问题给出恰当的答案,在必要的时候给出参考法条。"
            },
            {"role": "user", "content": question}
        ],
        api_base="http://localhost:11434", 
        stream=True  # 启用流式传输
    )

    async for chunk in response:
        content = chunk.choices[0].delta.content
        if content:
            yield content  # 每当有新的内容时,立刻 yield 返回

函数作用:

    接受用户的提问 question 作为参数。
    调用 litellm.acompletion 异步函数请求 AI 模型生成回答。
参数说明:

    model="ollama/model_name":指定 AI 模型,这里使用的是一个占位名称,需要根据实际情况替换为具体模型名称。
    messages:对话消息列表,其中包含两个角色:system:系统消息,用来定义 AI 模型的行为与角色(此处要求扮演中国的法律考试助手)。user:用户消息,将客户端传入的问题作为内容。
    api_base:指定 AI 服务所在的地址(这里假定运行在本地 localhost:11434)。
    stream=True:开启流式响应,即模型会逐步生成回答而不是一次性返回完整结果。
异步生成器:

    使用 async for 循环遍历 AI 模型返回的每个数据块(chunk)。
    从每个 chunk 中提取实际的文本内容 content。
    如果存在内容,则通过 yield 语句逐步返回该内容,从而实现流式传输,客户端可以边接收边显示生成的文本。

6)定义 API 路由

@app.post("/QA")
async def qa(request: QuestionRequest):
    return StreamingResponse(generate_ollama_response(request.question), media_type="text/plain")

接口定义:

    通过 @app.post("/QA") 装饰器定义一个 POST 类型的接口,路由路径为 /QA。
请求体解析:

    函数参数 request: QuestionRequest 表示该接口接收的请求体必须符合 QuestionRequest 模型,即必须包含一个 question 字段。
返回流式响应:

    调用之前定义的异步生成器函数 generate_ollama_response,传入 request.question 作为参数。
    使用 StreamingResponse 将生成器返回的内容包装为流式响应,媒体类型设置为 text/plain,使客户端能够接收到文本流。

二、客户端

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>AI Chatbot</title>
  <!-- 引入 Bootstrap 和 Google Fonts -->
  <link
    href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
    rel="stylesheet"
  />
  <link
    href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap"
    rel="stylesheet"
  />
  <style>
    body {
      font-family: 'Roboto', sans-serif;
      background-color: #f4f6f9;
      display: flex;
      flex-direction: column;
      height: 100vh;
      margin: 0;
    }
    /* 让 #app 使用整个页面高度 */
    #app {
      height: 100vh;
    }
    /* 聊天框铺满除输入框外的区域 */
    #chat {
      flex-grow: 1;
      overflow-y: auto;
      padding: 20px;
      background-color: #fff;
      border-radius: 15px;
      margin: 0; /* 移除外边距 */
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
    }
    .message {
      display: flex;
      margin-bottom: 20px;
    }
    .question {
      justify-content: flex-end;
    }
    .question .text {
      background-color: #e2f7ff;
      padding: 12px;
      border-radius: 20px;
      max-width: 70%;
      box-shadow: 0 4px 8px rgba(0, 123, 255, 0.1);
    }
    .answer {
      justify-content: flex-start;
    }
    .answer .avatar {
      width: 40px;
      height: 40px;
      border-radius: 50%;
      margin-right: 15px;
    }
    .answer .text {
      background-color: #f0f2f5;
      padding: 12px;
      border-radius: 20px;
      max-width: 70%;
      box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
    }
    #question-form {
      display: flex;
      align-items: center;
      padding: 15px;
      border-top: 1px solid #ddd;
      background-color: #fff;
    }
    #question {
      flex-grow: 1;
      padding: 15px;
      border-radius: 25px;
      border: 1px solid #ddd;
      font-size: 16px;
    }
    button {
      margin-left: 15px;
      background: linear-gradient(145deg, #0066cc, #004d99);
      color: #fff;
      border: none;
      padding: 15px;
      font-size: 16px;
      border-radius: 50%;
      width: 50px;
      height: 50px;
      cursor: pointer;
    }
    button:hover {
      background: linear-gradient(145deg, #004d99, #0066cc);
    }
    /* 设置按钮图案的大小 */
    button img {
      width: 20px;
      height: 20px;
    }
    .spinner {
      border: 4px solid rgba(0, 0, 0, 0.1);
      border-left-color: #007bff;
      border-radius: 50%;
      width: 30px;
      height: 30px;
      animation: spin 1s linear infinite;
      margin: 10px;
    }
    @keyframes spin {
      to {
        transform: rotate(360deg);
      }
    }
  </style>
</head>
<body>
  <div id="app" class="container-fluid d-flex flex-column">
    <div id="chat">
      <!-- 使用 Vue 的 v-for 渲染消息 -->
      <div v-for="(msg, index) in messages" :key="index" :class="['message', msg.type]">
        <template v-if="msg.type === 'question'">
          <div class="text">{{ msg.text }}</div>
        </template>
        <template v-else>
          <img src="lawyer-logo.png" class="avatar" alt="Avatar" />
          <div class="text">{{ msg.text }}</div>
        </template>
      </div>
    </div>

    <!-- 加载动画,仅在获取数据时显示 -->
    <div v-if="loading" class="d-flex align-items-center justify-content-center">
      <div class="spinner"></div>
    </div>

    <!-- 输入框及发送按钮 -->
    <form id="question-form" @submit.prevent="sendQuestion">
      <input
        type="text"
        id="question"
        v-model="newQuestion"
        class="form-control"
        placeholder="请输入您的问题"
        autocomplete="off"
      />
      <button type="submit">
        <img src="https://img.icons8.com/ios/50/ffffff/sent.png" alt="Send" />
      </button>
    </form>
  </div>

  <!-- 引入 Vue 3 -->
  <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
  <script>
    const { createApp, ref, onMounted } = Vue;
    createApp({
      setup() {
        const messages = ref([]);
        const newQuestion = ref('');
        const loading = ref(false);

        // 从本地存储中加载历史记录
        onMounted(() => {
          const saved = localStorage.getItem('chatHistory');
          if (saved) {
            messages.value = JSON.parse(saved);
          }
          // 如果没有历史记录,则显示初始问候
          if (messages.value.length === 0) {
            addAnswer("您好");
          }
          scrollToBottom();
        });

        // 保存聊天记录
        function saveChatHistory() {
          localStorage.setItem('chatHistory', JSON.stringify(messages.value));
        }

        // 添加用户问题
        function addQuestion(text) {
          messages.value.push({ type: 'question', text });
          saveChatHistory();
          scrollToBottom();
        }

        // 添加回答
        function addAnswer(text) {
          messages.value.push({ type: 'answer', text });
          saveChatHistory();
          scrollToBottom();
        }

        // 滚动到底部
        function scrollToBottom() {
          setTimeout(() => {
            const chatDiv = document.getElementById('chat');
            chatDiv.scrollTop = chatDiv.scrollHeight;
          }, 0);
        }

        // 发送问题并获取回答(字逐渐显示)
        async function sendQuestion() {
          const question = newQuestion.value.trim();
          if (!question) return;
          addQuestion(question);
          newQuestion.value = '';
          loading.value = true;

          // 先插入一个空的回答项,用于逐字显示
          const answerIndex = messages.value.length;
          messages.value.push({ type: 'answer', text: '' });
          scrollToBottom();

          try {
            const response = await fetch('http://127.0.0.1:8000/QA', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ question })
            });

            const reader = response.body.getReader();
            const decoder = new TextDecoder();

            while (true) {
              const { value, done } = await reader.read();
              if (done) break;
              const chunk = decoder.decode(value, { stream: true });
              // 模拟逐字显示效果
              for (let char of chunk) {
                messages.value[answerIndex].text += char;
                await new Promise((resolve) => setTimeout(resolve, 50));
                scrollToBottom();
              }
            }
            saveChatHistory();
          } catch (error) {
            console.error("Error:", error);
          } finally {
            loading.value = false;
          }
        }

        return { messages, newQuestion, sendQuestion, loading };
      }
    }).mount('#app');
  </script>
</body>
</html>

1)主要结构说明

<head> 部分:
    引入了 Bootstrap 提供的 CSS 框架,使页面响应式并具有美观的默认样式。
    使用 Google Fonts 加载了 “Roboto” 字体。
    内部 <style> 标签定义了整个页面的样式(后面会详细讲解)。
<body> 部分:
    整个应用被包裹在一个 id="app" 的容器中,这也是 Vue 的挂载点。
    聊天区域 (#chat):用于显示所有对话消息。消息采用 Vue 的 v-for 指令进行循环渲染,不同消息类型(用户提问或 AI 回答)采用不同的模板。
    加载动画:当请求正在处理中时,会显示一个旋转的 loading 图标。
    输入区域:由一个文本输入框和一个提交按钮组成。提交表单时调用 Vue 定义的 sendQuestion 方法。

2)CSS 样式

body {
  font-family: 'Roboto', sans-serif;
  background-color: #f4f6f9;
  display: flex;
  flex-direction: column;
  height: 100vh;
  margin: 0;
}
#app {
  height: 100vh;
}
#chat {
  flex-grow: 1;
  overflow-y: auto;
  padding: 20px;
  background-color: #fff;
  border-radius: 15px;
  margin: 0;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.message {
  display: flex;
  margin-bottom: 20px;
}
.question {
  justify-content: flex-end;
}
.question .text {
  background-color: #e2f7ff;
  padding: 12px;
  border-radius: 20px;
  max-width: 70%;
  box-shadow: 0 4px 8px rgba(0, 123, 255, 0.1);
}
.answer {
  justify-content: flex-start;
}
.answer .avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  margin-right: 15px;
}
.answer .text {
  background-color: #f0f2f5;
  padding: 12px;
  border-radius: 20px;
  max-width: 70%;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
#question-form {
  display: flex;
  align-items: center;
  padding: 15px;
  border-top: 1px solid #ddd;
  background-color: #fff;
}
#question {
  flex-grow: 1;
  padding: 15px;
  border-radius: 25px;
  border: 1px solid #ddd;
  font-size: 16px;
}
button {
  margin-left: 15px;
  background: linear-gradient(145deg, #0066cc, #004d99);
  color: #fff;
  border: none;
  padding: 15px;
  font-size: 16px;
  border-radius: 50%;
  width: 50px;
  height: 50px;
  cursor: pointer;
}
button:hover {
  background: linear-gradient(145deg, #004d99, #0066cc);
}
button img {
  width: 20px;
  height: 20px;
}
.spinner {
  border: 4px solid rgba(0, 0, 0, 0.1);
  border-left-color: #007bff;
  border-radius: 50%;
  width: 30px;
  height: 30px;
  animation: spin 1s linear infinite;
  margin: 10px;
}
@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

整体布局:
    body 和 #app 使用了 Flex 布局,确保整个页面高度占满屏幕,并垂直排列内容。
聊天区域 (#chat):
    通过 flex-grow: 1 占满除输入框以外的所有空间,并设置 overflow-y: auto 保证消息内容超出时可以滚动查看。
消息气泡样式:
    用户提问(question 类)右对齐,背景色为淡蓝色;
    AI 回答(answer 类)左对齐,并在消息前显示一个头像(lawyer-logo.png)。
输入区与按钮:
    输入框占据大部分宽度,并有圆角和边框;
    发送按钮设计为圆形,并使用渐变背景,同时在按钮上显示发送图标。
加载动画:
    定义了一个简单的 CSS 动画,通过 @keyframes spin 实现旋转效果。 

3)Vue 3 脚本部分

const { createApp, ref, onMounted } = Vue;
createApp({
  setup() {
    // 定义响应式变量
    const messages = ref([]);
    const newQuestion = ref('');
    const loading = ref(false);

    // 页面加载时执行的逻辑
    onMounted(() => {
      // 尝试从 localStorage 中加载历史聊天记录
      const saved = localStorage.getItem('chatHistory');
      if (saved) {
        messages.value = JSON.parse(saved);
      }
      // 如果没有历史记录,则添加初始问候
      if (messages.value.length === 0) {
        addAnswer("您好");
      }
      scrollToBottom();
    });

    // 保存聊天记录到 localStorage
    function saveChatHistory() {
      localStorage.setItem('chatHistory', JSON.stringify(messages.value));
    }

    // 添加用户提问到消息列表
    function addQuestion(text) {
      messages.value.push({ type: 'question', text });
      saveChatHistory();
      scrollToBottom();
    }

    // 添加回答到消息列表
    function addAnswer(text) {
      messages.value.push({ type: 'answer', text });
      saveChatHistory();
      scrollToBottom();
    }

    // 使聊天窗口滚动到最新消息
    function scrollToBottom() {
      setTimeout(() => {
        const chatDiv = document.getElementById('chat');
        chatDiv.scrollTop = chatDiv.scrollHeight;
      }, 0);
    }

    // 处理发送问题并获取回答
    async function sendQuestion() {
      const question = newQuestion.value.trim();
      if (!question) return;
      // 将用户问题添加到聊天记录中
      addQuestion(question);
      newQuestion.value = '';
      loading.value = true;

      // 先在消息列表中插入一个空的回答,用于后续逐字显示生成的回答内容
      const answerIndex = messages.value.length;
      messages.value.push({ type: 'answer', text: '' });
      scrollToBottom();

      try {
        // 向后端发送 POST 请求,调用 /QA 接口(对应之前的 Python 后端)
        const response = await fetch('http://127.0.0.1:8000/QA', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ question })
        });

        // 获取响应体的可读流
        const reader = response.body.getReader();
        const decoder = new TextDecoder();

        // 逐块读取后端返回的流数据
        while (true) {
          const { value, done } = await reader.read();
          if (done) break;
          // 将二进制数据解码成字符串
          const chunk = decoder.decode(value, { stream: true });
          // 模拟逐字显示的效果:对每个字符逐一添加,并设置小延时
          for (let char of chunk) {
            messages.value[answerIndex].text += char;
            await new Promise((resolve) => setTimeout(resolve, 50));
            scrollToBottom();
          }
        }
        saveChatHistory();
      } catch (error) {
        console.error("Error:", error);
      } finally {
        loading.value = false;
      }
    }

    // 将需要绑定到模板中的数据和方法返回
    return { messages, newQuestion, sendQuestion, loading };
  }
}).mount('#app');

响应式数据与生命周期钩子
    使用 ref 定义了三个响应式变量:
        messages:存放所有聊天记录(包括用户问题和 AI 回答)。
        newQuestion:绑定输入框的值,保存用户当前输入的问题。
        loading:标记是否正在等待后端响应,控制加载动画的显示。
    在 onMounted 钩子中:
        尝试从浏览器的 localStorage 中加载历史聊天记录,实现页面刷新后依然保留对话记录。
        如果没有历史记录,则调用 addAnswer 显示初始问候 “您好”。


消息处理函数
        addQuestion(text) 和 addAnswer(text) 分别将用户提问和 AI 回答添加到 messages 数组中,同时保存记录并滚动到底部。
        scrollToBottom() 利用 DOM 操作使聊天容器始终滚动到最新消息处。


发送问题与接收流式回答
    sendQuestion() 是整个交互的核心:

        首先检查输入是否为空,然后调用 addQuestion 将用户的问题添加到聊天记录中。
        清空输入框并设置 loading 为 true,表明正在等待回答。
        在 messages 数组中先插入一条空的回答记录,用于后续逐字显示后端返回的数据。
        使用 fetch 方法向 Python 后端的 /QA 接口发送 POST 请求,传递 JSON 格式的 { question }。(注意:后端的接口地址为 http://127.0.0.1:8000/QA,需要与后端部署时的地址保持一致)
        获取响应流后,利用 reader.read() 循环逐步读取后端传来的数据块。
        对每个数据块,使用 TextDecoder 解码成字符串,并通过一个 for 循环逐字追加到对应的回答消息中,同时设置 50 毫秒的延时,以模拟“打字机”效果。
        完成后保存聊天记录,并将 loading 状态恢复为 false。


与后端的交互
    当前端通过 fetch 向 /QA 发送请求时,后端(即之前讲解的 Python 代码)接收到包含 question 字段的 JSON 数据后,会调用 AI 模型生成回答,并以流式方式返回文本。
    前端的代码利用流式读取和逐字追加的方式,实现了实时显示 AI 回答的效果,增强用户体验。 

 

ollama流式响应参考资料

GitHub - ollama/ollama-python: Ollama Python library

<think>好的,用户想了解关于网页对话的实现方法或工具。首先,我需要回顾一下用户提供的引用资料,看看有哪些相关的工具和技术可以推荐。 根据引用[2],有一个基于JavaScript的基础款工具,虽然功能简单,但可以直接复制粘贴使用,适合快速搭建。不过缺点是网页刷新后对话内容无法保留,可能适合临时或简单的需求。 引用[3]提到Open UI,使用现代Web技术如HTML、CSS、JavaScript,并结合React或Vue等框架。模块化设计和Web Components技术能提高开发效率,适合需要定制化界面的项目。这可能是一个不错的选择,特别是如果用户希望有更高的灵活性和复用性。 引用[4]介绍了Chainlit集成Dashscope实现语音交互,支持语音克隆。这对于需要语音功能的网页对话应用非常有用,比如虚拟助手或虚拟主播。用户如果对语音交互感兴趣,这个工具可能很合适。 引用[1]涉及LangChain框架和ChatGLM3,用于对话生成和工具调用。这适合需要复杂对话管理和自然语言处理的场景,比如智能客服或教育应用。本地部署的API端点可能提供更好的隐私和控制。 接下来,我需要整理这些信息,分点说明不同的实现方法和工具,每个部分包括技术栈、特点、适用场景以及可能的代码示例。还要确保按照用户的要求,行内公式用$...$,独立公式用$$...$$,虽然目前问题可能不涉及数学公式,但需要保持警惕。同时生成相关问题,帮助用户进一步探索。 需要检查是否遗漏其他可能的工具或框架,比如Socket.IO用于实时通信,或者Firebase的实时数据库,但用户提供的引用中没有提到这些,所以可能不需要额外添加。保持回答基于提供的引用资料,确保引用标识正确。 最后,确保语言简洁明了,结构清晰,用户能够一步步了解不同方法,并根据自身需求选择合适的工具。同时生成的相关问题要围绕网页对话的实现,比如技术选型、实时通信、安全性等,帮助用户深入思考。</think>关于网页对话的实现方法或工具,以下是基于技术特点和场景需求的分步解析: ### 一、基础实现方案 1. **纯JavaScript基础款** - 技术栈:HTML + CSS + JavaScript - 特点:无需框架,直接通过DOM操作实现界面交互 - 代码示例: ```javascript // 对话容器初始化 const chatBox = document.getElementById('chat-container'); // 消息发送逻辑 function sendMessage() { const input = document.getElementById('input-field"); chatBox.innerHTML += `<div class="message">${input.value}</div>`; input.value = ''; } ``` - 局限:数据无法持久化(刷新后丢失)[^2] ### 二、现代前端框架方案 2. **Open UI + React/Vue** - 技术架构: $$技术栈=React/Vue+WebComponents+模块化设计$$ - 核心优势: - 通过Web Components实现组件复用 - 按需加载减少代码体积(压缩后<50KB)[^3] - 典型应用: ```jsx // React组件示例 import { ChatWindow } from '@openui/web-components'; function App() { return <ChatWindow messageQueue={messages} />; } ``` ### 三、AI增强方案 3. **Chainlit + Dashscope语音集成** - 核心能力: - 支持语音克隆(SenseVoice技术) - 语音转文本输入/语音合成输出 - 对话流程控制API[^4] - 部署架构: ```mermaid graph LR A[网页前端] --> B(WebSocket) B --> C[Chainlit服务] C --> D{DashscopeAPI} ``` ### 四、企业级解决方案 4. **LangChain + ChatGLM3** - 关键配置: ```python # LangChain集成示例 from langchain.chains import ConversationChain chain = ConversationChain(llm=ChatGLM3( endpoint_url="http://localhost:8000/v1/chat/completions" )) ``` - 优势特征: - 支持工具调用(天气查询/数据库访问) - 对话历史管理(可配置TTL)[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

脑子不好真君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值