vibe coding 这个词最近频繁出现在开发者社区里,很多人以为它只是“让 AI 全自动写代码”,但真正上手 13 天后会发现,它背后其实是另一种开发节奏。这篇博客记录的就是一个 13 天的个人项目:用 vibe coding 的方式做了一个名为“QQ华夏挂机版”的怀旧网游挂机模拟小 Demo,用来纪念 18 年前的网游时光。这个项目不包含原版游戏任何素材,只通过文字、数据、日志还原“自动打怪、升级、掉装备”的玩法,核心目标是跑通一条从自然语言需求到可运行代码的完整链路。
如果你对 vibe coding 感兴趣,又不想一上来就写复杂的 Web 应用,这个项目是非常合适的练手对象。它会涉及角色数据建模、自动战斗循环、经验成长曲线、存档机制、异常恢复和日志输出。每个部分都能用 Python 标准库实现,不需要额外安装第三方依赖,因此无论你是在 Windows、macOS 还是 Linux 上学习,跟着文章把脚本跑起来只需要十几分钟。
做完这个项目之后,你会理解 vibe coding 的边界:AI 能生成大量代码,但需求拆解、结果验证、边界处理、数值平衡和存档兼容仍然需要人来把握。下面先讲清楚 vibe coding 的工作方式,再展开这个挂机 Demo 的开发全过程。
1. vibe coding 到底是什么,它为什么适合这个怀旧项目
1.1 vibe coding 不是“让 AI 替你干活”,而是“用语言驱动开发节奏”
vibe coding 可以理解成一种开发方式:开发者用自然语言描述需求、现象和期望结果,AI 模型生成代码,开发者负责验证、运行、发现问题和继续迭代。与传统编程不同的是,你不再逐行敲完所有逻辑,而是把程序拆成一个个“能说清楚”的片段,再用提示词让 AI 补全。
“vibe”这个词强调的是保持在开发心流里,不要因为等待 AI 生成或反复改提示词而丢掉上下文。你可以把它看作一种人机结对编程:AI 负责快速起草,你负责判断方向。比如你告诉 AI“写一个玩家对战怪物的函数,怪物攻击力高于玩家防御时会造成伤害,伤害至少为 1”,AI 会给出类似max(1, monster.attack - player.defense + random.randint(-1, 2))的代码。你不需要从零敲出每一行,但你需要能判断这个公式是否符合预期。
这种模式对开发者的要求不是降低,而是换了一个方向。你不需要疯狂拼写语法,但你必须能读懂代码、能跑测试、能描述清楚问题。尤其是在排错时,如果把一段没有上下文的报错信息丢给 AI,它很难给出准确修复。真正有效的做法是先定位问题发生在哪个函数、输入是什么、希望输出什么,再把代码片段和错误日志一起交给 AI。
1.2 这类开发方式适合什么项目,不适合什么项目
vibe coding 不是万能钥匙。根据这 13 天的实践,我认为它非常适合以下场景:
- 工具脚本和自动化脚本:数据处理、文件整理、定时任务。
- 原型和 Demo:快速验证交互逻辑、算法流程、页面布局。
- 学习项目:用 AI 生成代码并阅读,是最快熟悉一门语言的方式之一。
- 模拟器和小游戏:状态清晰,逻辑边界完整,方便用函数表达。
不适合的场景也需要提前说明。涉及真实资金交易、高并发、用户隐私、安全权限控制的系统,不能只靠 vibe coding 生成代码后直接上线。金融计算中的金额精度、防止并发超卖、权限绕过等问题,都需要人工专门审查和充分测试。AI 可以帮你生成初稿,但“安全”和“合规”不能靠 vibe coding 的“感觉”来保证。
另外,个人学习项目要注意版权和合规。这次的项目只保留了“挂机玩法”的文字逻辑,不包含原版游戏的美术、音乐、角色名和数值体系。如果你想做一个怀旧主题小游戏,建议只写玩法框架,不要直接搬运原游戏的资源或真实商业数据。
1.3 为什么“QQ华夏挂机版”是一个合适的 vibe coding 练习
选择“挂机版”作为练习项目,是因为它的逻辑链路非常适合 AI 辅助开发:
- 状态变化清楚:玩家有等级、经验、生命、攻击、防御、金币、背包。
- 循环边界完整:每回合玩家打怪、怪物反击、结算经验与掉落。
- 结果可视化容易:控制台日志就是最简单的游戏界面。
- 迭代空间充足:先跑通战斗,再补存档,再调数值平衡,每一步都能独立验收。
传统开发模式里,你可能会花很多时间在 UI 和交互上。挂机 Demo 则不同,核心是状态在时间上的推进。AI 很容易理解“每 1 秒打一只怪,怪物死了给玩家经验,经验满了升级”这样一句需求,并且能生成相当完整的代码。
这个项目的另一个优点是失败成本低。即使生成的代码有 bug,最多只是控制台输出不对或存档文件损坏,不会影响任何线上服务。你可以放心地反复试验提示词,观察 AI 在不同描述方式下的输出差异,从而建立一套适合自己的 vibe coding 工作流。
2. 13天开发节奏:从自然语言需求到第一个可运行版本
2.1 先拆需求,再写提示词,不要直接让 AI“做一个游戏”
“做一个 QQ 华夏挂机版”这句话听起来完整,但对 AI 来说信息量远远不够。AI 没有自动理解需求上下文的功能,它只能根据提示词中的行为描述生成代码。如果一句话需求直接丢过去,你得到的很可能是结构混乱、参数残缺、边界缺失的代码,甚至是一个只打印欢迎语的空壳。
所以第一步是需求拆解。我把它拆成了六个可以独立验收的模块:
| 模块 | 核心功能 | 验收标准 |
|---|---|---|
| 角色创建 | 选择姓名、职业,初始化属性 | 启动时能生成一个角色实例 |
| 自动战斗 | 每回合玩家攻击怪物、怪物反击 | 能在控制台看到双方伤害数值 |
| 经验成长 | 击杀怪物获得经验,经验满升级 | 升级后属性提升,生命恢复 |
| 掉落背包 | 怪物掉落金币和物品 | 背包列表增长且有容量限制 |
| 存档读档 | 保存角色状态到 JSON 文件 | 重启程序后能恢复进度 |
| 运行控制 | 支持指定回合数和间隔时间 | 用户可以用命令行参数控制流程 |
拆完模块后,每个模块都可以单独写提示词。比如“创建一个 Player 数据类,包含 name、job、level、exp、hp、max_hp、attack、defense、gold、bag 字段,并写一个 gain_exp 方法用于增加经验。”这比“做一个游戏”清晰得多,AI 也不容易跑偏。
2.2 第一天到第三天:把“挂机”翻译成数据结构
前三天我没有让 AI 写完整项目,而是先编写游戏的数据模型。这个阶段最重要的任务是回答几个问题:
- 玩家需要保存哪些属性?
- 怪物和玩家战斗时,哪些属性参与伤害计算?
- 经验升级曲线是线性还是递增?
- 存档文件需要包含哪些字段?
数据结构一旦定下来,后续功能都是围绕它展开的。比如玩家数据使用 Python 的dataclass,每个字段都有明确含义。怪物数据使用字典列表,每个怪物包含名称、生命、攻击、防御、经验、金币和掉落物。这样做的好处是,AI 生成战斗函数时可以直接操作这些字段,不需要反复解析字符串。
数据结构表如下:
| 对象 | 字段 | 说明 |
|---|---|---|
| Player | name, job, level, exp | 基础身份与成长进度 |
| Player | hp, max_hp, attack, defense | 战斗属性 |
| Player | gold, bag | 经济与背包状态 |
| Monster | name, hp, attack, defense | 怪物战斗属性 |
| Monster | exp, gold, drop | 击杀结算信息 |
| 存档 | version, saved_at, player | 保存版本、时间、玩家状态 |
把数据模型固定下来之后,后面所有提示词都可以引用“Player 对象”和“Monster 字典”,AI 生成代码的方向会稳定很多。这里也推荐把字段名统一成英文,避免在代码里混用中文属性名。存档文件里的中文可以通过ensure_ascii=False正常显示,但字段名尽量使用英文。
2.3 第四天到第七天:跑通第一个战斗循环
有了数据模型,第四天开始让 AI 生成战斗函数。我给 AI 的提示词类似下面这样:
请用 Python 写一个函数,输入为玩家对象和怪物字典,输出为战斗日志字典。 规则: 1. 玩家命中率为 85%,命中后伤害为 player.attack 加上 -2 到 3 的随机数,再减去怪物 defense,最低伤害为 1。 2. 如果怪物未死亡,怪物对玩家造成伤害,伤害为怪物 attack 减去玩家 defense 加上 -1 到 2 的随机数,最低为 1。 3. 怪物生命小于等于 0 时,玩家获得经验、金币和掉落物。 4. 函数打印清晰的战斗日志。运行第一版后,发现一个问题:角色升级时生命值没有恢复到最大值,导致连续战斗时角色越来越虚弱。原因是 AI 生成的升级逻辑只增加了max_hp,没有把hp同步设置成新的max_hp。这属于非常典型的状态更新遗漏,AI 只关注“加属性”,忽略了“补状态”。
把报错和期望发给 AI 后,它很快补上了self.hp = self.max_hp。这个阶段最重要的事情是建立“运行后必须看日志”的习惯。挂机程序本身没有界面,日志就是唯一的反馈渠道。如果日志没有把当前回合的状态打印清楚,很难凭感觉判断数值是否合理。
2.4 第八天到第十三天:迭代、测试、存档和平衡
第八天之后,主体逻辑已经能跑。接下来的迭代重点是:
- 增加存档和读档功能,避免每次启动都从零开始。
- 调整伤害公式,让战士、法师、刺客三个职业有差异。
- 修复背包无限增长的问题,加入容量限制。
- 增加
--load参数,允许从存档继续挂机。 - 测试连续升级时
while循环是否会卡住。
这个阶段最大的感受是,AI 修 bug 的能力依赖于你提供的上下文。如果你只把Traceback粘贴过去,AI 经常只能猜测问题方向。更好的做法是同时提供:
运行 python idle_huaxia.py --rounds 20 后,第 8 回合出现 AttributeError: 'Player' object has no attribute 'take_damage'。 代码片段: class Player: ... 我需要调用 player.take_damage(5) 来扣血,但没有这个方法。 请给 Player 类补充 take_damage 方法,并保证生命值最低为 0,同时更新调用处。这样 AI 能快速定位问题。13 天中,大量时间不是花在“让 AI 生成代码”,而是花在“解释清楚期望行为、验证输出、补充边界条件”上。这个过程持续到最后一天,最终的小项目虽然没有复杂 UI,但核心玩法已经完整。
3. 从零实现最小可运行 Demo
3.1 环境准备
为了让这个项目可复现,我把它做成了一个单文件 Python 脚本,只依赖标准库。环境要求非常简单:
| 环境项 | 推荐配置 | 说明 |
|---|---|---|
| Python | 3.10 或更高 | 使用了dataclass和类型注解 |
| 操作系统 | Windows / macOS / Linux | 命令行输出需要支持 UTF-8 |
| 第三方依赖 | 无 | 只用标准库argparse、json、random、time、pathlib |
| 编辑器 | VS Code / PyCharm / 任意文本编辑器 | 配合 AI 插件体验更好 |
不需要创建虚拟环境,也不需要pip install。直接复制代码保存为idle_huaxia.py即可运行。
3.2 目录结构
虽然脚本是单文件,但推荐在项目目录下保留提示词和存档目录,方便后续维护:
idle-huaxia-demo/ ├── idle_huaxia.py # 主脚本 ├── prompts/ # 存放每次给 AI 的提示词 │ ├── 01-data-model.txt │ ├── 02-battle-loop.txt │ └── 03-save-load.txt ├── data/ │ └── save.json # 自动生成的存档文件 └── README.md # 项目说明prompts目录是整个 vibe coding 项目最有价值的部分。每次让 AI 生成功能之前,先把需求写进该目录,运行过程如果发现问题,再新增一个带有错误日志的修复提示词。这样即使换了电脑,你也能通过提示词文件还原整个开发过程。
3.3 完整代码(单文件版)
下面是一个最小可运行版本。它包含了角色创建、自动战斗、经验升级、背包掉落、存档读档和命令行控制。这段代码可以作为你继续扩展的起点。
""" idle_huaxia.py 怀旧网游挂机玩法的最小控制台 Demo。 仅用于学习 Python 和 vibe coding 工作流,不包含任何原版游戏素材。 """ import argparse import json import random import time from dataclasses import dataclass, field, asdict from pathlib import Path DEFAULT_CONFIG = { "interval": 1.0, "hit_rate": 0.85, "bag_limit": 20, "level_factor": 0.2, } MONSTERS = [ {"name": "野狗", "hp": 25, "attack": 6, "defense": 2, "exp": 12, "gold": 3, "drop": "布甲"}, {"name": "山贼", "hp": 45, "attack": 9, "defense": 4, "exp": 20, "gold": 8, "drop": "短刀"}, {"name": "石像", "hp": 80, "attack": 12, "defense": 6, "exp": 35, "gold": 15, "drop": "护心镜"}, ] @dataclass class Player: name: str job: str = "战士" level: int = 1 exp: int = 0 hp: int = 100 max_hp: int = 100 attack: int = 15 defense: int = 5 gold: int = 0 bag: list = field(default_factory=list) @property def exp_needed(self) -> int: return int(50 * self.level * (1 + DEFAULT_CONFIG["level_factor"] * self.level)) def gain_exp(self, amount: int) -> None: self.exp += amount while self.exp >= self.exp_needed: self.exp -= self.exp_needed self.level_up() def level_up(self) -> None: self.level += 1 self.max_hp += 10 self.hp = self.max_hp self.attack += 2 self.defense += 1 print(f"[升级] 恭喜升到 {self.level} 级,生命和攻击提升。") def take_damage(self, amount: int) -> None: self.hp = max(0, self.hp - amount) def player_attack(player: Player, monster: dict) -> int: if random.random() > DEFAULT_CONFIG["hit_rate"]: return 0 damage = max(1, player.attack + random.randint(-2, 3) - monster["defense"]) monster["hp"] = max(0, monster["hp"] - damage) return damage def monster_attack(player: Player, monster: dict) -> int: if monster["hp"] <= 0: return 0 damage = max(1, monster["attack"] - player.defense + random.randint(-1, 2)) player.take_damage(damage) return damage def battle(player: Player, monster: dict) -> None: pd = player_attack(player, monster) md = monster_attack(player, monster) print(f"[战斗] 你对 {monster['name']} 造成 {pd} 点伤害;它反击了 {md} 点伤害。") if monster["hp"] <= 0: player.gain_exp(monster["exp"]) player.gold += monster["gold"] player.bag.append(monster["drop"]) print(f"[胜利] 击杀 {monster['name']},获得经验 {monster['exp']}、金币 {monster['gold']}、物品 {monster['drop']}。") def save_game(player: Player, save_path: str = "data/save.json") -> None: payload = { "version": 1, "saved_at": int(time.time()), "player": asdict(player), } path = Path(save_path) path.parent.mkdir(exist_ok=True) tmp_path = path.with_suffix(".json.tmp") tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") tmp_path.replace(path) def load_game(save_path: str = "data/save.json"): path = Path(save_path) if not path.exists(): return None payload = json.loads(path.read_text(encoding="utf-8")) if payload.get("version") != 1: raise ValueError("存档版本不兼容") return Player(**payload["player"]) def run(player: Player, rounds: int = 10, interval: float = 1.0) -> None: monster_pool = MONSTERS for idx in range(1, rounds + 1): if player.hp <= 0: print("[结束] 角色倒下,挂机流程自动停止。") return monster = dict(monster_pool[(idx - 1) % len(monster_pool)]) battle(player, monster) print(f"[状态] 第 {idx} 回合:等级 {player.level},HP {player.hp}/{player.max_hp},经验 {player.exp}/{player.exp_needed},金币 {player.gold}。") if len(player.bag) > DEFAULT_CONFIG["bag_limit"]: print("[提示] 背包已满,本轮掉落的低阶物品被自动出售。") player.gold += 2 player.bag = player.bag[-DEFAULT_CONFIG["bag_limit"]:] time.sleep(interval) def create_player(name: str, job: str) -> Player: job_stats = { "战士": {"hp": 100, "attack": 15, "defense": 6}, "法师": {"hp": 70, "attack": 20, "defense": 3}, "刺客": {"hp": 80, "attack": 18, "defense": 4}, } base = job_stats.get(job, job_stats["战士"]) return Player(name=name, job=job, hp=base["hp"], max_hp=base["hp"], attack=base["attack"], defense=base["defense"]) def main(): parser = argparse.ArgumentParser(description="怀旧网游挂机玩法 Demo") parser.add_argument("--name", default="清风") parser.add_argument("--job", default="战士", choices=["战士", "法师", "刺客"]) parser.add_argument("--rounds", type=int, default=10) parser.add_argument("--interval", type=float, default=1.0) parser.add_argument("--load", action="store_true", help="加载已有存档") args = parser.parse_args() if args.load: player = load_game() or create_player(args.name, args.job) else: player = create_player(args.name, args.job) print(f"开始挂机:玩家 {player.name}({player.job}),计划 {args.rounds} 回合。") run(player, rounds=args.rounds, interval=args.interval) save_game(player) print("挂机结束,进度已保存到 data/save.json。") if __name__ == "__main__": main()这段代码最关键的地方是gain_exp中的while循环。如果经验一次性超过多级所需,AI 很可能只写if判断导致只升一级。使用while可以连续升级,直到剩余经验不足以继续升级为止。take_damage方法则保证了生命值不会变成负数,角色死亡后挂机主循环会停下来。
3.4 如何运行
在项目目录下执行:
python idle_huaxia.py --name 清风 --job 战士 --rounds 10 --interval 1预期输出类似:
开始挂机:玩家 清风(战士),计划 10 回合。 [战斗] 你对 野狗 造成 13 点伤害;它反击了 2 点伤害。 [胜利] 击杀 野狗,获得经验 12、金币 3、物品 布甲。 [状态] 第 1 回合:等级 1,HP 98/100,经验 12/60,金币 3。 ... 挂机结束,进度已保存到 data/save.json。执行结束后打开data/save.json,可以看到玩家状态已经持久化:
{ "version": 1, "saved_at": 1790000000, "player": { "name": "清风", "job": "战士", "level": 1, "exp": 12, "hp": 98, "max_hp": 100, "attack": 15, "defense": 6, "gold": 3, "bag": ["布甲"] } }如果要继续挂机,可以执行:
python idle_huaxia.py --load --rounds 20 --interval 1程序读取存档后从已有状态继续推进。这里要注意,如果存档文件版本字段不是 1,会抛出ValueError。给存档加版本号是 vibe coding 项目里很值得保留的好习惯,它让将来的数据迁移成为可能。
4. 关键代码拆解:AI 生成之后,人工该检查什么
4.1 随机数、命中率和伤害公式
战斗逻辑是最容易让 AI “编得很像但实际不平衡”的部分。原始的伤害公式如果写成:
damage = player.attack - monster["defense"]那么低攻击角色打高防御怪物时会出现零伤害,挂机永远推不下去。加入随机数和下限保护后,公式变得稳了:
damage = max(1, player.attack + random.randint(-2, 3) - monster["defense"])这里三个关键参数值得单独检查:
| 参数 | 作用 | 调大影响 | 调小影响 |
|---|---|---|---|
hit_rate | 控制玩家命中概率 | 战斗更稳定 | 会出现大量 Miss,推进变慢 |
| 随机浮动范围 | 模拟伤害波动 | 输出方差大 | 战斗容易变成固定数值 |
| 最小伤害下限 | 保证角色始终能造成伤害 | 低攻职业也能通关 | 数值可能不真实,但不会卡死 |
在实际项目中,建议把命中率、经验系数等配置从代码中抽出来,放到DEFAULT_CONFIG字典里统一维护。这样 AI 在后续修改时不需要改动多个函数,也能避免“这里改了伤害公式,那里还在用旧参数”的问题。
4.2 升级经验与属性成长
经验成长曲线是挂机游戏里最影响体验的部分。如果经验需求固定不变,游戏会越来越无聊;如果需求指数增长过快,又会让人看不到提升空间。我使用的公式是:
int(50 * self.level * (1 + DEFAULT_CONFIG["level_factor"] * self.level))也就是每升一级,下一级所需经验会随等级增加而增加。level_factor控制增长速度。当前值是 0.2,1 级升级需要 60 经验,5 级升级需要大约 100 经验,10 级升级需要大约 150 经验。你可以根据挂机时长调整这个系数。
有一个容易出错的点:gain_exp里一定要用while而不是if。因为高级怪物给的经验可能超过当前升级所需的好几倍。假设角色 1 级时需要 60 经验,一只怪物给了 150 经验,if版本只会升到 2 级,剩余 90 经验会消失;while版本会连续升到 3 级,剩余经验继续保留。这个边界在 AI 第一版代码里往往会被忽略。
4.3 存档和异常恢复
存档功能看似简单,但 AI 生成的第一版可能直接这样写:
path.write_text(json.dumps(payload), encoding="utf-8")这种写法有一个隐患:如果程序在写入过程中被强制终止,存档文件可能只写了一半,下一次读档直接JSONDecodeError。更好的方案是“先写临时文件,再原子替换”:
tmp_path = path.with_suffix(".json.tmp") tmp_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") tmp_path.replace(path)tmp_path.replace(path)在同一个目录内替换文件时是原子操作,读取方只会看到旧文件或新文件,不会看到写了一半的文件。这个技巧在许多生产级系统里都有应用,放在一个小挂机 Demo 里也很值得练习。
读档时还要注意版本兼容:
if payload.get("version") != 1: raise ValueError("存档版本不兼容")如果未来你给角色增加了新字段,旧存档没有这个字段,直接传入Player(**payload["player"])会报TypeError。加上版本号后,你可以写一个“旧版本迁移”函数,把旧存档升级成新结构。这是长期维护 vibe coding 项目的关键细节。
4.4 日志与可观测性
挂机程序没有界面,日志就是唯一的观察窗口。建议至少输出以下信息:
- 当前回合数。
- 玩家对怪物造成的伤害。
- 怪物反击造成的伤害。
- 击杀后的经验、金币、物品收益。
- 玩家当前等级、生命、经验、金币状态。
输出格式可以统一为[回合] [战斗] [胜利] [升级] [提示] [结束]这类前缀。如果你想把日志采集起来做分析,可以加上时间戳:
import datetime timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") print(f"[{timestamp}][战斗] ...")有了时间戳后,日志就能用于定位“某个阶段为什么挂机变慢”“背包什么时候开始满”等问题。一个 mini 项目也可以提前养成可观测性的习惯。
5. 常见问题排查:挂机脚本为什么不按预期工作
5.1 五个常见现象与处理表
Vibe coding 项目里,大多数问题不是 AI 不会写,而是生成代码后在特定输入下触发了边界 bug。下面是这个项目最常见的五类问题,以及对应的排查建议:
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 模块找不到或脚本无法启动 | 运行目录不对,或文件被保存为错误名称 | 执行pwd查看当前目录,执行python idle_huaxia.py看报错 | 确认脚本在工作目录下,不要直接双击运行 |
| 控制台中文乱码 | Windows 终端默认编码不是 UTF-8 | 查看终端代码页,或检查 Python 报错信息 | Windows 下先执行set PYTHONIOENCODING=utf-8再运行 |
| 挂机跑了几轮就停止 | 角色生命值降为零,主循环检测到hp <= 0 | 查看日志中[结束] 角色倒下之前的状态 | 提高职业初始防御,或加入自动回血逻辑 |
| 角色升级后没有恢复生命 | 升级时只增加max_hp,没有同步hp | 查看升级日志前后的 HP 状态 | 在level_up中把hp设置为新的max_hp |
| 存档读出来报 JSON 错误 | 存档文件被中断写入,或手动编辑导致格式损坏 | 打开data/save.json查看内容 | 使用临时文件原子替换;开发时备份存档 |
除了以上问题,还有一种很隐蔽的情况:AI 生成的exp_needed使用了level_factor,但你在测试时把DEFAULT_CONFIG中的值改掉了,导致旧存档读取后升级经验变化。建议每次都从同一份配置启动,或者把配置一并写入存档。
5.2 从日志倒推问题
遇到 bug 时,不要直接看代码猜,先看日志。这个项目的日志已经包含回合状态,排查顺序可以这样安排:
- 查看最后一条日志决定问题发生在哪一阶段。
- 如果是战斗未推进,检查上一回合玩家和怪物的生命值。
- 如果是升级异常,检查经验变化和升级后的属性。
- 如果是存档问题,先打开 JSON 文件确认字段是否完整。
- 仍然无法定位,就把日志、关键函数代码、输入参数一起发给 AI,并要求它给出修改后的完整