ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AI项目结构图一键生成文件夹与空文件:树形解析脚本实践

AI项目结构图一键生成文件夹与空文件:树形解析脚本实践

上周在整理一个演示项目的骨架,AI 三秒钟甩给我一张结构图,二十多个目录、六十多个文件,层级最深的地方套了四层。我盯着那张图看了半天,最后还是老实地一个目录一个目录地建,建到第四层的时候就开始烦了——这种活纯粹是体力支出,没有任何智力含量,但错了又不行,漏一个目录后面 import 全崩。“从AI给的项目结构图快速生成项目文件夹及文件”这件事,就是在这种场景下被逼出来的需求:把 AI 输出的那段字符画,一键变成磁盘上真实存在的目录和空文件,让脚手架从“复制粘贴”升级到“落地执行”。这件事听起来像个小脚本,但真写起来,坑比想象的多:树形字符是 Unicode、缩进风格每家大模型都不一样、目录和文件的判定规则模糊、名字里带空格、Windows 和 macOS 的非法字符不一样。这篇把我从零把这件事做成的全过程拆开讲,包括解析器的设计、三套不同语言的实现、以及那些只有真跑过才会遇到的边界问题。不管你是刚接手一个空仓库不知道从哪下手,还是想给自己的自动化流程补一块拼图,下面的内容都能直接抄。 ## 1. 先把这件事的边界划清楚 ### 1.1 手工建目录到底浪费了什么 很多人第一反应是“建几个文件夹能有多费劲”。我实测过:一个中等规模的前端或服务端项目骨架,通常包含 15 到 40 个目录、30 到 100 个文件,层级 3 到 5 层。用 IDE 右键新建,平均一个目录 4 秒、一个文件 6 秒(还要输入文件名、确认后缀),加上中途的思考停顿和偶尔建错位置重来,整件事轻松吃掉 15 到 20 分钟。关键不在于这 20 分钟本身,而在于它打断了你的“上下文”——你本来在脑子里构想模块划分,结果被迫切换到鼠标操作,等你建完,最初的架构直觉已经散了。 更隐蔽的成本是一致性。手建的目录名容易前后不统一,比如一会儿utils、一会儿utilities,一会儿components放在src下、一会儿又放到根目录。这种不一致在当时看不出问题,等到三周后要写构建配置、要按目录做代码分割,才发现路径要一个个改。用脚本从一份结构图生成,本质上是在做“基础设施即代码”的同一件事:结构图是声明,脚本是执行,两者一一对应,可版本管理、可 review、可重放。这也是我坚持把结构图存成文件而不是直接贴进终端的核心原因。 ### 1.2 AI 吐出来的“结构图”其实有三种形态 这一节非常关键,因为解析器的复杂度几乎完全由输入形态决定。我统计了自己过去几个月收集的样本,AI 输出的结构图基本落在三类里。第一类是标准 UTF-8 树形字符,也就是tree命令那种带├──、└──、│的画法,这类最漂亮,也最常见。第二类是 ASCII 退化版,用|--、`--、|拼出来,多出现在要求“纯 ASCII 输出”的场景,或者在老旧终端里被自动降级。第三类干脆不是树,而是纯缩进列表或者纯路径清单,一行一个,用两个或四个空格表示层级。project/ ├── src/ │ ├── main.py │ └── utils/ │ └── helper.py ├── tests/ │ └── test_main.py └── README.md上面这段就是第一类。如果我把同一棵树改写成第三种形态,它会变成这样:project/ src/ main.py utils/ helper.py tests/ test_main.py README.md两种写法表达的是同一件事,但解析逻辑完全不同:前者靠符号判断层级,后者靠缩进宽度判断层级。更麻烦的是,同一个模型在不同对话里可能混用——我遇到过上面几行是树字符、底下几行突然变成纯缩进的输出。所以解析器必须能同时处理两种情况,而不能赌一种格式。这也是后面我要用“前缀宽度除以缩进单位”这个算法的原因,它对两种形态天然通用。 ### 1.3 三个必须提前拍板的决策 动手写代码之前,有三件事如果不先想清楚,写到一半一定会推翻重来。第一件是根节点的语义:结构图的第一行通常是project/,它到底是“我要在当前目录下新建一个叫 project 的文件夹”,还是“当前目录就是 project,从它的子项开始建”?这个决定影响整个脚本的路径基准,我最后做成了命令行参数--root,默认把第一行当作根目录名,同时提供--no-root跳过它。 第二件是已存在文件怎么处理。三种策略:跳过(默认)、覆盖、报错退出。我的选择是“文件跳过、目录合并”,因为绝大多数使用场景是在已有仓库里补结构,覆盖会毁掉已有代码,这个风险不值得冒。第三件是目录和文件怎么区分。最可靠的信号是名字以/结尾,这在树形输出里几乎是约定俗成;但 AI 有时会漏掉斜杠,这时就看它有没有子节点——有子节点必然是目录。这两个规则叠加,准确率能到 99% 以上,剩下的靠人工扫一眼。 > 提示:这三个决策不要等到写完脚本再补,先把它们做成命令行开关,后面调试的时候你会感谢自己。 ## 2. 解析器的核心:把画出来的树读成数据 ### 2.1 一套能吃掉所有缩进风格的前缀识别法 我最初的想法是按符号逐个解析:遇到│算一层、遇到├算一层、遇到空格按四个一组算一层。写出来之后立刻翻车,因为 ASCII 树的|-- src里,|和--各被算了一次,深度直接翻倍。后来又改成“先把所有连接符统一替换成同一套字符再解析”,结果my-app这种带连字符的文件名被误伤,-被当成连接符吃掉了一半。 折腾两轮之后我意识到,最稳的办法不是“数符号”,而是“量宽度”。观察一下标准树形输出的结构:每个层级固定占 4 个字符宽度,├──是 4 个字符(├、─、─、空格),│也是 4 个字符(│加三个空格), (四空格)还是 4 个字符。这意味着行首到文件名的字符数量,除以 4,就是深度。ASCII 版本同理:|--是 4 个字符,|是 4 个字符。python PREFIX_CHARS = set(' │|├└┌`+─-—–') def split_line(raw): raw = raw.rstrip() i = 0 while i < len(raw) and raw[i] in PREFIX_CHARS: i += 1 return raw[:i], raw[i:]这段代码只做一件事:从行首开始,把所有属于“树装饰字符”的字符一路吃掉,剩下的就是名字。len(prefix) // unit就是深度。它同时兼容 UTF-8 树、ASCII 树、纯空格缩进三种形态,因为在这三种形态里,装饰字符都只出现在名字之前。唯一需要小心的是文件名本身以-或+开头的情况,这种名字极其罕见,我在脚本里直接加了一条“不安全名字跳过并告警”的兜底。 ### 2.2 缩进单位怎么自动探测 上一节说“除以 4”,但 4 不是常量。有的模型用 2 空格缩进,有的用 3 空格,我甚至见过用 Tab 的——Tab 被渲染成几个空格取决于终端设置,这是最恶心的一种。硬编码 4 会让 2 空格缩进的结构图全部塌成一层,所有文件挤在根目录下。 解决办法是先把所有行的“前缀长度”收集起来,求最大公约数。比如前缀长度集合是{0, 2, 2, 4, 6},gcd 是 2,说明单位是 2;如果是{0, 4, 8, 12},gcd 是 4。这里有个陷阱:如果所有非零前缀恰好都是 8 的倍数(比如整张图最浅的节点就在第 2 层),gcd 会算出 8,深度全部除以 8 就只剩 1 层了。所以我加了一条钳制:结果必须落在 1 到 4 之间,超出就退回 4。python def detect_unit(lines): spans = [] for line in lines: m = re.match(r'^( +)\S', line) if m: spans.append(len(m.group(1))) if not spans: return 4 unit = reduce(gcd, spans) return unit if 1 <= unit <= 4 else 4这个函数只在前缀全是空格时生效,含树字符的行前缀长度本身就是 4 的倍数,gcd 自然得 4,两种情况殊途同归。实测下来,这套探测在几十份样本里只有一份判错,出错那份是因为 AI 在树外面额外加了两行带三个空格缩进的说明文字,被算进了 gcd。所以我后来把“过滤噪声行”放在了探测之前,顺序不能反。 ### 2.3 目录、文件、注释与噪声行的判定 噪声行是解析器最大的敌人。AI 输出结构图时,习惯性地在外面包一层三反引号的代码围栏,有时候还会在前后加一句“以上是项目结构”。这些行如果混进去,会生成名为​```` 的目录或者叫“以上是项目结构”的文件。我的过滤规则是:跳过空行、跳过以三个反引号开头的行、跳过以#或//开头的纯注释行、跳过包含空格且以中文标点结尾的说明行。 目录判定走两级:先看名字是否以/或` 结尾,是则判定为目录并去掉尾部斜杠;否则看它在整棵树里有没有下一层节点(即下一行的深度是否大于当前行),有则晋升为目录。第二级判定是为了兜住 AI 忘记加斜杠的情况,实测能救回大概两成的漏标。文件名里的合法字符还要做一次校验,<>:"|?*这些在主流桌面系统上非法,出现就跳过该行并在终端打出警告,避免生成一堆打不开的僵尸文件。python def classify(nodes): result = [] for idx, (depth, name) in enumerate(nodes): is_dir = name.endswith('/') or name.endswith('\\') clean = name.rstrip('/\\').strip() if not is_dir and idx + 1 < len(nodes) and nodes[idx + 1][0] > depth: is_dir = True result.append((depth, clean, is_dir)) return result这段逻辑很短但价值很高。它把“AI 有没有老实写斜杠”这件事从“必须满足”降级成了“最好满足”,容错空间大了很多。我个人的经验是,凡是跟外部输入打交道的地方,都不要假设对方会严格遵守格式,宁可多写十行兜底代码,也别在半夜收到“目录建不出来”的求助。 ## 3. 生成器实现:Python 完整版逐段拆解 ### 3.1 完整脚本 这是我现在用的版本,一百行左右,无第三方依赖,Python 3.8 以上直接跑。我把它命名为treegen.py,平时扔在~/bin里全局可调。脚本做了这几件事:读文件、过滤噪声、探测缩进单位、解析成(深度, 名字, 是否目录)三元组、按深度维护一个路径栈、逐条落地、最后打印统计。python #!/usr/bin/env python3 import argparse import re import sys from functools import reduce from math import gcd from pathlib import Path PREFIX_CHARS = set(' │|├└┌`+─-—–') NOISE_RE = re.compile(r'^\s*(|~~~|...$|$ )') def detect_unit(lines): spans = [] for line in lines: m = re.match(r'^( +)\S', line) if m: spans.append(len(m.group(1))) if not spans: return 4 unit = reduce(gcd, spans) return unit if 1 <= unit <= 4 else 4 def parse(text, unit=None, strip_root=False): raw_lines = [l.rstrip() for l in text.splitlines()] lines = [l for l in raw_lines if l.strip() and not NOISE_RE.match(l)] if unit is None: unit = detect_unit(lines) nodes = [] for line in lines: i = 0 while i < len(line) and line[i] in PREFIX_CHARS: i += 1 name = line[i:].strip() if not name: continue nodes.append((len(line[:i]) // unit, name)) if strip_root and nodes: base = nodes[0][0] nodes = [(d - base - 1, n) for d, n in nodes[1:]] return nodes def safe(name): if name in ('.', '..') or not name: return False if '/' in name or '\' in name: return False return not re.search(r'[<>:"|?\x00-\x1f]', name) def load_templates(path): if not path or not Path(path).exists(): return {} import json return json.loads(Path(path).read_text(encoding='utf-8')) def build(nodes, root, dry_run=False, templates=None): root = Path(root) templates = templates or {} paths = {} created_dirs = created_files = skipped = 0 for depth, name in nodes: is_dir = name.endswith(('/', '\')) or any( d > depth for d, _ in nodes[nodes.index((depth, name)) + 1:][:1] ) clean = name.rstrip('/\').strip() if not safe(clean): print(f'[skip] 非法名称: {clean!r}', file=sys.stderr) skipped += 1 continue parent = root if depth == 0 else paths.get(depth - 1, root) target = parent / clean if is_dir: if not dry_run: target.mkdir(parents=True, exist_ok=True) paths[depth] = target created_dirs += 1 else: if dry_run: print(f'[file] {target}') created_files += 1 continue if target.exists(): skipped += 1 continue target.parent.mkdir(parents=True, exist_ok=True) suffix = target.suffix.lstrip('.') target.write_text(templates.get(suffix, templates.get('default', '')), encoding='utf-8') created_files += 1 return created_dirs, created_files, skipped def main(): ap = argparse.ArgumentParser() ap.add_argument('tree_file') ap.add_argument('--root', default='.') ap.add_argument('--strip-root', action='store_true') ap.add_argument('--unit', type=int, default=None) ap.add_argument('--dry-run', action='store_true') ap.add_argument('--templates', default=None) args = ap.parse_args() text = Path(args.tree_file).read_text(encoding='utf-8') nodes = parse(text, args.unit, args.strip_root) d, f, s = build(nodes, args.root, args.dry_run, load_templates(args.templates)) print(f'目录 {d} 个,文件 {f} 个,跳过 {s} 个') ifname== 'main': main()### 3.2 关键行在做什么 上面这段代码里有几处值得单独说明,因为它们是踩坑踩出来的。第一处是 `paths` 这个字典,它记录了“第 N 层当前对应的实际路径”。解析是顺序进行的,当遇到深度回退的行(比如从第 3 层跳回第 1 层),字典里第 2、3 层的旧值还在,但下一行查的是 `paths[depth - 1]`,也就是第 0 层,所以不受影响。这个设计比维护一个显式栈更省代码,而且天然容忍“跳级”这种畸形输入。 第二处是 `target.parent.mkdir(parents=True, exist_ok=True)` 这一行在创建文件之前又调了一次。看起来多余,其实是必要的:如果 AI 输出的树里某个目录被漏掉了(只写了文件没写父目录那一行),提前建父目录能保证文件不会因为父路径不存在而抛异常。`parents=True` 会递归创建所有缺失的中间目录,`exist_ok=True` 则让重复创建不报错,这两个参数组合起来就是幂等的基础。 第三处是 `strip_root` 的实现。注意我砍掉根节点的方式是“把所有深度减去第一行的深度再减一”,而不是简单地丢掉第一行。因为有些 AI 会在树的外面再包一层名字,或者第一行就是根节点本身,两种情况下“减一”的语义都要一致,否则子节点会全部跑到上一层去。这一小段我改了三版才稳定下来,最开始的写法是直接 `nodes[1:]`,结果深度没归零,所有节点都挂到了错误的父路径下面。 > 注意:`nodes.index((depth, name))` 这种查找方式在重名节点上会出错。如果你的结构图里有两个同名的 `index.ts`,它总是返回第一个。生产版本我换成了带下标的循环,博文里为了可读性保留了这种写法,你抄的时候记得改。 ### 3.3 模板与初始内容 空文件在建完之后会亮一片红,IDE 的 linter 会把它们全标成警告。与其忍受这个,不如在创建时就写入最小可用的初始内容。我做了个 `.treegen.json` 配置,按后缀映射模板:json { "default": "", "py": """"TODO: 模块说明"""\n", "ts": "export {};\n", "tsx": "export {};\n", "js": "export {};\n", "md": "# TODO\n", "go": "package main\n", "css": "/TODO/\n" }`ts` 和 `tsx` 的 `export {};` 这一行是刻意加的——它让这个空文件变成一个 ES 模块,避免 TypeScript 把它当成全局脚本,也避免某些构建工具把它直接剔除。`py` 那个文档字符串同理,有些静态检查工具会把完全没有内容的文件跳过,加个 docstring 就正常了。`go` 的 `package main` 是为了让文件能被 `go build` 识别到,否则整个目录不参与编译。 模板机制还有个大用处:可以批量写入统一的版权头或者文件说明。我在某个多人协作的项目里就干过这件事,给所有 `py` 文件预置了一个带模块名占位符的注释头,脚本创建时把文件名填进去,后面新人看代码时一眼就知道这个文件是几月几号为了什么加的。这个做法能不能推广见仁见智,但如果你所在的团队有规范要求,它确实能省掉大量重复劳动。 ## 4. 轻量场景的另外两条路:Bash 与 Node ### 4.1 让 AI 直接输出路径清单,Bash 十几行搞定 如果你的机器上没有 Python,或者你就想要最轻的方案,那答案是“别让 AI 输出树,让它输出路径清单”。这是我从源头降低难度的一个思路:与其费劲解析树形字符,不如在提示词里就要求 AI 用一行一个相对路径、目录以斜杠结尾的格式输出。这样拿到的东西已经是机器友好的,脚本可以短到十几行。bash #!/usr/bin/env bash set -euo pipefail src="${1:?用法: mkdirs.sh paths.txt [root]}" root="${2:-.}" while IFS= read -r line; do line="${line%$'\r'}" [[ -z "${line// }" ]] && continue [[ "$line" == #]] && continue line="${line#"${line%%[![:space:]]*}"}" if [[ "$line" == */ ]]; then mkdir -p -- "$root/$line" echo "[dir ] $root/$line" else mkdir -p -- "$root/$(dirname -- "$line")" [[ -e "$root/$line" ]] || : > "$root/$line" echo "[file] $root/$line" fi done < "$src"这段代码干的事很直白:读一行、去掉行尾的 `\r`(Windows 导出的文本常有)、去掉行首缩进、判断是不是目录、建目录或者建文件。`-- ` 是必须的,它告诉 `mkdir` 后面全是参数而不是选项,否则遇到以 `-` 开头的名字就会报“无效选项”。`[[ -e ]] || : >` 这个组合实现了“文件存在就跳过”,比先判断再创建少一次 IO。 它的短板也很明显:一旦 AI 还是给你输出了树形字符,这个脚本会把 `├── src/` 整个当成名字,建出一个叫 `├── src` 的目录。所以我一般会配一个降级处理,用 `sed` 先把树字符洗掉再喂给它。但 `sed` 处理多字节字符在不同 locale 下的行为不一致,这部分我没在 Bash 里死磕,直接劝人上 Python 了。 ### 4.2 Node 版:前端仓库里顺手用 前端同学手里通常已经有 Node 环境,不想为了建目录单独装 Python,所以我写了一版 Node 实现,逻辑和 Python 版一一对应。它用 `node:fs` 的 `mkdirSync` 递归模式,这个 API 从 Node 10 就有了,不用担心兼容。javascript #!/usr/bin/env node import fs from 'node:fs'; import path from 'node:path'; const [, , treeFile, rootArg = '.'] = process.argv; const root = path.resolve(rootArg); const PREFIX = /^[ │|├└┌+─\-—–]+/; const lines = fs.readFileSync(treeFile, 'utf8') .split(/\r?\n/) .filter((l) => l.trim() && !/^\s*(```|~~~)/.test(l)); const spans = lines .map((l) => (l.match(/^( +)\S/) || [])[1]?.length || 0) .filter((n) => n > 0); const unit = spans.length ? Math.min(...spans) : 4; const dict = new Map(); let dirs = 0, files = 0; for (const line of lines) { const prefix = line.match(PREFIX)?.[0] ?? ''; const depth = Math.floor(prefix.length / unit); const name = line.slice(prefix.length).trim(); if (!name || /[<>:"|?*]/.test(name)) continue; const isDir = /[/\\]$/.test(name); const clean = name.replace(/[/\\]+$/, ''); const parent = depth === 0 ? root : dict.get(depth - 1) ?? root; const target = path.join(parent, clean); if (isDir) { fs.mkdirSync(target, { recursive: true }); dict.set(depth, target); dirs++; } else { fs.mkdirSync(path.dirname(target), { recursive: true }); if (!fs.existsSync(target)) { fs.writeFileSync(target, '', 'utf8'); files++; } } } console.log(目录 ${dirs} 个,文件 ${files} 个); ``` 这里我用Math.min(...spans)代替了 Python 版的 gcd,因为大多数情况下最小缩进就等于单位,够用了。如果遇到混排(既有 2 空格又有 4 空格的层级),min会得到 2,深度算出来是对的,反而比 gcd 更鲁棒。这个细节是写第二版的时候才想明白的:别再追求数学上的严谨,实际输入里“最小正缩进就是单位”这条经验规则命中率高得多。 ### 4.3 三种方案怎么选 我把三套实现的取舍整理成了一张表,你可以按自己的环境对号入座。核心结论是:**解析树形字符这件事,Python 的成本最低;只要路径清单,Bash 最轻;前端项目里 Node 最省事**。不要为了统一而强行选一种,工具是拿来用的,不是拿来供奉的。 | 方案 | 适用输入 | 依赖 | 跨平台 | 推荐场景 | |---|---|---|---|---| | Python 完整版 | 树形字符、缩进列表、混合 | 无(标准库) | 好 | 日常主力,建议全局安装 | | Bash 清单版 | 纯路径清单 | 无 | Linux/macOS | 临时救急、服务器上 | | Node 版 | 树形字符、缩进列表 | Node 10+ | 好 | 前端仓库、CI 里 | 还有一点值得提醒:如果你打算把这个脚本放进 CI 或者 npm scripts 里跑,务必选 Python 或 Node,别选 Bash。Bash 脚本对 locale、对多字节字符、对以-开头的文件名都有微妙的依赖,在本地跑得好好的,换个镜像环境就翻车。这种问题排查起来极其费时,不如一开始就用有明确字符语义的运行时。 ## 5. 让它能在真实项目里跑:幂等、安全与跨平台 ### 5.1 dry-run 与幂等 第一次跑脚本之前,一定要先 dry-run。我见过太多人直接把 AI 给的结构图往一个已有仓库里一贴,跑完发现所有路径都多套了一层project/,整个仓库变成了project/project/src。加个--dry-run开关,把所有“将要执行的动作”打印出来,你肉眼扫一遍确认无误再真跑,成本不到十秒,能省掉一次git clean -fd的心惊肉跳。 幂等是另一条底线。所谓幂等,就是同一个脚本连续跑十次,结果和跑一次一样。实现上就靠两个东西:目录用mkdir -p(或者exist_ok=True),文件先判存在再写。我在生成器里对文件是“存在即跳过”,绝不覆盖。这个策略的代价是没法通过重跑脚本来更新已有文件内容,但那个需求本来就不该由建目录脚本来承担——那属于模板引擎或者脚手架工具的范畴,混在一起只会让职责变模糊。 ```bash # 建议的实操顺序 python3 treegen.py tree.txt --root ./my-app --dry-run python3 treegen.py tree.txt --root ./my-app git status --short ``` 第三步git status是我强烈建议加进流程的。它会如实告诉你哪些文件是新增的、哪些目录被合并了。如果输出的列表比你预期的多出一大截,多半是解析时把噪声行当成节点了,这时候回头检查tree.txt的前几行和最后几行,问题通常就藏在那里。 ### 5.2 路径安全校验 如果你的结构图是从外部来的(AI 生成、别人发的、从文档里复制的),那它就是一个不可信输入,必须校验。最典型的攻击或事故是..路径穿越:结构图里写一行└── ../../../etc/xxx,脚本老老实实建到系统目录里去了。我的校验规则是三条:名字不能是.或..;名字里不能含/或`(路径分隔符只能在解析阶段被识别为目录标记,不能出现在名字中间);名字不能含控制字符和<>:"|?*这些在各平台上有特殊含义的符号。python def safe(name): if name in ('.', '..') or not name: return False if '/' in name or '\\' in name: return False return not re.search(r'[<>:"|?*\x00-\x1f]', name)第二条规则在 Linux 上看起来过于严格——Linux 文件名里允许:和|,删掉它们反而是过度限制。但考虑到脚本大概率会在多人之间流转、在 Windows 和 macOS 上都跑,统一禁掉是最省心的做法。真遇到必须带:的名字,我的处理是建完之后手动改名,并在 README 里留一行说明,比在脚本里做平台分支要清爽。 ### 5.3 编码、换行、长路径 编码这事看着基础,翻车的人不少。AI 输出的结构图里经常带中文目录名,比如├── 组件/,如果文件保存成了 GBK 而脚本按 UTF-8 读,解析出来的全是乱码,建出来的目录名打不开也删不掉,非常难受。我的做法是脚本里硬编码encoding='utf-8',并且在读文件之前先检查有没有 BOM 头,有就剥掉。Windows 上用记事本保存的文本默认带 BOM,这个坑我踩过不止一次。 换行符是另一个隐形炸弹。Windows 的\r\n会在行尾多留一个\r,这个字符肉眼看不见,但会让文件名末尾多一个控制字符,ls输出的时候看不出来,等你import的时候就是找不到模块。Python 的splitlines()会自动处理多种换行符,Bash 方案里我手动加了${line%$'\r'}做剥离,Node 里用的是split(/\r?\n/),三条路都堵上了。 长路径的问题主要出现在 Windows,路径总长度超过 260 字符会报错,而深层的node_modules或者嵌套的组件目录很容易超。我的应对是在生成前算一下预估路径长度,超过 240 字符就打警告,让你自己决定要不要简化目录层级。这个 warning 不是阻止执行,只是提醒,因为很多时候你确实需要那么深的目录,只是不知道自己离红线有多近。 ## 6. 踩坑实录与速查表 ### 6.1 高频故障速查 下面这张表是我把自己和身边人遇到的问题汇总之后的版本,基本覆盖了九成以上的情况。遇到问题的第一反应应该是查表,而不是打开调试器。 | 现象 | 大概率原因 | 处理方式 | |---|---|---| | 所有文件都堆在根目录 | 缩进单位探测失败 | 手动加--unit 4或--unit 2| | 生成了一堆叫├──的目录 | 前缀字符集没覆盖到 | 检查是否有特殊的制表符号 | | 目录层级多了一层 | 根节点被重复计算 | 加--strip-root| | 文件名末尾有奇怪字符 | 行尾\r没剥离 | 统一按 UTF-8 + splitlines 处理 | | 中文目录名乱码 | 文件编码不是 UTF-8 | 转码后再跑,或剥 BOM | | 文件建出来了但内容是空的 | 模板配置没生效 | 检查.treegen.json路径和后缀名 | | 已有代码被覆盖 | 用了覆盖策略 | 改成“存在即跳过” | | 报错“路径过长” | Windows 260 限制 | 简化层级或缩短目录名 | | 建出了和预期不符的名字 | AI 在行尾加了注释 | 在提示词里禁止行尾注释 | 表格里最后一条特别值得展开。AI 非常喜欢在结构图的每一行后面加注释,比如├── main.py # 程序入口。如果你的解析器不处理这个,建出来的文件名就是main.py # 程序入口,虽然能建出来,但根本没法用。我的处理是在解析阶段做一次“行尾注释剥离”:如果名字里出现两个以上连续空格后跟#或//,就把那部分砍掉。这个规则有误伤风险(文件名里真的有两个空格),但概率极低,值得换。 ### 6.2 几条用血换来的心得 第一条心得是在提示词里就把格式约束好,这比在解析器里做一百行兜底都划算。我现在固定用这样一段话要求 AI:“请用树形结构输出项目结构,目录以斜杠结尾,不要输出代码围栏,不要在行尾加注释,不要输出除结构图以外的任何文字。”加上这句之后,需要兜底的情况少了八成,脚本也从两百行瘦到了一百行。这是典型的“往上游走一步,下游省十倍力”。 第二条心得是把结构图文件纳入版本管理。这听起来有点反直觉,结构图不是一次性的吗?但实际项目里结构会演进,你把每次生成的结构图存成docs/structure-v1.txt、structure-v2.txt,未来想对比两次重构之间目录变化,直接diff就行,比翻 git log 看谁删了哪个文件夹直观得多。我现在接手陌生项目,第一步就是把当前目录树 dump 一份存下来,作为后续重构的基线。 第三条心得是不要试图让脚本处理一切。有人问我能不能让脚本自动判断“这个文件该不该存在”“这个目录名是不是拼错了”,我的回答是不能,也不该。脚本的职责边界很清楚:把结构图如实映射到

返回列表