[MCDR] 让你的MC服务器接入AI
DeepSeek API终于恢复正常了。在ChatGPT刚出的时候就有过将GPT加入到MC服务器的想法,但是受限于当时的Java水平和财力,放弃了这个想法。今天下午看到DeepSeek API 开放充值了,决定实现一下曾经的想法
这篇文章只是v1.0.0版,更新请移步github
github
DeepSeek 开放平台
DeepSeek API文档
MCDR 插件开发
其实之前调用Ollama API 时积累了很多经验,核心交互基本都是照搬
个人感觉MCDR官方文档写的过于简略了,写插件时磕磕碰碰的…
首先到ds开放平台申请一个 API key,这里不做演示了
基础核心逻辑为:获取用户输入
→发给ai,获得响应
→把响应发给用户
单次对话
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 from mcdreforged.api.all import *from openai import OpenAIPLUGIN_METADATA = { 'id' : 'chat_with_ds' , 'version' : '1.0.0' , 'name' : 'Chat with DeepSeek' , 'description' : 'Let your server chat with DeepSeek!' , 'author' : 'gubai' , } ds_key = "your-key" def on_load (server: ServerInterface, old_module ): server.register_help_message('!!ds <message>' , '与DeepSeek对话' ) server.register_command( Literal ('!!ds' ).then( GreedyText('msg' ).runs(get_user_content) ) ) def get_user_content (source: CommandSource, context: CommandContext ): msg = context['msg' ] if source.is_player: source.reply(f"§a[DeepSeek]§r 收到你的消息:{msg} " ) source.reply(f"§a[DeepSeek]§r {send_message_to_ds(msg)} " ) else : source.reply("§c该命令只能由玩家使用" ) def send_message_to_ds (msg: str ): client = OpenAI(api_key=ds_key, base_url="https://api.deepseek.com" ) response = client.chat.completions.create( model="deepseek-chat" , messages=[ {"role" : "system" , "content" : "You are a helpful assistant" }, {"role" : "user" , "content" : msg}, ], stream=False ) return response.choices[0 ].message.content
逻辑很简单,在入口注册命令 !!ds <message>
,其中<message>
是玩家向ai发送的内容,调用 get_user_content
,将<message>
作为参数传入。
GreedyText('msg')
可以理解为 <message>
的key是 msg
。context['msg']
就是 <message>
。
send_message_to_ds
参考ds api文档
这样实现最简单的单次对话。将.py文件放入插件文件夹并重载,尝试第一次对话
多轮对话
参考之前调用 Ollama API的经历,实际上就是把每次的输入和输出拼起来一起发给ai
为了保证每次关服不丢失历史数据,且让玩家数据隔离,可以通过文件存储,其中文件以玩家名命名~~(为什么不用uuid?因为不知道MCDR uuid的接口是什么…)~~
既然要实现多轮对话,那么查看对话历史和清空对话历史便是标配了。先在入口注册这几个命令。为了区分,用 dsp
作为主命令~~(deepseek plus)~~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 def on_load (server: ServerInterface, old_module ): server.register_help_message('!!ds <message>' , '与DeepSeek对话' ) server.register_command( Literal ('!!dsp' ).then( Literal ('help' ).runs(get_help) ).then( Literal ('history' ).runs(get_history) ).then( Literal ('clear' ).runs(clear_history) ).then( GreedyText('message' ).runs(get_user_content) ) ) def get_help (source: CommandSource ): source.reply("§a[DeepSeek]§r 命令:\n" "§6!!dsp help§r 查看帮助\n" "§6!!dsp history§r 查看历史消息\n" "§6!!dsp clear§r 清空历史消息\n" "§6!!dsp <message>§r 与DeepSeek对话" ) def get_history (source: CommandSource ): ... def clear_history (source: CommandSource ): ... def get_user_content (source: CommandSource, context: CommandContext ): ...
一定要注意的是注册命令顺序,help
history
clear
三个是并列的,但 GreedyText
属于 贪婪的文本参数 ,MCDR描述如下
如果把 GreedyText('message').runs(get_user_content)
放到第一个注册,那么会覆盖后面所有待注册的命令,也就是说,后面所有命令都将无效。
为了让结构更清晰,我们可以定义一个数据管理类,专门对数据进行操作。
MCDR 插件数据保存位置为根目录下的 config
文件夹下
1 path = './config/DeepSeek/'
假设一个玩家叫 name,那么完整路径可以写为
1 path = './config/DeepSeek/' + name + '.json'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 class DataManager : def __init__ (self, source: CommandSource, name: str ): self .initial_message = [{"role" : "system" , "content" : "你是一个Minecraft助手,负责解答玩家关于Minecraft相关问题。解答问题时,要联系上下文,给出精确的答案。" }] self .source = source self .name = name self .history_path = './config/DeepSeek/' + name + '.json' if not os.path.exists(self .history_path): os.makedirs(os.path.dirname(self .history_path), exist_ok=True ) with open (self .history_path, 'w' ) as file: json.dump(self .initial_message, file) def get_send_message (self ): with open (self .history_path, 'r' ) as file: messages = json.load(file) return messages def get_history (self ): with open (self .history_path, 'r' ) as file: messages = json.load(file) history = "\n--------------------history--------------------\n" for index, message in enumerate (messages): history += f"##{index} ##: {message['role' ]} : {message['content' ]} \n" history += "-----------------------------------------------\n" self .source.reply(history) def add_message (self, role: str , user_input: str ): with open (self .history_path, 'r' ) as file: messages = json.load(file) messages.append({"role" : role, "content" : user_input}) with open (self .history_path, 'w' ) as file: json.dump(messages, file) def clear_history (self ): with open (self .history_path, 'w' ) as file: json.dump(self .initial_message, file) self .source.reply("Clear history" )
因为向ai发送的请求中,message是一个列表,所以我们存储也应当是一个列表,这样只需要简单读取发送即可,而不需要考虑格式问题。关于 role
system
等参数,参考调用Ollama API那篇文章
这样命令的函数便很好写了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 def get_history (source: CommandSource ): player_data = DataManager(source, source.player) player_data.get_history() def clear_history (source: CommandSource ): player_data = DataManager(source, source.player) player_data.clear_history() def get_user_content (source: CommandSource, context: CommandContext ): message = context['message' ] if source.is_player: player_data = DataManager(source, source.player) player_data.add_message("user" , message) source.reply(f"§a[DeepSeek]§r 收到你的消息:{message} " ) send_message = player_data.get_send_message() response = send_message_to_ds(send_message) source.reply(f"§a[DeepSeek]§r {response} " ) player_data.add_message("assistant" , response) else : source.reply("§c该命令只能由玩家使用" ) def send_message_to_ds (send_message: str ): client = OpenAI(api_key=key, base_url="https://api.deepseek.com" ) response = client.chat.completions.create( model="deepseek-chat" , messages=send_message, stream=False ) return response.choices[0 ].message.content
ALL CODES
单次对话
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from mcdreforged.api.all import *from openai import OpenAIPLUGIN_METADATA = { 'id' : 'chat_with_ds' , 'version' : '1.0.0' , 'name' : 'Chat with DeepSeek' , 'description' : 'Let your server chat with DeepSeek!' , 'author' : 'gubai' , } ds_key = "YOUR_API_KEY" def on_load (server: ServerInterface, old_module ): server.register_help_message('!!ds <message>' , '与DeepSeek对话' ) server.register_command( Literal ('!!ds' ).then( GreedyText('msg' ).runs(get_user_content) ) ) def get_user_content (source: CommandSource, context: CommandContext ): msg = context['msg' ] if source.is_player: source.reply(f"§a[DeepSeek]§r 收到你的消息:{msg} " ) source.reply(f"§a[DeepSeek]§r {send_message_to_ds(msg)} " ) else : source.reply("§c该命令只能由玩家使用" ) def send_message_to_ds (msg: str ): client = OpenAI(api_key=ds_key, base_url="https://api.deepseek.com" ) response = client.chat.completions.create( model="deepseek-chat" , messages=[ {"role" : "system" , "content" : "You are a helpful assistant" }, {"role" : "user" , "content" : msg}, ], stream=False ) return response.choices[0 ].message.content
多轮对话
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 from mcdreforged.api.all import *from openai import OpenAIimport osimport jsonPLUGIN_METADATA = { 'id' : 'chat_with_AI' , 'version' : '1.0.0' , 'name' : 'Chat with AI' , 'description' : 'Let your server chat with AI' , 'author' : 'gubai' , } key = "YOUR_API_KEY" class DataManager : def __init__ (self, source: CommandSource, name: str ): self .initial_message = [{"role" : "system" , "content" : "你是一个Minecraft助手,负责解答玩家关于Minecraft相关问题。解答问题时,要联系上下文,给出精确的答案。" }] self .source = source self .name = name self .history_path = './config/DeepSeek/' + name + '.json' if not os.path.exists(self .history_path): os.makedirs(os.path.dirname(self .history_path), exist_ok=True ) with open (self .history_path, 'w' ) as file: json.dump(self .initial_message, file) def get_send_message (self ): with open (self .history_path, 'r' ) as file: messages = json.load(file) return messages def get_history (self ): with open (self .history_path, 'r' ) as file: messages = json.load(file) history = "\n--------------------history--------------------\n" for index, message in enumerate (messages): history += f"##{index} ##: {message['role' ]} : {message['content' ]} \n" history += "-----------------------------------------------\n" self .source.reply(history) def add_message (self, role: str , user_input: str ): with open (self .history_path, 'r' ) as file: messages = json.load(file) messages.append({"role" : role, "content" : user_input}) with open (self .history_path, 'w' ) as file: json.dump(messages, file) def clear_history (self ): with open (self .history_path, 'w' ) as file: json.dump(self .initial_message, file) self .source.reply("Clear history" ) def on_load (server: ServerInterface, old_module ): server.register_help_message('!!ds <message>' , '与DeepSeek对话' ) server.register_command( Literal ('!!dsp' ).then( Literal ('help' ).runs(get_help) ).then( Literal ('history' ).runs(get_history) ).then( Literal ('clear' ).runs(clear_history) ).then( GreedyText('message' ).runs(get_user_content) ) ) def get_help (source: CommandSource ): source.reply("§a[DeepSeek]§r 命令:\n" "§6!!dsp help§r 查看帮助\n" "§6!!dsp history§r 查看历史消息\n" "§6!!dsp clear§r 清空历史消息\n" "§6!!dsp <message>§r 与DeepSeek对话" ) def get_user_content (source: CommandSource, context: CommandContext ): message = context['message' ] if source.is_player: player_data = DataManager(source, source.player) player_data.add_message("user" , message) source.reply(f"§a[DeepSeek]§r 收到你的消息:{message} " ) send_message = player_data.get_send_message() response = send_message_to_ds(send_message) source.reply(f"§a[DeepSeek]§r {response} " ) player_data.add_message("assistant" , response) else : source.reply("§c该命令只能由玩家使用" ) def get_history (source: CommandSource ): player_data = DataManager(source, source.player) player_data.get_history() def clear_history (source: CommandSource ): player_data = DataManager(source, source.player) player_data.clear_history() def send_message_to_ds (send_message: str ): client = OpenAI(api_key=key, base_url="https://api.deepseek.com" ) response = client.chat.completions.create( model="deepseek-chat" , messages=send_message, stream=False ) return response.choices[0 ].message.content
ENDS
水平有限,代码写的很粗糙,但是也算是实现了曾经的愿望…
世界生成算法吞下了我的十七岁。
那封没寄出的信还在末地折跃门边缘,
漂浮如未完成的红石电路。
当第一个AI村民说出预设外的对白,
我忽然听见2022年的自己,
在矿洞深处敲打铁轨的节奏。
那些被放弃的坐标参数,
正在基岩层下重新编译春天。
Update