chiaki-ng 配置转换说明(chiaki-settings.json -> chiaki-ng-*.ini)
1. 目标与适用范围
本文说明如何把一个平台导出的 chiaki-settings.json(JSON 格式)转换为桌面版可导入的 chiaki-ng-*.ini(QSettings INI 格式)。
适用场景:
- 你只有 JSON 备份,想在桌面版 chiaki-ng 中恢复主机注册信息。
- 需要迁移
registered_hosts和manual_hosts。
不适用场景:
- 直接把 JSON 文件喂给桌面版导入器(桌面版导入器只接受
*.ini)。
2. 输入与输出
- 输入文件:
chiaki-settings.json - 输出文件:
chiaki-ng-Default.ini(或任意*.ini文件名)
建议先备份当前 INI:
Copy-Item .\chiaki-ng-Default.ini .\chiaki-ng-Default.ini.bak
3. 解析 JSON 结构
chiaki-settings.json 顶层应包含:
format:固定为chiaki-settingsversion:当前为2settings:对象,包含以下数组registered_hostsmanual_hosts
registered_hosts 单项字段:
target(例如PS5_1/PS4_10)ap_ssidap_bssidap_keyap_nameserver_mac(字符串,格式aa:bb:cc:dd:ee:ff)server_nicknamerp_regist_key(Base64,解码后应为 16 字节)rp_key_type(整数)rp_key(Base64,解码后应为 16 字节)
manual_hosts 单项字段:
host(IP 或域名)server_mac(可空)
4. 字段转换规则(核心)
4.1 target 字符串转整数
写入 INI 时,target 必须是整数枚举值:
PS4_UNKNOWN->0PS4_8->800PS4_9->900PS4_10->1000PS5_UNKNOWN->1000000PS5_1->1000100
4.2 server_mac 文本转 @ByteArray
JSON 中:
aa:bb:cc:dd:ee:ff
INI 中:
@ByteArray(\xAA\xBB\xCC\xDD\xEE\xFF)
要求严格为 6 字节。
4.3 rp_regist_key / rp_key
- 先 Base64 解码
- 解码结果必须是 16 字节
- 写入
@ByteArray(...)
示例:
rp_key=@ByteArray(\x11\x22\x33\x44\x55\x66\x77\x88\x99\xAA\xBB\xCC\xDD\xEE\xFF\x00)
4.4 manual_hosts 的注册关联
如果 manual_hosts[i].server_mac 不为空:
registered=trueregistered_mac=@ByteArray(...)(6 字节 MAC)
如果 server_mac 为空:
registered=falseregistered_mac可写 6 字节全 0
5. 写入 INI 的结构
桌面版可导入的 INI 至少包含以下段:
[registered_hosts][hidden_hosts][manual_hosts][controller_mappings][settings][General]
其中数组段使用 n\field=value 格式,n 从 1 开始,并包含 size:
1\target=...1\server_mac=...- ...
size=1
注意:
size必须与实际条目数一致,否则导入后可能丢项。
6. 匿名示例(完整 INI 骨架)
[registered_hosts]
1\target=1000100
1\ap_ssid=
1\ap_bssid=
1\ap_key=
1\ap_name=PS5
1\server_nickname=Console-A
1\server_mac=@ByteArray(\xAA\xBB\xCC\xDD\xEE\xFF)
1\rp_regist_key=@ByteArray(\x10\x20\x30\x40\x50\x60\x70\x80\x90\xA0\xB0\xC0\xD0\xE0\xF0\x00)
1\rp_key_type=2
1\rp_key=@ByteArray(\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10)
size=1
[hidden_hosts]
size=0
[manual_hosts]
1\id=0
1\host=192.168.0.10
1\registered=true
1\registered_mac=@ByteArray(\xAA\xBB\xCC\xDD\xEE\xFF)
size=1
[controller_mappings]
size=0
[settings]
hw_decoder=auto
this_profile=
[General]
version=2
7. 最小校验清单
导入前检查:
format/version/settings在 JSON 中存在。- 每个
rp_regist_keyBase64 解码后是 16 字节。 - 每个
rp_keyBase64 解码后是 16 字节。 - 每个 MAC 最终是 6 字节。
- INI 中每个数组段
size正确。 [General]中version=2。
可用 PowerShell 快速验证 Base64 长度:
$j = Get-Content .\chiaki-settings.json -Raw | ConvertFrom-Json
$j.settings.registered_hosts | ForEach-Object {
[PSCustomObject]@{
server_nickname = $_.server_nickname
rp_regist_key_len = ([Convert]::FromBase64String($_.rp_regist_key)).Length
rp_key_len = ([Convert]::FromBase64String($_.rp_key)).Length
}
}
8. 导入到 chiaki-ng
- 打开 chiaki-ng。
- 进入设置,执行
Import Profile From File。 - 选择转换后的
*.ini。 - 导入后重启应用并检查主机列表。
9. 常见失败原因
- 使用了 JSON 直接导入(桌面导入器不支持)。
rp_key/rp_regist_key没有先 Base64 解码就直接写入。- MAC 仍是字符串格式,未写成
@ByteArray。 target仍是字符串,未转整数。size与条目数不一致。
10. 自动转换脚本(Python,推荐)
已提供脚本:
scripts/convert_chiaki_json_to_ini.py
功能:
- 输入
chiaki-settings.json和一个ini模板文件。 - 自动清空并重写
[registered_hosts]与[manual_hosts]。 - 其他段(如
[settings]、[General])保持原样。
前置要求(脚本会检查并提示):
- 模板 INI 必须是“空备份配置文件”,即:
[registered_hosts]为size=0[manual_hosts]为size=0- 不能有
1\...这类索引项
运行示例:
py -3 .\scripts\convert_chiaki_json_to_ini.py --json .\chiaki-settings.json --template-ini .\chiaki-ng-Default.ini --output-ini .\chiaki-ng-Default-from-json.ini
若不传 --output-ini,默认输出:
<模板文件名>-from-json.ini
11. Python 脚本(嵌入)
"""
Convert chiaki-settings.json to chiaki-ng ini backup format.
Only [registered_hosts] and [manual_hosts] sections are replaced.
All other sections are preserved from the template ini.
"""
from __future__ import annotations
import argparse
import base64
import binascii
import json
import re
import sys
from pathlib import Path
from typing import Any
TARGET_MAP = {
"PS4_UNKNOWN": 0,
"PS4_8": 800,
"PS4_9": 900,
"PS4_10": 1000,
"PS5_UNKNOWN": 1000000,
"PS5_1": 1000100,
}
def target_to_int(value: Any) -> int:
if isinstance(value, int):
return value
if isinstance(value, str) and value in TARGET_MAP:
return TARGET_MAP[value]
raise ValueError(f"Unsupported target value: {value!r}")
def bytes_to_qbytearray_literal(data: bytes) -> str:
return "@ByteArray(" + "".join(f"\\x{b:02X}" for b in data) + ")"
def mac_to_bytes(mac: str) -> bytes:
parts = mac.split(":")
if len(parts) != 6:
raise ValueError(f"Invalid MAC address {mac!r}, expected aa:bb:cc:dd:ee:ff")
try:
return bytes(int(p, 16) for p in parts)
except ValueError as exc:
raise ValueError(f"Invalid MAC address {mac!r}") from exc
def decode_base64(value: str, field_name: str) -> bytes:
try:
return base64.b64decode(value, validate=True)
except (ValueError, binascii.Error) as exc:
raise ValueError(f"Field {field_name!r} is not valid Base64") from exc
def section_regex(section: str) -> re.Pattern[str]:
escaped = re.escape(section)
return re.compile(rf"^\[{escaped}\]\r?\n.*?(?=^\[[^\]]+\]\r?\n|\Z)", re.MULTILINE | re.DOTALL)
def get_section_block(content: str, section: str) -> str | None:
m = section_regex(section).search(content)
return m.group(0) if m else None
def assert_empty_section(content: str, section: str) -> None:
block = get_section_block(content, section)
if block is None:
raise ValueError(f"Template ini is missing required section [{section}]")
body = re.sub(rf"^\[{re.escape(section)}\]\r?\n", "", block, count=1, flags=re.MULTILINE | re.DOTALL)
size_match = re.search(r"^size=(\d+)\s*$", body, flags=re.MULTILINE)
if not size_match:
raise ValueError(f"Template ini section [{section}] must contain size=0")
size = int(size_match.group(1))
has_entries = bool(re.search(r"^\d+\\", body, flags=re.MULTILINE))
if size != 0 or has_entries:
raise ValueError(
f"Template ini section [{section}] is not empty. "
"Use an empty backup ini (size=0 and no indexed entries)."
)
def replace_or_append_section(content: str, section: str, body: str, nl: str) -> str:
pattern = section_regex(section)
replacement = f"[{section}]{nl}{body.rstrip(chr(13) + chr(10))}{nl}"
if pattern.search(content):
return pattern.sub(lambda _m: replacement, content, count=1)
return content.rstrip("\r\n") + nl + nl + replacement
def to_str(value: Any) -> str:
if value is None:
return ""
return str(value)
def build_registered_section(hosts: list[dict[str, Any]]) -> str:
lines: list[str] = []
for idx, host in enumerate(hosts, start=1):
target_value = target_to_int(host.get("target"))
server_mac_literal = bytes_to_qbytearray_literal(mac_to_bytes(to_str(host.get("server_mac"))))
rp_reg = decode_base64(to_str(host.get("rp_regist_key")), "rp_regist_key")
if len(rp_reg) != 16:
raise ValueError(f"registered_hosts[{idx - 1}].rp_regist_key length {len(rp_reg)} != 16")
rp_key = decode_base64(to_str(host.get("rp_key")), "rp_key")
if len(rp_key) != 16:
raise ValueError(f"registered_hosts[{idx - 1}].rp_key length {len(rp_key)} != 16")
try:
rp_key_type = int(host.get("rp_key_type", 0))
except (TypeError, ValueError) as exc:
raise ValueError(f"registered_hosts[{idx - 1}].rp_key_type is not an integer") from exc
lines.append(f"{idx}\\target={target_value}")
lines.append(f"{idx}\\ap_ssid={to_str(host.get('ap_ssid'))}")
lines.append(f"{idx}\\ap_bssid={to_str(host.get('ap_bssid'))}")
lines.append(f"{idx}\\ap_key={to_str(host.get('ap_key'))}")
lines.append(f"{idx}\\ap_name={to_str(host.get('ap_name'))}")
lines.append(f"{idx}\\server_nickname={to_str(host.get('server_nickname'))}")
lines.append(f"{idx}\\server_mac={server_mac_literal}")
lines.append(f"{idx}\\rp_regist_key={bytes_to_qbytearray_literal(rp_reg)}")
lines.append(f"{idx}\\rp_key_type={rp_key_type}")
lines.append(f"{idx}\\rp_key={bytes_to_qbytearray_literal(rp_key)}")
lines.append(f"size={len(hosts)}")
return "\n".join(lines)
def build_manual_section(hosts: list[dict[str, Any]]) -> str:
lines: list[str] = []
for idx, host in enumerate(hosts, start=1):
lines.append(f"{idx}\\id={idx - 1}")
lines.append(f"{idx}\\host={to_str(host.get('host'))}")
server_mac = to_str(host.get("server_mac")).strip()
if server_mac:
lines.append(f"{idx}\\registered=true")
lines.append(f"{idx}\\registered_mac={bytes_to_qbytearray_literal(mac_to_bytes(server_mac))}")
else:
lines.append(f"{idx}\\registered=false")
lines.append(f"{idx}\\registered_mac={bytes_to_qbytearray_literal(bytes([0, 0, 0, 0, 0, 0]))}")
lines.append(f"size={len(hosts)}")
return "\n".join(lines)
def detect_newline(text: str) -> str:
return "\r\n" if "\r\n" in text else "\n"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Convert chiaki-settings.json into a chiaki-ng ini backup.\n"
"Only [registered_hosts] and [manual_hosts] are replaced."
)
)
parser.add_argument("--json", dest="json_path", required=True, help="Path to chiaki-settings.json")
parser.add_argument("--template-ini", dest="template_ini", required=True, help="Path to empty backup ini template")
parser.add_argument("--output-ini", dest="output_ini", default=None, help="Output ini path")
return parser.parse_args()
def main() -> int:
args = parse_args()
json_path = Path(args.json_path).expanduser().resolve()
template_ini_path = Path(args.template_ini).expanduser().resolve()
if not json_path.is_file():
print(f"JSON file not found: {json_path}", file=sys.stderr)
return 1
if not template_ini_path.is_file():
print(f"Template ini file not found: {template_ini_path}", file=sys.stderr)
return 1
print("This script requires an EMPTY backup ini template exported by chiaki-ng.")
print("Required: [registered_hosts] size=0 and [manual_hosts] size=0.")
try:
json_data = json.loads(json_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
print(f"Invalid JSON: {exc}", file=sys.stderr)
return 1
template_text = template_ini_path.read_text(encoding="utf-8-sig")
newline = detect_newline(template_text)
if json_data.get("format") != "chiaki-settings":
print("Invalid JSON field 'format'. Expected 'chiaki-settings'.", file=sys.stderr)
return 1
if json_data.get("version") != 2:
print(f"Unsupported JSON version: {json_data.get('version')!r}. Expected 2.", file=sys.stderr)
return 1
settings = json_data.get("settings")
if not isinstance(settings, dict):
print("Invalid JSON: missing 'settings' object.", file=sys.stderr)
return 1
try:
assert_empty_section(template_text, "registered_hosts")
assert_empty_section(template_text, "manual_hosts")
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
registered_hosts = settings.get("registered_hosts") or []
manual_hosts = settings.get("manual_hosts") or []
if not isinstance(registered_hosts, list) or not isinstance(manual_hosts, list):
print("Invalid JSON: 'registered_hosts' and 'manual_hosts' must be arrays.", file=sys.stderr)
return 1
try:
registered_body = build_registered_section(registered_hosts)
manual_body = build_manual_section(manual_hosts)
except ValueError as exc:
print(str(exc), file=sys.stderr)
return 1
updated_text = template_text
updated_text = replace_or_append_section(updated_text, "registered_hosts", registered_body, newline)
updated_text = replace_or_append_section(updated_text, "manual_hosts", manual_body, newline)
if args.output_ini:
output_path = Path(args.output_ini).expanduser().resolve()
else:
output_path = template_ini_path.with_name(template_ini_path.stem + "-from-json.ini")
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(updated_text, encoding="utf-8")
print("")
print("Conversion completed.")
print(f"Input JSON: {json_path}")
print(f"Template INI: {template_ini_path}")
print(f"Output INI: {output_path}")
print(f"Registered hosts: {len(registered_hosts)}")
print(f"Manual hosts: {len(manual_hosts)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())

参与讨论
(Participate in the discussion)
参与讨论