Skip to content

BukkitGPT 开发日志: 添加 AI 编辑现有 Bukkit 插件的功能

作者 / AUTHOR 白墨麒麟 BaimoQilin

授权协议 / LICENSE CC-BY-NC-SA 署名-非商业性使用-相同方式共享

撰稿日期 / DATE OF WRITING 2/12/2025 19:00

明天就正式开学了 这这我还没玩够的怎么就初二下半学期了

趁着上午报道回来、下午空闲的一小段时间更新了一下搁置了很久的 BukkitGPT-v3 项目~

这代码写的我已经快看不懂了

在站内也有资源帖~ https://www.mczwlt.net/resource/52c7tw1m

目标

  1. 完成插件编辑功能
  2. 对 DeepSeek R1 等有思维链的 LLM 添加支持
  3. 修复各种 Bug

插件编辑

af0ec5a0-8562-49cc-bdb4-2ce8ecef8a00-image.png 这个功能很早就想做了,前几天还被用户提出来了 本来想着最近一段时间开学了没时间搞了 好好我们先来看这个

大致思路

13a3c70c-0f3c-41bb-9139-7c1ed48d5d84-original.jpg

反编译获取代码

be47dc04-0166-487f-b7fd-258efb5c83e4-image.png (本项目所有的 docs 都是 copilot 生成的 问就是懒) 直接调用 CFR,够轻量

这边这个 cfr 我直接放到 libs/ 文件夹了()

(最佳实践:自动下载库文件而不是放在 libs )

扔给 LLM

因为这个 BukkitGPT 还有来自 2/12/2024 (是的整整1年前)的 v2 版本的代码,当时没有 structured output 这种东西,也没有 json mode,甚至都没听说过 agent 是什么东西,导致我原来的解析方式是

fbeed494-8f3c-4290-897a-8058dbe0bd79-image.png

年幼无知的我不知道 cot 的重要性

现在这个方法是不能用了,尤其是我还想做 r1 的支持呢

这里我参考了 gpt-engineer 的一点思路,让 LLM 自由输出其他内容,程序只识别```diff``` tag内的内容,并解析 diff 应用更改。

这里是 prompt ~

YAML
  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>
9403c105-5852-421b-8a16-fd84b9e62716-image.png

用正则表达式匹配所有diff就可以啦

如何向 LLM 展示代码

这个本来以为是最简单的步骤,遍历一遍文件就完了,但是还是有一些坑的

  1. LLM 数不清行数,得给它额外加上行号

  2. 原 JAR 一旦里面有图片之类的,就会炸出一大段无意义内容导致爆 tokens,所以需要识别仅文本文件才扔给 LLM。本来想用 magic number 识别,但是还是有些编码识别不出来,最后还是用了最 basic 的方法 识别后缀

上代码:

Python
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 年写的陈年旧码 58a7e349-7db2-422b-a9b3-e5308be298ed-image.png

这一看就不是我能写出来的 为什么一开始我要逞强自己重复造轮子而不去搜搜可能的已有解决方案呢

但是这里它不会获取原文件和新文件(即---+++行)

所以我这边稍微改了改

这个问题算是解决了

构建

正经做插件开发强烈建议用 gradle

这边为了 LLM 写起来方便直接用 maven 了 ( 真实原因:我懒得写 gradle 模板 )

但是问题是反编译过来的插件相当于只有 `src/main/java·文件夹里的几个代码文件,其他 pom.xml 都木有

5add6806-856c-4a30-a31e-656770afb7f8-image.png

然后 LLM 就会自己写上了(prompt里面提醒一句即可)

为思维链提供支持

前面新写的编辑插件功能就原生支持思维链了,这里把原来的生成插件的部分也加上;当然 prompt 也得改。

ba074ec1-1416-4b58-8e4f-094f1df66bca-image.png

细碎的修改

  • 现在支持在 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.askgptdisable_json_mode 参数
  • logger 现在会同时在日志文件和控制台输出了

最后的话

欸这一下午就过去了 这代码真得再找个时间优化优化了 现在真是一大坨

然后欢迎各位如果不嫌弃我的屎山的话可以提提 PR ……

最后说一下 BukkitGPT WebApp 的进展 原来的 WebApp 是部署在 某国外著名游戏服务商送我的 VPS 上的 —— 但是因为我更新太不活跃人家把 partnership 给我 remove 了

90c478b2-c773-4550-9a0f-53179fca0c00-image.png

没了美国的这台 4c8g (Disk 160G; Bandwith 8192GB) 的 VPS,我不得不找阿里云租了台轻量应用服务器,然后地址选错成上海了 要备案

所以最近这个 WebApp 的进程要延缓了。现在打算前端部署到 Vercel 上,然后后端放在阿里云上。但是不知道要等到什么时候了