699 lines
23 KiB
Python
699 lines
23 KiB
Python
import argparse
|
||
import json
|
||
import os
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
sys.stdout.reconfigure(encoding='utf-8')
|
||
sys.stderr.reconfigure(encoding='utf-8')
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent.parent
|
||
DEFAULT_CONFIG_DIR = PROJECT_ROOT / "Assets" / "Resources" / "Resources_moved" / "config"
|
||
DEFAULT_OUTPUT = Path(__file__).resolve().parent / "ConfigLinkData.json"
|
||
DEFAULT_TABLE_INFO = Path(__file__).resolve().parent / "table_info.json"
|
||
|
||
|
||
def parse_args():
|
||
parser = argparse.ArgumentParser(description="ConfigLinkViewer 配置表联动关系自动生成")
|
||
parser.add_argument("--config-dir", type=str, default=None,
|
||
help="JSON 配置导出目录 (默认: Assets/Resources/Resources_moved/config)")
|
||
parser.add_argument("--output", type=str, default=None,
|
||
help="输出 ConfigLinkData.json 路径")
|
||
parser.add_argument("--table-info", type=str, default=None,
|
||
help="表信息配置 JSON 路径 (默认: table_info.json)")
|
||
return parser.parse_args()
|
||
|
||
|
||
def load_table_info(filepath):
|
||
if filepath.exists():
|
||
with open(filepath, "r", encoding="utf-8") as f:
|
||
data = json.load(f)
|
||
entries = data.get("entries", [])
|
||
return {
|
||
entry["name"]: (entry.get("displayName", entry["name"]), entry.get("description", ""))
|
||
for entry in entries
|
||
}
|
||
return {}
|
||
|
||
|
||
TABLE_NAME_TO_CN = {
|
||
"hero": "英雄",
|
||
"enemy": "敌人",
|
||
"equip": "装备",
|
||
"prop": "道具",
|
||
"skill": "技能",
|
||
"buff": "增益效果",
|
||
"attribute": "属性",
|
||
"quest": "任务",
|
||
"player": "玩家",
|
||
"rune": "符文",
|
||
"recharge": "充值",
|
||
"checkpointreward": "关卡奖励",
|
||
"activity": "活动",
|
||
"map_act": "地图波次1",
|
||
"map_act2": "地图波次2",
|
||
"map_act3": "地图波次3",
|
||
"map_act4": "地图波次4",
|
||
"fight_sample": "主线关卡",
|
||
"fight_arena": "竞技场战斗",
|
||
"fight_fb1": "副本1",
|
||
"fight_fb2": "副本2",
|
||
"fight_fb3": "副本3",
|
||
"fight_fb4": "副本4",
|
||
"map": "地图",
|
||
}
|
||
|
||
|
||
def load_json_file(filepath):
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8-sig") as f:
|
||
return json.load(f)
|
||
except UnicodeDecodeError:
|
||
with open(filepath, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
except json.JSONDecodeError:
|
||
return []
|
||
|
||
|
||
def extract_fields_from_json(filepath):
|
||
data = load_json_file(filepath)
|
||
|
||
if isinstance(data, list) and len(data) > 0:
|
||
return list(data[0].keys())
|
||
elif isinstance(data, dict):
|
||
return list(data.keys())
|
||
return []
|
||
|
||
|
||
def detect_reward_format(values):
|
||
if not values:
|
||
return ""
|
||
sample = ""
|
||
for v in values[:5]:
|
||
if isinstance(v, str):
|
||
sample = v
|
||
break
|
||
elif isinstance(v, list) and len(v) > 0:
|
||
for item in v[:3]:
|
||
if isinstance(item, str):
|
||
sample = item
|
||
break
|
||
if sample:
|
||
break
|
||
|
||
if not sample:
|
||
return ""
|
||
|
||
if re.match(r"^item_\d+_\d+$", sample):
|
||
return "item_id_num"
|
||
if re.match(r"^hero_\d+$", sample):
|
||
return "hero_id"
|
||
if re.match(r"^rune_\d+_\d+$", sample):
|
||
return "item_id_num"
|
||
if re.match(r"^equip_\d+_\d+$", sample):
|
||
return "item_id_num"
|
||
if re.match(r"^Hero_\d+$", sample):
|
||
return "item_id_num"
|
||
if re.match(r"^\d+_\d+_\d+_\d+$", sample):
|
||
return "id_lv_count_delay"
|
||
if re.match(r"^\d+_\d+_\d+$", sample):
|
||
return "id_lv_num"
|
||
return ""
|
||
|
||
|
||
def detect_consumes_format(values):
|
||
if not values:
|
||
return ""
|
||
sample = ""
|
||
for v in values[:5]:
|
||
if isinstance(v, str):
|
||
sample = v
|
||
break
|
||
elif isinstance(v, list) and len(v) > 0:
|
||
for item in v[:3]:
|
||
if isinstance(item, str):
|
||
sample = item
|
||
break
|
||
if sample:
|
||
break
|
||
|
||
if not sample:
|
||
return ""
|
||
|
||
if re.match(r"^item_\d+_\d+$", sample):
|
||
return "item_id_num"
|
||
return ""
|
||
|
||
|
||
def detect_monster_format(values):
|
||
if not values:
|
||
return ""
|
||
sample = ""
|
||
for v in values[:5]:
|
||
if isinstance(v, str):
|
||
sample = v
|
||
break
|
||
elif isinstance(v, list) and len(v) > 0:
|
||
for item in v[:3]:
|
||
if isinstance(item, str):
|
||
sample = item
|
||
break
|
||
if sample:
|
||
break
|
||
|
||
if not sample:
|
||
return ""
|
||
|
||
if re.match(r"^\d+_\d+_\d+_\d+(\.\d+)?$", sample):
|
||
return "id_lv_count_delay"
|
||
if re.match(r"^\d+_\d+_\d+$", sample):
|
||
return "id_lv_num"
|
||
return ""
|
||
|
||
|
||
def get_relation_for_target_id(table_name):
|
||
mapping = {
|
||
"herolevel": ("hero", "关联英雄ID"),
|
||
"herostage": ("hero", "关联英雄ID"),
|
||
"herolevelinternal": ("enemy", "关联敌人ID"),
|
||
"enemylevel": ("enemy", "关联敌人ID"),
|
||
"equiplevel": ("equip", "关联装备ID"),
|
||
"equipstage": ("equip", "关联装备ID"),
|
||
"playerlevel": ("player", "关联玩家ID"),
|
||
"playerstage": ("player", "关联玩家ID"),
|
||
"runelevel": ("rune", "关联符文ID"),
|
||
"skilllevel": ("skill", "关联技能ID"),
|
||
"buff": ("skill", "关联技能ID"),
|
||
"map": ("map", "关联地图ID"),
|
||
"map1": ("map", "关联地图ID"),
|
||
"map2": ("map", "关联地图ID"),
|
||
"map3": ("map", "关联地图ID"),
|
||
"map_act": ("map", "关联地图ID"),
|
||
"map_act2": ("map", "关联地图ID"),
|
||
"map_act3": ("map", "关联地图ID"),
|
||
"map_act4": ("map", "关联地图ID"),
|
||
"test_map_act": ("map", "关联地图ID"),
|
||
"trainbreak": ("prop", "关联训练突破ID"),
|
||
"trainlevel": ("prop", "关联训练等级ID"),
|
||
}
|
||
if table_name in mapping:
|
||
return mapping[table_name]
|
||
return None
|
||
|
||
|
||
def build_relations(table_name, fields, sample_values):
|
||
relations = []
|
||
|
||
reward_fields = {
|
||
"rewards", "rewards2", "pass_rewards", "activateRewards", "reward",
|
||
"dailyQuestScoreRewards", "weeklyQuestScoreRewards", "achievementScoreRewards",
|
||
"heroTujianRewards",
|
||
}
|
||
consume_fields = {"consumes", "lostConsumes"}
|
||
monster_fields = {"monsters", "enemy_ids"}
|
||
equip_fields = {"equips"}
|
||
skill_fields = {"skill_ids", "skills"}
|
||
hero_fields = {"hero_ids"}
|
||
quest_fields = {"quest"}
|
||
attr_fields = {"attr_id", "level_attr_id", "stage_attr_id"}
|
||
shard_fields = {"shardItem"}
|
||
compose_fields = {"compose"}
|
||
next_fields = {"next"}
|
||
recharge_fields = {"recharge_id"}
|
||
product_fields = {"productId"}
|
||
out_enemy_fields = {"out_enemy_config"}
|
||
scene_fight_fields = {"fight_cf_name"}
|
||
scene_map_fields = {"map_cf_name"}
|
||
time_limit_fields = {"time_limit_boss"}
|
||
|
||
for field in fields:
|
||
if field == "target_id":
|
||
result = get_relation_for_target_id(table_name)
|
||
if result:
|
||
target, desc = result
|
||
relations.append({
|
||
"field": field,
|
||
"target": target,
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": desc,
|
||
})
|
||
continue
|
||
|
||
if field == "activityId":
|
||
relations.append({
|
||
"field": field,
|
||
"target": "activity",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "所属活动ID",
|
||
})
|
||
continue
|
||
|
||
if field == "reward_id":
|
||
relations.append({
|
||
"field": field,
|
||
"target": "checkpointreward",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "关卡奖励ID",
|
||
})
|
||
continue
|
||
|
||
if field in reward_fields:
|
||
fmt = detect_reward_format(sample_values.get(field, []))
|
||
if not fmt:
|
||
fmt = "item_id_num"
|
||
|
||
desc_map = {
|
||
"rewards": "奖励道具",
|
||
"rewards2": "付费奖励道具",
|
||
"pass_rewards": "首通奖励道具",
|
||
"activateRewards": "激活奖励道具",
|
||
"reward": "宝箱奖励道具",
|
||
}
|
||
desc = desc_map.get(field, f"{field}奖励道具")
|
||
|
||
if table_name == "gacha" and field == "rewards":
|
||
relations.append({
|
||
"field": field,
|
||
"target": "hero",
|
||
"targetField": "id",
|
||
"format": "hero_id",
|
||
"description": "召唤产出英雄(格式: hero_英雄ID)",
|
||
})
|
||
else:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "prop",
|
||
"targetField": "id",
|
||
"format": fmt,
|
||
"description": f"{desc}(格式: {fmt})" if fmt else desc,
|
||
})
|
||
continue
|
||
|
||
if field in consume_fields:
|
||
fmt = detect_consumes_format(sample_values.get(field, []))
|
||
desc_map = {
|
||
"consumes": "消耗道具",
|
||
"lostConsumes": "失败损失道具",
|
||
}
|
||
desc = desc_map.get(field, field)
|
||
relations.append({
|
||
"field": field,
|
||
"target": "prop",
|
||
"targetField": "id",
|
||
"format": fmt if fmt else "item_id_num",
|
||
"description": f"{desc}(格式: item_id_num)" if fmt else f"{desc}(格式: item_id_num)",
|
||
})
|
||
continue
|
||
|
||
if field in monster_fields:
|
||
fmt = detect_monster_format(sample_values.get(field, []))
|
||
desc_map = {
|
||
"monsters": "怪物列表",
|
||
"enemy_ids": "敌人列表",
|
||
}
|
||
desc = desc_map.get(field, field)
|
||
format_desc = f"(格式: {fmt})" if fmt else ""
|
||
relations.append({
|
||
"field": field,
|
||
"target": "enemy",
|
||
"targetField": "id",
|
||
"format": fmt,
|
||
"description": f"{desc}{format_desc}",
|
||
})
|
||
continue
|
||
|
||
if field in time_limit_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "enemy",
|
||
"targetField": "id",
|
||
"format": "id_lv_num_hp",
|
||
"description": "限时Boss(格式: 敌人ID_等级_数量_血量百分比)",
|
||
})
|
||
continue
|
||
|
||
if field in equip_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "equip",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "装备槽位装备ID列表",
|
||
})
|
||
continue
|
||
|
||
if field in skill_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "skill",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "技能ID列表",
|
||
})
|
||
continue
|
||
|
||
if field in hero_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "hero",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "关联英雄ID列表",
|
||
})
|
||
continue
|
||
|
||
if field in quest_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "quest",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "关联任务ID列表",
|
||
})
|
||
continue
|
||
|
||
if field in attr_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "attribute",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "关联属性ID",
|
||
})
|
||
continue
|
||
|
||
if field in shard_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "prop",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "碎片道具ID",
|
||
})
|
||
continue
|
||
|
||
if field in compose_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "prop",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "合成材料道具ID列表",
|
||
})
|
||
continue
|
||
|
||
if field in next_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": table_name,
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "下一个ID(自引用)",
|
||
})
|
||
continue
|
||
|
||
if field in recharge_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "recharge",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "关联充值档位ID",
|
||
})
|
||
continue
|
||
|
||
if field in product_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "prop",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "商品道具ID",
|
||
})
|
||
continue
|
||
|
||
if field in out_enemy_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "map_act",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "出怪波次配置表名",
|
||
})
|
||
continue
|
||
|
||
if field in scene_fight_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "fight_sample",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "关联战斗配置表名",
|
||
})
|
||
continue
|
||
|
||
if field in scene_map_fields:
|
||
relations.append({
|
||
"field": field,
|
||
"target": "map",
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": "关联地图配置表名",
|
||
})
|
||
continue
|
||
|
||
if re.match(r"^.+Id$", field) and field != "Id":
|
||
prefix = field[:-2]
|
||
for known_table in TABLE_NAME_TO_CN:
|
||
if known_table.lower() == prefix.lower():
|
||
relations.append({
|
||
"field": field,
|
||
"target": known_table,
|
||
"targetField": "id",
|
||
"format": "",
|
||
"description": f"关联{TABLE_NAME_TO_CN[known_table]}ID",
|
||
})
|
||
break
|
||
|
||
return relations
|
||
|
||
|
||
SUSPICIOUS_ID_PATTERNS = [
|
||
(re.compile(r"^.+Id$"), "可能引用其他表的ID字段(驼峰)"),
|
||
(re.compile(r"^.+_id$"), "可能引用其他表的ID字段(下划线)"),
|
||
(re.compile(r"^.+_ids$"), "可能引用其他表的ID列表(下划线)"),
|
||
(re.compile(r"^.+_Ids$"), "可能引用其他表的ID列表(驼峰)"),
|
||
]
|
||
|
||
|
||
def analyze_gaps(all_tables, table_fields_map):
|
||
issues = []
|
||
ai_prompt_parts = []
|
||
|
||
tables_not_in_info = [t["name"] for t in all_tables if t["displayName"] == t["name"]]
|
||
if tables_not_in_info:
|
||
issues.append({
|
||
"type": "missing_display_name",
|
||
"severity": "warning",
|
||
"title": "以下表缺少中文名和描述",
|
||
"items": tables_not_in_info,
|
||
})
|
||
ai_prompt_parts.append(
|
||
f"以下表缺少中文名和描述,请补全 displayName 和 description:\n"
|
||
+ "\n".join(f" - {t}" for t in tables_not_in_info)
|
||
)
|
||
|
||
no_relation_tables = [t["name"] for t in all_tables if len(t["relations"]) == 0]
|
||
if no_relation_tables:
|
||
issues.append({
|
||
"type": "no_relations",
|
||
"severity": "info",
|
||
"title": "以下表没有任何引用关系(可能是叶子表或需人工检查)",
|
||
"items": no_relation_tables,
|
||
})
|
||
|
||
unrecognized_fields = []
|
||
for table_name, fields in table_fields_map.items():
|
||
table_entry = next((t for t in all_tables if t["name"] == table_name), None)
|
||
if not table_entry:
|
||
continue
|
||
related_fields = {r["field"] for r in table_entry["relations"]}
|
||
for field in fields:
|
||
if field in related_fields:
|
||
continue
|
||
for pattern, desc in SUSPICIOUS_ID_PATTERNS:
|
||
if pattern.match(field):
|
||
unrecognized_fields.append(f" {table_name}.{field} - {desc}")
|
||
break
|
||
|
||
if unrecognized_fields:
|
||
issues.append({
|
||
"type": "unrecognized_fields",
|
||
"severity": "warning",
|
||
"title": "以下字段可能是引用关系但脚本未能识别",
|
||
"items": unrecognized_fields,
|
||
})
|
||
ai_prompt_parts.append(
|
||
"以下字段可能是引用关系但脚本未能识别,请分析并补全 relations:\n"
|
||
+ "\n".join(unrecognized_fields)
|
||
)
|
||
|
||
tables_with_missing_fields = []
|
||
for table in all_tables:
|
||
if table["displayName"] == table["name"] and len(table["relations"]) == 0:
|
||
fields = table_fields_map.get(table["name"], [])
|
||
if fields:
|
||
tables_with_missing_fields.append(
|
||
f" {table['name']} (字段: {', '.join(fields[:8])}{'...' if len(fields) > 8 else ''})"
|
||
)
|
||
|
||
if tables_with_missing_fields:
|
||
issues.append({
|
||
"type": "fully_missing",
|
||
"severity": "error",
|
||
"title": "以下表完全未配置(无中文名、无描述、无引用关系)",
|
||
"items": tables_with_missing_fields,
|
||
})
|
||
ai_prompt_parts.append(
|
||
"以下表完全未配置,请补全 displayName、description 和 relations:\n"
|
||
+ "\n".join(tables_with_missing_fields)
|
||
)
|
||
|
||
return issues, ai_prompt_parts
|
||
|
||
|
||
def main():
|
||
args = parse_args()
|
||
|
||
config_dir = Path(args.config_dir) if args.config_dir else DEFAULT_CONFIG_DIR
|
||
output_file = Path(args.output) if args.output else DEFAULT_OUTPUT
|
||
table_info_file = Path(args.table_info) if args.table_info else DEFAULT_TABLE_INFO
|
||
|
||
table_info = load_table_info(table_info_file)
|
||
|
||
all_tables = []
|
||
table_fields_map = {}
|
||
|
||
json_files = sorted(config_dir.glob("*.json"))
|
||
|
||
for filepath in json_files:
|
||
table_name = filepath.stem
|
||
fields = extract_fields_from_json(filepath)
|
||
if not fields:
|
||
continue
|
||
|
||
table_fields_map[table_name] = fields
|
||
|
||
sample_values = {}
|
||
data = load_json_file(filepath)
|
||
|
||
if isinstance(data, list) and len(data) > 0:
|
||
first_row = data[0]
|
||
for field in fields:
|
||
if field in first_row:
|
||
val = first_row[field]
|
||
if isinstance(val, list):
|
||
sample_values[field] = val
|
||
else:
|
||
sample_values[field] = [val] if val is not None else []
|
||
|
||
display_name, description = table_info.get(table_name, (table_name, ""))
|
||
relations = build_relations(table_name, fields, sample_values)
|
||
|
||
all_tables.append({
|
||
"name": table_name,
|
||
"displayName": display_name,
|
||
"description": description,
|
||
"fileExtension": ".xlsx",
|
||
"relations": relations,
|
||
})
|
||
|
||
txt_files = sorted(config_dir.glob("*.txt"))
|
||
for filepath in txt_files:
|
||
table_name = filepath.stem
|
||
if table_name in table_info:
|
||
display_name, description = table_info[table_name]
|
||
else:
|
||
display_name, description = table_name, ""
|
||
all_tables.append({
|
||
"name": table_name,
|
||
"displayName": display_name,
|
||
"description": description,
|
||
"fileExtension": ".txt",
|
||
"relations": [],
|
||
})
|
||
|
||
output = {"tables": all_tables}
|
||
output_file.parent.mkdir(parents=True, exist_ok=True)
|
||
with open(output_file, "w", encoding="utf-8") as f:
|
||
json.dump(output, f, ensure_ascii=False, indent=4)
|
||
|
||
total_tables = len(all_tables)
|
||
total_relations = sum(len(t['relations']) for t in all_tables)
|
||
tables_with_rels = sum(1 for t in all_tables if t['relations'])
|
||
|
||
print("=" * 60)
|
||
print(" ConfigLinkData.json 生成完成")
|
||
print("=" * 60)
|
||
print(f" 输出文件: {output_file}")
|
||
print(f" 总表数: {total_tables}")
|
||
print(f" 有引用关系的表: {tables_with_rels}")
|
||
print(f" 总引用关系数: {total_relations}")
|
||
print()
|
||
|
||
issues, ai_prompt_parts = analyze_gaps(all_tables, table_fields_map)
|
||
|
||
if not issues:
|
||
print(" 所有表均已完整配置,无需要补全的内容。")
|
||
else:
|
||
print("-" * 60)
|
||
print(" 查漏补缺报告")
|
||
print("-" * 60)
|
||
for issue in issues:
|
||
severity_icon = {"error": "[!]", "warning": "[*]", "info": "[i]"}.get(issue["severity"], "")
|
||
print(f"\n {severity_icon} {issue['title']} ({len(issue['items'])}项)")
|
||
for item in issue["items"]:
|
||
print(f" {item}")
|
||
|
||
if ai_prompt_parts:
|
||
print()
|
||
print("=" * 60)
|
||
print(" AI 补全提示词(可复制发送给 AI)")
|
||
print("=" * 60)
|
||
print()
|
||
print("我需要你帮助维护配置表联动查看器的配置文件。")
|
||
print()
|
||
print(f"当前目录:{output_file.parent}")
|
||
print()
|
||
print("请:")
|
||
print("1. 读取 ConfigLinkData.json,分析所有表的配置情况")
|
||
print("2. 找出缺少中文名称(displayName == name)或描述为空的表,补充中文名称和描述")
|
||
print("3. 分析字段引用关系,为缺少联动关系的表添加 relations")
|
||
print("4. 对于 target_id 字段,通常引用对应的基础表(如 herolevel.target_id -> hero)")
|
||
print("5. 对于 xxxId/xxx_id 字段,根据前缀推断目标表(如 itemId -> prop)")
|
||
print("6. 同时更新 table_info.json(中文名称)和 ConfigLinkData.json(完整配置)")
|
||
print("7. 输出修改摘要")
|
||
print()
|
||
print("需要处理的内容:")
|
||
print()
|
||
for part in ai_prompt_parts:
|
||
print(part)
|
||
print()
|
||
print("注意:")
|
||
print("- displayName 应该是简洁的中文名称(如\"英雄\"、\"装备\")")
|
||
print("- description 应该是清晰的功能描述(如\"英雄基础属性配置\")")
|
||
print("- relations 字段格式:{\"field\": \"字段名\", \"target\": \"目标表名\", \"targetField\": \"id\", \"description\": \"关联描述\"}")
|
||
print("- 保持 JSON 格式正确,不要添加多余逗号")
|
||
print("- table_info.json 只需更新 displayName 和 description")
|
||
print("- ConfigLinkData.json 需要更新 displayName、description 和 relations")
|
||
|
||
print()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|