Gradio 4.37.1官方教程三:Chatbot

12 篇文章 2 订阅


传送门:

gradio(GitHub)Chatbot官方教程ChatInterface官方文档

一、使用ChatInterface创建聊天机器人

  聊天机器人是大型语言模型的一种流行应用。使用 Gradio,可以轻松构建聊天机器人模型的演示。gr.ChatInterface()是一个高级抽象,通常只需一行代码即可快速创建聊天机器人 UI,还可以与几个流行的 API 和库(包括 langchain、openai 和 Hugging Face)中的实际语言模型结合使用。

1.1 定义聊天函数

   在使用 gr.ChatInterface() 时,首先需要定义聊天函数。你的聊天函数应接受两个参数:messagehistory(参数名称可以随意,但顺序必须如此)。

  • message:表示用户输入的字符串。
  • history:表示到目前为止的对话历史记录的列表。每个内嵌列表由两个字符串组成,表示一个对话对:[user input, bot response]

最终返回返回一个字符串响应(bot response)。下面是一个简单的示例:

import random
import gradio as gr

def alternatingly_agree(message, history):
    if len(history) % 2 == 0:
        return f"Yes, I do think that '{message}'"
    else:
        return "I don't think so"

gr.ChatInterface(alternatingly_agree).launch()

在这里插入图片描述

1.2 流式聊天机器人(Streaming chatbots)

  流式聊天机器人的响应是逐步生成并显示的,这样用户在等待完整回复的过程中,可以逐步看到部分回复(实时反馈),提高了互动的流畅度和用户体验。在聊天函数中,使用 yield 逐步生成响应,就能实现一个流式聊天机器人。

import time
import gradio as gr

def slow_echo(message, history):
    for i in range(len(message)):
        time.sleep(0.3)
        yield "You typed: " + message[: i+1]

gr.ChatInterface(slow_echo).launch()

  slow_echo:在每次迭代中,函数都会暂停 0.3 秒,然后将用户输入的信息一个个字母的生成出来。另外,在响应流式传输时,“Submit”按钮会变成“Stop”按钮,用来停止生成器函数。你可以使用 stop_btn 参数自定义“Stop”按钮的外观和行为。

1.3 自定义聊天机器人

和Gradio的Interface类一样,gr.ChatInterface类也包括许多相同的参数,可以用来自定义聊天机器人的外观和感觉,比如:

  • 使用titledescription参数在聊天机器人上方添加标题和描述。
  • 使用themecss参数添加主题或自定义CSS。
  • 使用placeholder参数用于设置聊天界面的“占位符”(用户未输入时的起始显示,用于提示用户输入),该参数接受Markdown或HTML。占位符会在聊天机器人中居中显示。
  • 使用examples添加示例,并设置cache_examples=True缓存示例(启动demo时自动运行示例,缓存结果)。
  • 更改或禁用聊天界面中的各个按钮:submit_btnretry_btnundo_btnclear_btn
  • 自定义ChatInterface中的gr.Chatbotgr.Textbox等组件
import gradio as gr

def yes_man(message, history):
    if message.endswith("?"):
        return "Yes"
    else:
        return "Ask me anything!"

gr.ChatInterface(
    yes_man,
    chatbot=gr.Chatbot(height=300,placeholder="<strong>Your Personal Yes-Man</strong><br>Ask Me Anything"),
    textbox=gr.Textbox(placeholder="Ask me a yes or no question", container=False, scale=7),
    title="Yes Man",
    description="Ask Yes Man any question",
    theme="soft",
    examples=["Hello", "Am I cool?", "Are tomatoes vegetables?"],
    cache_examples=True,
    retry_btn=None,
    undo_btn="Delete Previous",
    clear_btn="Clear",
).launch()

在这里插入图片描述

1.4 添加多模态功能

  你可以向聊天机器人添加多模态功能。例如,你可能希望用户能够轻松上传图片或文件,并向聊天机器人提问。在gr.ChatInterface中设置multimodal=True可以使聊天机器人“多模态化”。

  当multimodal=True时,函数fn的签名会稍微变化。你的函数的第一个参数应该接受一个包含提交文本和上传文件的字典,比如{"text": "user input", "file": ["file_path1", "file_path2", ...]}。同样,你提供的任何示例也应该是这种形式的字典。函数返回值不变,仍旧返回一个单一的字符串消息。

import gradio as gr
import time

def count_files(message, history):
    num_files = len(message["files"])
    return f"You uploaded {num_files} files"

demo = gr.ChatInterface(fn=count_files, examples=[{"text": "Hello", "files": []}], title="Echo Bot", multimodal=True)

demo.launch()

在这里插入图片描述

✍️ 提示:如果你想自定义多模态聊天机器人的文本框的UI/UX,你应该传递一个gr.MultimodalTextbox实例到ChatInterface的textbox参数,而不是gr.Textbox实例。

1.5 通过additional_inputs添加额外组件

  通过additional_inputs参数可以添加额外的输入组件,例如,添加一个系统提示的文本框,或设置聊天机器人响应中token数量的滑块。以下是更具体的说明:

  • additional_inputs 参数接受一个组件或组件列表,你可以直接传递组件实例,或使用它们的字符串快捷方式(比如用“textbox”代替gr.Textbox()
  • 如果传递的组件尚未渲染,会出现在聊天机器人(和任何示例)下面的折叠面板中(gr.Accordion(),可以打开/折叠)。可以使用additional_inputs_accordion_name参数设置此折叠面板的标签

  下面创建一个带有额外输入参数的聊天机器人界面,用户可以在折叠面板的系统提示语文本框中输入自定义提示语,使用滑块设置响应的最大token数量。聊天机器人根据用户输入的消息和系统提示语生成响应,并逐字符显示响应内容,模拟打字效果。

import gradio as gr
import time

def echo(message, history, system_prompt, tokens):
    response = f"System prompt: {system_prompt}\n Message: {message}."
    for i in range(min(len(response), int(tokens))):
        time.sleep(0.05)
        yield response[: i + 1]


demo = gr.ChatInterface(
    echo,chatbot=gr.Chatbot(height=150),
    additional_inputs=[
        gr.Textbox("You are helpful AI.", label="System Prompt"),
        gr.Slider(10, 100),
    ],
)

if __name__ == "__main__":
    demo.queue().launch()

在这里插入图片描述

  ChatInterface的input参数通常用于主要的用户输入。而代码中的system_prompttokens是辅助配置,把这些参数放在additional_inputs中,可以与用户输入明确区分,让UI界面更清晰。

  如果组件已经在父gr.Blocks()中被渲染,则不会在折叠面板中重新渲染。这提供了灵活性,可以决定输入组件的布局位置。例如,我们可以将gr.Textbox()放在聊天机器人UI的顶部,同时将滑块放在下面:

import gradio as gr
import time

def echo(message, history, system_prompt, tokens):
    response = f"System prompt: {system_prompt}\n Message: {message}."
    for i in range(min(len(response), int(tokens))):
        time.sleep(0.05)
        yield response[: i+1]

with gr.Blocks() as demo:
    system_prompt = gr.Textbox("You are helpful AI.", label="System Prompt")
    slider = gr.Slider(10, 100, render=False)  # 设置滑块组件不渲染

    gr.ChatInterface(
        echo, additional_inputs=[system_prompt, slider],
    )

demo.launch()

在这里插入图片描述

如果需要创建更自定义的内容,最好使用底层的gr.Blocks() API构建聊天机器人UI(见下一章节)

1.6 直接添加 Gradio 组件

  在Gradio的Chatbot组件中,可以使用许多核心的Gradio组件(如gr.Imagegr.Plotgr.Audiogr.HTML),使聊天机器人可以返回更丰富的多媒体内容。只需从你的函数中返回这些组件之一,就可以在gr.ChatInterface中使用它们。

  下面是一个示例,用户可以通过输入框请求播放特定艺术家的音乐。如果用户输入有效信息,聊天机器人将返回一个音频文件的链接并播放;如果输入无效,聊天机器人会提示用户提供艺术家名称。

import gradio as gr

def fake(message, history):
    if message.strip():
        return gr.Audio("https://github.com/gradio-app/gradio/raw/main/test/test_files/audio_sample.wav")
    else:
        return "Please provide the name of an artist"

gr.ChatInterface(
    fake, 
    textbox=gr.Textbox(placeholder="Which artist's music do you want to listen to?", scale=7),
    chatbot=gr.Chatbot(placeholder="Play music by any artist!"),
).launch()

  textbox组件用于指定用户消息输入框,而chatbot组件用于指定聊天机器人的显示区域,都是主要的聊天功能组件。

1.7 通过 API 使用聊天机器人

1.7.1 调用托管的聊天机器人

  即使你在Hugging Face Spaces或其他地方托管了你的Gradio聊天机器人,可以通过一个简单的API在/chat端点与其进行交互。端点只需要用户的消息(以及可能的额外输入,如果你在创建聊天机器人时使用了additional_inputs参数),API内部会自动跟踪到目前为止发送的所有消息,以便生成上下文相关的响应。要调用这个API端点,可以使用 Gradio Python ClientGradio JS client

  Gradio Python client可以让我们非常容易地将任何Gradio应用当作API来使用。以托管在Hugging Face Spaces的abidlabs/whisper为例,这个应用可以从麦克风录制音频文件并进行转录。通过Gradio Python client,可以编程方式地使用这个Gradio应用来转录音频文件。

在这里插入图片描述

# pip install --upgrade gradio_client

from gradio_client import Client, file

# 创建客户端实例,指向托管的Gradio应用
client = Client("abidlabs/whisper")

# 使用Gradio应用的API来转录音频文件
client.predict(
    audio=file("audio_sample.wav")
)

>> "This is a test of the whisper speech recognition model."
1.7.2 使用Langchain构建流式聊天机器人
from langchain.chat_models import ChatOpenAI
from langchain.schema import AIMessage, HumanMessage
import openai
import gradio as gr

os.environ["OPENAI_API_KEY"] = "sk-..."  # Replace with your key

llm = ChatOpenAI(temperature=1.0, model='gpt-3.5-turbo-0613')

def predict(message, history):
    history_langchain_format = []
    for human, ai in history:
        history_langchain_format.append(HumanMessage(content=human))
        history_langchain_format.append(AIMessage(content=ai))
    history_langchain_format.append(HumanMessage(content=message))
    gpt_response = llm(history_langchain_format)
    return gpt_response.content

gr.ChatInterface(predict).launch()

这段代码的逻辑与之前的类似,只是在predict函数中,通过LangChain加载了OpenAI的大语言模型来生成用户响应。

1.7.3 使用openai 构建流式聊天机器人

我们也可以直接使用openai来进行构建:

from openai import OpenAI
import gradio as gr

api_key = "sk-..."  # Replace with your key
client = OpenAI(api_key=api_key)

def predict(message, history):
    history_openai_format = []
    for human, assistant in history:
        history_openai_format.append({"role": "user", "content": human })
        history_openai_format.append({"role": "assistant", "content":assistant})
    history_openai_format.append({"role": "user", "content": message})
  
    response = client.chat.completions.create(model='gpt-3.5-turbo',
    messages= history_openai_format,
    temperature=1.0,
    stream=True)

    partial_message = ""
    for chunk in response:
        if chunk.choices[0].delta.content is not None:
              partial_message = partial_message + chunk.choices[0].delta.content
              yield partial_message

gr.ChatInterface(predict).launch()
1.7.4 使用本地开源LLM进行构建

  在更多情况下,你可能希望在本地运行聊天机器人。下面是一个使用Hugging Face上的
RedePajama model的示例,这需要GPU。

import gradio as gr
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, StoppingCriteria, StoppingCriteriaList, TextIteratorStreamer
from threading import Thread

tokenizer = AutoTokenizer.from_pretrained("togethercomputer/RedPajama-INCITE-Chat-3B-v1")
model = AutoModelForCausalLM.from_pretrained("togethercomputer/RedPajama-INCITE-Chat-3B-v1", torch_dtype=torch.float16)
model = model.to('cuda:0')

class StopOnTokens(StoppingCriteria):
    def __call__(self, input_ids: torch.LongTensor, scores: torch.FloatTensor, **kwargs) -> bool:
        stop_ids = [29, 0]
        for stop_id in stop_ids:
            if input_ids[0][-1] == stop_id:
                return True
        return False

def predict(message, history):  							# 接受用户的输入消息和聊天历史记录
    history_transformer_format = history + [[message, ""]]  # 将聊天历史转换为 Transformer 模型所需的格式
    stop = StopOnTokens()

    messages = "".join(["".join(["\n<human>:"+item[0], "\n<bot>:"+item[1]])
                for item in history_transformer_format])	# 将新消息附加到历史记录中

    model_inputs = tokenizer([messages], return_tensors="pt").to("cuda")
    # 创建 TextIteratorStreamer 对象用于逐步生成文本
    streamer = TextIteratorStreamer(tokenizer, timeout=10., skip_prompt=True, skip_special_tokens=True)
    generate_kwargs = dict(
        model_inputs,
        streamer=streamer,
        max_new_tokens=1024,
        do_sample=True,
        top_p=0.95,
        top_k=1000,
        temperature=1.0,
        num_beams=1,
        stopping_criteria=StoppingCriteriaList([stop])
        )
    # 通过多线程方式调用 model.generate 生成文本,并将结果逐步传给 streamer
    t = Thread(target=model.generate, kwargs=generate_kwargs)
    t.start()
	
	# 生成过程中逐步产出部分消息 (partial_message),实现流式输出
    partial_message = ""
    for new_token in streamer:
        if new_token != '<':
            partial_message += new_token
            yield partial_message

gr.ChatInterface(predict).launch()

  代码中, StopOnTokens:继承自 StoppingCriteria,用于实现自定义的停止条件。在生成文本时,如果生成的 token 序列中包含 ID 为 29 或 0 的标记,生成过程将终止( <pad> 或结束标记<eos>)。__call__方法会在生成每个新 token 时被调用,以检查是否应该停止生成。

二、 使用Blocks 创建自定义聊天机器人

2.1 快速入门

  Gradio 提供了两种不同的方式来创建聊天机器人:Gradio Blocksgr.ChatInterface。Gradio Blocks是更底层的API,允许开发者完全自定义聊天机器人的界面和行为。开发者可以自定义各种组件的排列、样式和交互方式,可以自定义事件处理逻辑,适合需要复杂布局或特殊功能的应用。

  下面我们先使用 gradio.Blocks 类来构建一个简单的聊天机器人示例,这个机器人会随机响应"How are you?", "I love you", "I'm very hungry"

import gradio as gr
import random
import time

with gr.Blocks() as demo:
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.ClearButton([msg, chatbot])

    def respond(message, chat_history):
    	# chat_history是一个包含(message, bot_message)二元组的列表,表示历史对话记录
        bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
        chat_history.append((message, bot_message))
        time.sleep(2)									# 模拟机器人响应的延迟
        return "", chat_history							# 返回一个二元组,第一个元素是一个空字符串,用于清空输入框

    msg.submit(respond, [msg, chatbot], [msg, chatbot])

demo.launch()

这里有三个 Gradio 组件:

  1. Chatbot:存储了整个对话历史,作为用户和机器人响应对的列表。
  2. Textbox:用户输入框,按回车或提交按钮以触发聊天机器人响应。
  3. ClearButton:清除 TextboxChatbot

   respond函数接收整个聊天机器人的历史记录,并追加一个随机消息。在等待 2 秒后返回更新后的聊天历史。函数在返回时也会清除文本框。在实际应用中,你可以用更复杂的函数替换 respond,比如调用预训练模型或 API 来生成响应。

2.2 为聊天机器人添加流式响应

  我们可以通过几种方式改进上面的聊天机器人用户体验。首先,我们可以流式传输响应,这样用户可以看到实时反馈。其次,我们可以在生成机器人的响应时也显示用户的消息。

import gradio as gr
import random
import time

with gr.Blocks() as demo:
    chatbot = gr.Chatbot()
    msg = gr.Textbox()
    clear = gr.Button("Clear")

    def user(user_message, history):
        return "", history + [[user_message, None]]

    def bot(history):
        bot_message = random.choice(["How are you?", "I love you", "I'm very hungry"])
        history[-1][1] = ""
        for character in bot_message:
            history[-1][1] += character
            time.sleep(0.05)
            yield history

    msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False).then(
        bot, chatbot, chatbot
    )
    clear.click(lambda: None, None, chatbot, queue=False)
    
demo.queue()
demo.launch()
  • user函数 :处理用户输入,将用户消息(user_message, None)添加到聊天历史中(其中 None 表示机器人尚未响应),同时清空输入框。此方法还使输入字段非交互式,以便用户在聊天机器人响应时无法发送另一条消息。

  • bot 函数:生成机器人的回复,并逐个字母地显示出来,每次暂停0.05s,模拟打字效果。这里没有创建新消息,只是用机器人的响应替换之前创建的 None 消息。Gradio 会自动将任何包含 yield 关键字的函数转换为流式输出界面

.then()方法:主要功能是链式连接多个事件,实现复杂的交互逻辑。

  • 顺序执行:使得一个事件在另一个事件完成后执行
  • 数据传递:一个事件的输出可以作为下一个事件的输入,使得数据流在整个链条中保持连贯。
  • 异步处理:可以处理异步操作,例如等待某个操作完成后再执行下一个操作。这对于处理API调用或复杂计算非常有用。
  • 提高可读性和维护性:通过链式调用,使代码更具可读性和结构化,方便理解和维护。

   .then()方法主要参数为(fn,input,output,queue),其中默认queue=True。通过设置queue=False可以禁用队列,以便立即执行。本例中,当用户提交消息时,通过.then()方法将三个事件链式连接:

  • msg.submit(user, [msg, chatbot], [msg, chatbot], queue=False):第一个事件,调用user函数,处理用户输入,更新聊天记录并清空输入框。
  • .then(bot, chatbot, chatbot):第二个事件,调用bot函数,更新聊天历史,用机器人的回复替换之前创建的None消息,并逐字显示回复内容。
  • clear.click(lambda: None, None, chatbot, queue=False):第三个事件,清空聊天窗口。输入框重新变为可交互状态,允许用户继续输入消息。
    • 触发回调函数lambda: None
    • 回调函数返回None,不进行任何操作。
    • 输出目标是chatbot,因此Gradio清除chatbot组件的内容
    • queue=False确保这个操作立即执行,不会被排队。

最后,我们通过运行 demo.queue() 启用队列功能,以便实现中间流式输出的功能。

2.3 添加点赞和点踩功能

可以为聊天机器人添加点赞和点踩功能,以便用户可以对每条机器人的回复进行评价。通过附加.like()事件,就可以在每条机器人消息旁边自动显示点赞和点踩图标。

  .like()方法需要传入一个函数,当用户点击图标时调用该函数。该函数应有一个gr.LikeData类型的参数,Gradio会自动传入包含被点赞或点踩消息信息的对象。下面是一个简单的示例:

import gradio as gr

def greet(history, input):
    return history + [(input, "Hello, " + input)]

def vote(data: gr.LikeData):
    if data.liked:
        print("You upvoted this response: " + data.value)
    else:
        print("You downvoted this response: " + data.value)
    

with gr.Blocks() as demo:
    chatbot = gr.Chatbot()
    textbox = gr.Textbox()
    textbox.submit(greet, [chatbot, textbox], [chatbot])    # 调用greet函数更新聊天记录,显示在聊天机器人组件中
    chatbot.like(vote, None, None)  						# 绑定vote函数。当用户点击点赞或点踩图标时,调用vote函数处理投票数据。
    
demo.launch()
  • vote:投票函数,接收一个gr.LikeData类型的参数data,包含关于被点赞或点踩消息的信息。根据data.liked的值,打印用户是否点赞或点踩了该消息。
  • chatbot.like(vote, None, None)
    • inputs: None 表示回调函数 vote 不需要从其他组件接收输入。Gradio 会自动传递 gr.LikeData 对象给 vote 函数。
    • outputs: None 表示回调函数 vote 不需要更新任何组件的输出。因为点赞或点踩操作只是记录用户的反馈,没有必要更新界面上的其他组件。

2.4 添加 Markdown、图像、音频或视频

  gr.Chatbot组件支持一部分Markdown格式,包括加粗、斜体和代码。例如,可以用加粗的文本回复用户消息,如“That’s cool!”。

def bot(history):
    response = "**That's cool!**"
    history[-1][1] = response
    return history

  gr.Chatbot可以处理媒体文件,如图片、音频和视频。使用MultimodalTextbox组件,可以方便地上传各种类型的媒体文件到聊天机器人。传递媒体文件的格式是一个包含两个字符串的元组,如(filepath, alt_text)。后者是可选的,所以也可以只传一个包含单个元素的元组(filepath,)

def add_message(history, message):
    for x in message["files"]:
        history.append(((x["path"],), None))  
    if message["text"] is not None:
        history.append((message["text"], None))
    return history, gr.MultimodalTextbox(value=None, interactive=False, file_types=["image"])

  下面是完整的示例代码,我们使用MultimodalTextbox组件创建了一个带有多模态文本框的多模态聊天机器人。

import gradio as gr
import os
import plotly.express as px

# Chatbot demo with multimodal input (text, markdown, LaTeX, code blocks, image, audio, & video). Plus shows support for streaming text.

def random_plot():
    df = px.data.iris()
    fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species",
                    size='petal_length', hover_data=['petal_width'])
    return fig

def print_like_dislike(x: gr.LikeData):
    print(x.index, x.value, x.liked)

def add_message(history, message):
    for x in message["files"]:
        history.append(((x,), None))
    if message["text"] is not None:
        history.append((message["text"], None))
    return history, gr.MultimodalTextbox(value=None, interactive=False)

def bot(history):
    history[-1][1] = "Cool!"
    return history

fig = random_plot()

with gr.Blocks(fill_height=True) as demo:
    chatbot = gr.Chatbot(
        elem_id="chatbot",
        bubble_full_width=False,
        scale=1,
    )

    chat_input = gr.MultimodalTextbox(interactive=True,
                                      file_count="multiple",
                                      placeholder="Enter message or upload file...", show_label=False)

    chat_msg = chat_input.submit(add_message, [chatbot, chat_input], [chatbot, chat_input])
    bot_msg = chat_msg.then(bot, chatbot, chatbot, api_name="bot_response")
    bot_msg.then(lambda: gr.MultimodalTextbox(interactive=True), None, [chat_input])

    chatbot.like(print_like_dislike, None, None)

demo.queue()
demo.launch()

三、通过Gradio Apps创建 Discord 机器人(略)

  • 15
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Building prefix dict from the default dictionary ... DEBUG:jieba:Building prefix dict from the default dictionary ... Loading model from cache C:\Users\LY-AI\AppData\Local\Temp\jieba.cache DEBUG:jieba:Loading model from cache C:\Users\LY-AI\AppData\Local\Temp\jieba.cache Loading model cost 0.717 seconds. DEBUG:jieba:Loading model cost 0.717 seconds. Prefix dict has been built successfully. DEBUG:jieba:Prefix dict has been built successfully. C:\Users\LY-AI\Desktop\AI\vits-uma-genshin-honkai\python3.9.13\3.9.13\lib\site-packages\gradio\processing_utils.py:183: UserWarning: Trying to convert audio automatically from float32 to 16-bit int format. warnings.warn(warning.format(data.dtype)) Traceback (most recent call last): File "C:\Users\LY-AI\Desktop\AI\vits-uma-genshin-honkai\python3.9.13\3.9.13\lib\site-packages\gradio\routes.py", line 442, in run_predict output = await app.get_blocks().process_api( File "C:\Users\LY-AI\Desktop\AI\vits-uma-genshin-honkai\python3.9.13\3.9.13\lib\site-packages\gradio\blocks.py", line 1392, in process_api data = self.postprocess_data(fn_index, result["prediction"], state) File "C:\Users\LY-AI\Desktop\AI\vits-uma-genshin-honkai\python3.9.13\3.9.13\lib\site-packages\gradio\blocks.py", line 1326, in postprocess_data prediction_value = block.postprocess(prediction_value) File "C:\Users\LY-AI\Desktop\AI\vits-uma-genshin-honkai\app.py", line 42, in audio_postprocess return gr_processing_utils.encode_url_or_file_to_base64(data["name"]) AttributeError: module 'gradio.processing_utils' has no attribute 'encode_url_or_file_to_base64'
07-24

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

神洛华

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

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

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

打赏作者

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

抵扣说明:

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

余额充值