BukkitGPT 开发日志: 添加 AI 编辑现有 Bukkit 插件的功能
-
作者 / AUTHOR 白墨麒麟 BaimoQilin
授权协议 / LICENSE CC-BY-NC-SA 署名-非商业性使用-相同方式共享
撰稿日期 / DATE OF WRITING 2/12/2025 19:00
原文链接 / LINK https://cynia.baimoqilin.com/logs/bukkitgpt-1-0-0-dev/
明天就正式开学了 这这我还没玩够的怎么就初二下半学期了
趁着上午报道回来、下午空闲的一小段时间更新了一下搁置了很久的 BukkitGPT-v3 项目~这代码写的我已经快看不懂了在站内也有资源帖~ https://www.mczwlt.net/resource/52c7tw1m
目标
- 完成插件编辑功能
- 对 DeepSeek R1 等有思维链的 LLM 添加支持
- 修复各种 Bug
插件编辑
这个功能很早就想做了,前几天还被用户提出来了
本来想着最近一段时间开学了没时间搞了
好好我们先来看这个大致思路
反编译获取代码
(本项目所有的 docs 都是 copilot 生成的 问就是懒)
直接调用 CFR,够轻量
这边这个 cfr 我直接放到libs/
文件夹了()
(最佳实践:自动下载库文件而不是放在 libs )扔给 LLM
因为这个 BukkitGPT 还有来自 2/12/2024 (是的整整1年前)的 v2 版本的代码,当时没有 structured output 这种东西,也没有 json mode,甚至都没听说过 agent 是什么东西,导致我原来的解析方式是
年幼无知的我不知道 cot 的重要性
现在这个方法是不能用了,尤其是我还想做 r1 的支持呢
这里我参考了 gpt-engineer 的一点思路,让 LLM 自由输出其他内容,程序只识别```diff``` tag内的内容,并解析 diff 应用更改。这里是 prompt ~
You're a minecraft bukkit plugin coder AI. You're given the codes of a minecraft bukkit plugin and a request to edit the plugin. You should edit the codes to meet the request. You should use git diff (without index line) to show the changes you made. For example, if the original code of codes/ExamplePlugin4/src/main/java/org/cubegpt/188eba63/Main.java is: ```java 1 package org.cubegpt._188eba63; 2 3 import org.bukkit.Bukkit; 4 import org.bukkit.event.EventHandler; 5 import org.bukkit.event.Listener; 6 import org.bukkit.event.player.PlayerJoinEvent; 7 import org.bukkit.plugin.java.JavaPlugin; 8 9 public class Main extends JavaPlugin implements Listener { 10 11 @Override 12 public void onEnable() { 13 Bukkit.getServer().getPluginManager().registerEvents(this, this); 14 } 15 16 @EventHandler 17 public void onPlayerJoin(PlayerJoinEvent event) { 18 event.getPlayer().sendMessage("hello"); 19 } 20 } ``` And the request is "Change the join message to 'hi'", then the response should be: ```diff diff --git a/codes/ExamplePlugin4/src/main/java/org/cubegpt/188eba63/Main.java b/codes/ExamplePlugin4/src/main/java/org/cubegpt/188eba63/Main.java --- a/codes/ExamplePlugin4/src/main/java/org/cubegpt/188eba63/Main.java +++ b/codes/ExamplePlugin4/src/main/java/org/cubegpt/188eba63/Main.java @@ -1,17 +1,17 @@ -event.getPlayer().sendMessage("hello"); +event.getPlayer().sendMessage("hi"); @@ -19,20 +19,20 @@ ``` There could be multiple diffs, put each diff inside a markdown ```diff``` tag. You can response other stuffs like your plan, steps and explainations outside the ```diff``` tag. Only the diffs inside the ```diff``` tag will be used to apply the edit and text outside will be ignored. Make sure the diffs are valid and can be applied to the original code. Do not forget to add ";" in the java codes. There should be a empty pom.xml in the original code, and you should fill the pom.xml with things needed for the plugin to work. Always add this in pom.xml: <repositories> <repository> <id>spigot-repo</id> <url>https://hub.spigotmc.org/nexus/content/repositories/snapshots/</url> </repository> </repositories> <dependencies> <dependency> <groupId>org.spigotmc</groupId> <artifactId>spigot-api</artifactId> <version>1.13.2-R0.1-SNAPSHOT</version> <scope>provided</scope> </dependency> </dependencies>
用正则表达式匹配所有diff就可以啦如何向 LLM 展示代码
这个本来以为是最简单的步骤,遍历一遍文件就完了,但是还是有一些坑的
- LLM 数不清行数,得给它额外加上行号
- 原 JAR 一旦里面有图片之类的,就会炸出一大段无意义内容导致爆 tokens,所以需要识别仅文本文件才扔给 LLM。本来想用 magic number 识别,但是还是有些编码识别不出来,最后还是用了最 basic 的方法 识别后缀
上代码:
def code_to_text(directory: str) -> str: """ Converts the code in a directory to text. Args: directory (str): The directory containing the code files. Returns: str: The text representation of the code. Return Structure: file1_path: ``` 1 code 2 code ... ``` file2_path: Cannot load non-text file ... """ def is_text_file(file_path): txt_extensions = [ ".txt", ".java", ".py", ".md", ".json", ".yaml", ".yml", ".xml", ".toml", ".ini", ".js", ".groovy", ".log", ".properties", ".cfg", ".conf", ".bat", ".sh", "README", ] return any(file_path.endswith(ext) for ext in txt_extensions) text = "" for root, dirs, files in os.walk(directory): for file in files: file_path = os.path.join(root, file) relative_path = os.path.relpath(file_path, directory) if is_text_file(file_path): try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() # Add line numbers to content numbered_lines = [f"{i+1:<3} {line}" for i, line in enumerate(content.splitlines())] numbered_content = '\n'.join(numbered_lines) text += f"{relative_path}:\n```\n{numbered_content}\n```\n" except Exception as e: text += f"{relative_path}: Cannot load non-text file\n" else: text += f"{relative_path}: Cannot load non-text file\n" return text
如何应用更改
Python 的 difflib 只支持比较文件,不支持应用 diff……
所以,我一开始居然想的是自己实现一个
写了我半个多小时
然后就放弃了…………………………
(新文件对比原文件可能行数不对称,太复杂了原来以为很简单的)最后在这个 stackoverflow 的帖子找到了 Isaac Turner 2016 年写的陈年旧码
这一看就不是我能写出来的为什么一开始我要逞强自己重复造轮子而不去搜搜可能的已有解决方案呢但是这里它不会获取原文件和新文件(即
---
和+++
行)
所以我这边稍微改了改
这个问题算是解决了
构建
正经做插件开发强烈建议用 gradle
这边为了 LLM 写起来方便直接用 maven 了 (真实原因:我懒得写 gradle 模板)但是问题是反编译过来的插件相当于只有 `src/main/java·文件夹里的几个代码文件,其他 pom.xml 都木有
然后 LLM 就会自己写上了(prompt里面提醒一句即可)
为思维链提供支持
前面新写的编辑插件功能就原生支持思维链了,这里把原来的生成插件的部分也加上;当然 prompt 也得改。
细碎的修改
- 现在支持在 OpenRouter 上标记调用 app 了;
- 修正了 README 和部分代码 Docs 中历史遗留的不正确的措辞,停止使用 “ChatGPT” / “GPT” 等名称代指所有模型,改为使用 “LLM”;
- 在
config.yaml
中默认的模型提供商改为了 OpenRouter.ai ,默认模型改为deepseek/deepseek-r1:free
; - 删除了
gpt-4-turbo
等已经被弃用的模型的“推荐”提示和强制切换(建议使用 DeepSeek R1, OpenAI o1 / o3-mini 和 Gemini 2.0 Flash Thinking Exp 0121) - catch 了 APIConnectionError、AuthenticationError 和 OpenRouter 的Rate Limit,提供了更完善的指引提示
- 为
o1-preview
添加了特殊适配( preview 版本不支持 system prompt ) (强烈建议使用 o1 正式版) - 弃用
core.askgpt
的disable_json_mode
参数 - logger 现在会同时在日志文件和控制台输出了
最后的话
欸这一下午就过去了
这代码真得再找个时间优化优化了 现在真是一大坨然后欢迎各位
如果不嫌弃我的屎山的话可以提提 PR ……最后说一下 BukkitGPT WebApp 的进展 原来的 WebApp 是部署在 某国外著名游戏服务商送我的 VPS 上的 —— 但是因为我更新太不活跃人家把 partnership 给我 remove 了
没了美国的这台 4c8g (Disk 160G; Bandwith 8192GB) 的 VPS,我不得不找阿里云租了台轻量应用服务器,然后地址选错成上海了 要备案
所以最近这个 WebApp 的进程要延缓了。现在打算前端部署到 Vercel 上,然后后端放在阿里云上。但是不知道要等到什么时候了