MCDR-让你的MC服务器接入AI

[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 OpenAI

PLUGIN_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}")
# DeepSeek回复
source.reply(f"§a[DeepSeek]§r {send_message_to_ds(msg)}")
# 广播给所有玩家
# source.get_server().execute(f'tellraw @a {{"text":"§6[系统广播]§r {source.player} 对DeepSeek说:{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是 msgcontext['msg'] 就是 <message>

send_message_to_ds 参考ds api文档

image-20250301201042941

这样实现最简单的单次对话。将.py文件放入插件文件夹并重载,尝试第一次对话

image-20250301201259105

多轮对话

参考之前调用 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描述如下

image-20250301202714803

如果把 GreedyText('message').runs(get_user_content) 放到第一个注册,那么会覆盖后面所有待注册的命令,也就是说,后面所有命令都将无效。

为了让结构更清晰,我们可以定义一个数据管理类,专门对数据进行操作。

MCDR 插件数据保存位置为根目录下的 config 文件夹下

1
path = './config/DeepSeek/' # 这里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)
# 获取拼接好的message
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() # 获取拼接好的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

image-20250301203949118

image-20250301204154692

image-20250301204236532

image-20250301204301717

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 OpenAI

PLUGIN_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 OpenAI
import os
import json
PLUGIN_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}")
# DeepSeek回复
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)
# 广播给所有玩家
# source.get_server().execute(f'tellraw @a {{"text":"§6[系统广播]§r {source.player} 对DeepSeek说:{message}"}}')
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

  • 2025.3.1 v1.0.0发布

  • 2025.3.2 v1.1.0发布,更改架构为多文件,允许使用配置文件