fix(clashmeta): render block-style yaml for subscription export
This commit is contained in:
@@ -46,12 +46,12 @@
|
|||||||
## 2. 方案
|
## 2. 方案
|
||||||
|
|
||||||
### 技术方案
|
### 技术方案
|
||||||
将 `ClashMeta` 中的 `Yaml::dump($config, 2, 4, ...)` 提升为更高的 inline 深度阈值,
|
将 `ClashMeta` 中依赖 `Yaml::dump(...)` 的默认风格选择,替换为显式的 block style YAML 渲染,
|
||||||
使 `proxies`、`proxy-groups` 等深层结构优先以 block style 导出,而不是压缩成单行 flow map。
|
避免 `proxies`、`proxy-groups` 等深层结构被压缩成单行 flow map。
|
||||||
|
|
||||||
本次仅修改:
|
本次仅修改:
|
||||||
|
|
||||||
1. `app/Protocols/ClashMeta.php` 的 dump 参数;
|
1. `app/Protocols/ClashMeta.php` 的 YAML 渲染路径;
|
||||||
2. 在代码旁增加一行说明性注释,明确修复目的;
|
2. 在代码旁增加一行说明性注释,明确修复目的;
|
||||||
3. 不改模板、不改节点字段构造、不改 Clash / Stash。
|
3. 不改模板、不改节点字段构造、不改 Clash / Stash。
|
||||||
|
|
||||||
@@ -99,17 +99,17 @@ N/A
|
|||||||
|
|
||||||
## 5. 技术决策
|
## 5. 技术决策
|
||||||
|
|
||||||
### fix-clashmeta-flow-map-export#D001: 只调整 dump 参数,不重写节点构造逻辑
|
### fix-clashmeta-flow-map-export#D001: 显式渲染 block style YAML,不再依赖 dumper 的默认风格选择
|
||||||
**日期**: 2026-04-18
|
**日期**: 2026-04-18
|
||||||
**状态**: ✅采纳
|
**状态**: ✅采纳
|
||||||
**背景**: 问题的根源在序列化风格,而不是 TUIC 节点字段本身的构造。
|
**背景**: 问题的根源在序列化风格,而不是 TUIC 节点字段本身的构造。仅修改 `Yaml::dump` 参数后,服务器真值仍然返回单行 flow map。
|
||||||
**选项分析**:
|
**选项分析**:
|
||||||
| 选项 | 优点 | 缺点 |
|
| 选项 | 优点 | 缺点 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| A: 提高 `Yaml::dump` 的 inline 深度阈值 | 改动最小,直接命中根因,不改变节点字段 | 输出会更展开 |
|
| A: 继续微调 `Yaml::dump` 参数 | 改动小 | 已被服务器真值否定,不能保证摆脱 flow map |
|
||||||
| B: 手工重写整个 YAML 渲染流程 | 可完全控制格式 | 风险高,改动大,超出当前修复范围 |
|
| B: 显式输出 block style YAML | 可完全控制 `proxies` / `proxy-groups` / `rules` 的格式 | 代码量更大,需要自定义渲染 |
|
||||||
**决策**: 选择方案 A
|
**决策**: 选择方案 B
|
||||||
**理由**: 当前问题是 flow style 输出兼容性差,调高 dump 阈值即可稳定转回 block style,最符合最小修复原则。
|
**理由**: 服务器真值已经证明仅调参数不够,必须显式控制 YAML 输出风格,才能彻底避免客户端继续收到 `- { ... }`。
|
||||||
**影响**: 仅影响 Clash Meta 订阅的最终文本格式,不影响节点字段语义
|
**影响**: 仅影响 Clash Meta 订阅的最终文本格式,不影响节点字段语义
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -37,9 +37,9 @@
|
|||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| 2026-04-18 00:29:00 | 方案包创建 | completed | 已创建 `202604180029_fix-clashmeta-flow-map-export` |
|
| 2026-04-18 00:29:00 | 方案包创建 | completed | 已创建 `202604180029_fix-clashmeta-flow-map-export` |
|
||||||
| 2026-04-18 00:31:00 | 1.1 | completed | 已确认问题点在 `ClashMeta.php` 的 `Yaml::dump(..., 2, 4, ...)` |
|
| 2026-04-18 00:31:00 | 1.1 | completed | 已确认问题点在 `ClashMeta.php` 的 `Yaml::dump(..., 2, 4, ...)` |
|
||||||
| 2026-04-18 00:32:00 | 1.2 | completed | 已将 `ClashMeta` dump inline 深度提升到 `10` 并增加注释 |
|
| 2026-04-18 00:32:00 | 1.2 | completed | 初始尝试已将 `ClashMeta` dump inline 深度提升到 `10` |
|
||||||
| 2026-04-18 00:33:00 | 2.1 | completed | 已确认 `git diff` 仅涉及 `ClashMeta` 序列化参数与注释,方案包校验通过 |
|
| 2026-04-18 00:33:00 | 2.1 | completed | 服务器真值显示 `?flag=meta` 仍返回 `- { ... }`,确认仅调参数无效 |
|
||||||
| 2026-04-18 00:33:30 | 2.2 | completed | 已记录运行验证受限原因:本机无 `php` 与 `vendor` |
|
| 2026-04-18 00:33:30 | 2.2 | completed | 已切换为显式 block style YAML 渲染方案,并保留运行验证受限说明 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -48,5 +48,5 @@
|
|||||||
> 记录执行过程中的重要说明、决策变更、风险提示等
|
> 记录执行过程中的重要说明、决策变更、风险提示等
|
||||||
|
|
||||||
- 本次只修 `ClashMeta`,未联动 `Clash` / `Stash`
|
- 本次只修 `ClashMeta`,未联动 `Clash` / `Stash`
|
||||||
- 本机缺少 `php` 与 `vendor`,无法执行运行时订阅生成验证
|
- 本机缺少 `php` 与 `vendor`,无法在当前工作区执行运行时订阅生成验证
|
||||||
- 当前 diff 还包含换行符提示:Git 显示该文件后续可能按工作树策略从 LF 触碰为 CRLF
|
- 当前 diff 还包含换行符提示:Git 显示该文件后续可能按工作树策略从 LF 触碰为 CRLF
|
||||||
|
|||||||
@@ -203,9 +203,9 @@ class ClashMeta extends AbstractProtocol
|
|||||||
$config['proxy-groups'] = array_values($config['proxy-groups']);
|
$config['proxy-groups'] = array_values($config['proxy-groups']);
|
||||||
$config = $this->buildRules($config);
|
$config = $this->buildRules($config);
|
||||||
|
|
||||||
// Keep nested proxy objects in block style to avoid long one-line flow maps
|
// Force full block-style YAML output because some Clash Meta clients fail
|
||||||
// that some Clash Meta clients fail to parse.
|
// to parse long flow maps such as "- { name: ..., alpn: [...] }".
|
||||||
$yaml = Yaml::dump($config, 10, 4, Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE);
|
$yaml = self::dumpExpandedYaml($config);
|
||||||
$yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
|
$yaml = str_replace('$app_name', admin_setting('app_name', 'XBoard'), $yaml);
|
||||||
return response($yaml)
|
return response($yaml)
|
||||||
->header('content-type', 'text/yaml')
|
->header('content-type', 'text/yaml')
|
||||||
@@ -214,6 +214,85 @@ class ClashMeta extends AbstractProtocol
|
|||||||
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
|
->header('content-disposition', 'attachment;filename*=UTF-8\'\'' . rawurlencode($appName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function dumpExpandedYaml(array $config): string
|
||||||
|
{
|
||||||
|
return rtrim(self::dumpYamlNode($config), "\n") . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function dumpYamlNode($value, int $indent = 0): string
|
||||||
|
{
|
||||||
|
if (!is_array($value)) {
|
||||||
|
return str_repeat(' ', $indent) . self::dumpYamlScalar($value) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value === []) {
|
||||||
|
return str_repeat(' ', $indent) . "[]\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines = [];
|
||||||
|
if (self::isAssocArray($value)) {
|
||||||
|
foreach ($value as $key => $item) {
|
||||||
|
$prefix = str_repeat(' ', $indent) . $key . ':';
|
||||||
|
if (is_array($item)) {
|
||||||
|
if ($item === []) {
|
||||||
|
$lines[] = $prefix . ' []';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = $prefix;
|
||||||
|
$lines[] = rtrim(self::dumpYamlNode($item, $indent + 4), "\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = $prefix . ' ' . self::dumpYamlScalar($item);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($value as $item) {
|
||||||
|
$prefix = str_repeat(' ', $indent) . '-';
|
||||||
|
if (is_array($item)) {
|
||||||
|
if ($item === []) {
|
||||||
|
$lines[] = $prefix . ' []';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = $prefix;
|
||||||
|
$lines[] = rtrim(self::dumpYamlNode($item, $indent + 4), "\n");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lines[] = $prefix . ' ' . self::dumpYamlScalar($item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode("\n", $lines) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function dumpYamlScalar($value): string
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return 'null';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return $value ? 'true' : 'false';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($value) || is_float($value)) {
|
||||||
|
return (string) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "'" . str_replace("'", "''", (string) $value) . "'";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function isAssocArray(array $value): bool
|
||||||
|
{
|
||||||
|
if ($value === []) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_keys($value) !== range(0, count($value) - 1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the rules for Clash.
|
* Build the rules for Clash.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user