Skill 方案 S7 · 项目架构勘查与文档生成
一句话:把「这个项目到底跑着什么、端口怎么分、任务怎么起、依赖是什么」的勘查过程固化成一次扫描,直接产出可提交的
environment.md;两段勘查会话的 1.37 亿 token 可压到 0.20 亿。
一、为什么需要它
11 月 13 日两段勘查会话合计 1.37 亿 token:扫描项目结构 8450 万,环境安装与验证 5217 万。
| 会话 | token | 工具调用 | 产出 |
|---|---|---|---|
| 扫描项目结构 | 8450 万 | 数百次 | 目录树、模块关系 |
| 环境安装与验证 | 5217 万 | 数百次 | 依赖版本、任务定义 |
| 合计 | 1.37 亿 | — | environment.md + 架构认知 |
| 指标 | 数值 |
|---|---|
| 勘查次数 | 2 次(同类任务,重复劳动) |
| 累计 token | 1.37 亿,占两个月 8.7% |
| 最终文档 | environment.md |
| 复用情况 | 写完就沉底,下次勘查又从零开始 |
痛点集中在"广度"上:一次全量 tree /F 会把 node_modules、.next、generated 全部吐出来,几十万行;模型为了不漏,只能一层层读,读了再总结,总结完再补读。没有裁剪规则的勘查,成本随目录规模线性爆炸。
二、触发词与边界
| 触发词 | 不适用场景 |
|---|---|
| 「摸一下这个项目」「架构勘查」「项目结构」 | 只想知道某个文件在哪 → Glob 就够 |
| 「生成 environment.md」「环境文档」 | 要改代码/重构 → 不动手,只记录 |
| 「端口怎么分的」「谁占着 3000」 | 要修端口冲突 → S2 |
| 「依赖是什么版本」「路由有哪些」 | 要升级依赖 → 单独评审 |
| 「更新一下环境文档」 | 首次安装环境 → 走安装流程,本 skill 只记录 |
边界:只读。勘查过程不启动、不停止、不安装任何东西;发现缺失(比如某任务不存在)只记录,不代劳修复。
三、输入与前置检查
| 项目 | 值 |
|---|---|
| 项目根 | C:\Users\cag\WorkBuddy\2026-10-11-06-00-16 |
| 子项目 | portal / kaoyan-vocab / notes / c-lab / ops |
| 排除目录 | node_modules、.next、generated、.git、.shot、.fonttmp、__pycache__ |
| 输出 | 项目根 environment.md |
| 状态文件 | ops/.scan-state.json(记录上次扫描的指纹) |
前置检查三条命令,确认勘查对象在线:
Get-NetTCPConnection -State Listen | Where-Object { $_.LocalPort -in 3000,3001,3102,3103 } |
Select-Object LocalPort, OwningProcess
Get-Process node, cloudflared -ErrorAction SilentlyContinue | Select-Object Id, ProcessName, Path
Get-ScheduledTask -TaskName "Kaoyan*" | Select-Object TaskName, State
若端口全空、任务全 Ready,说明服务没起,勘查结果里必须写"当前未运行",不能用历史印象代替实测。
四、执行步骤
步骤 1 · 裁剪目录树。 只到三层,且排除构建产物:
$R = "C:\Users\cag\WorkBuddy\2026-10-11-06-00-16"
Get-ChildItem $R -Directory -Depth 2 -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -notmatch '\\(node_modules|\.next|generated|\.git|\.shot|\.fonttmp|__pycache__)(\\|$)' } |
ForEach-Object { $_.FullName.Substring($R.Length + 1) } |
Sort-Object
裁剪后通常 60–120 行,可直接贴进文档;未裁剪是数万行。目录树的价值在"有哪些模块",不在"每个文件叫什么",第四层以后按需单独看。
步骤 2 · 采集端口与进程。 端口是架构的第一手证据:
Get-NetTCPConnection -State Listen |
Where-Object { $_.LocalPort -ge 3000 -and $_.LocalPort -le 3200 } |
ForEach-Object {
$p = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
[pscustomobject]@{ Port = $_.LocalPort; Pid = $_.OwningProcess; Name = $p.ProcessName; Path = $p.Path }
} | Sort-Object Port | Format-Table -AutoSize
步骤 3 · 采集计划任务。 任务名、状态、动作三者都要,动作里藏着启动器:
Get-ScheduledTask -TaskName "Kaoyan*" | ForEach-Object {
[pscustomobject]@{
Task = $_.TaskName
State = $_.State
Exec = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join ' ; '
}
} | Format-List
本机已知结论可直接对照:KaoyanPortal(3000)、KaoyanNextServer(3001)、KaoyanNotes(3102)、KaoyanCLab(3103)、KaoyanTunnel、KaoyanSelfHeal、KaoyanNatapp(Disabled)。对不上就是发现,要写进文档的"异常"一节。
步骤 4 · 提取依赖与路由。 依赖只取顶层版本,路由按框架各自的约定扫:
node -e "
const fs=require('fs');
for (const p of ['portal','kaoyan-vocab','notes','c-lab']) {
const f=p+'/package.json';
if(!fs.existsSync(f)) { console.log(p,'(no package.json)'); continue; }
const j=JSON.parse(fs.readFileSync(f,'utf8'));
console.log('##',p,j.name,'next='+(j.dependencies?.next||'-'),'prisma='+(j.dependencies?.['@prisma/client']||'-'));
}"
cd "C:/Users/cag/WorkBuddy/2026-10-11-06-00-16" && \
find notes/app kaoyan-vocab/app -name "page.tsx" -o -name "route.ts" 2>/dev/null | head -60
门户是零依赖 Node,路由写在 portal/server.js 的注释与 urlPath 分支里,用 grep -n "urlPath ===" portal/server.js 取,不要靠猜。
步骤 5 · 采集环境事实。 这一节是上次 5217 万 token 换来的教训,必须固化:
| 事实 | 采集方式 |
|---|---|
| Node 版本与绝对路径 | node -v、(Get-Command node).Source |
| 代理变量(双份会炸) | `Get-ChildItem Env: |
| Prisma 生成目录 | grep output notes/prisma/schema.prisma(当前 ../generated/db3) |
| 提权限制 | 本机注册 S4U 任务报 0x80070005 |
步骤 6 · 生成 environment.md。
# 运行环境
> 生成时间 / 扫描耗时 / 指纹
## 1. 服务与端口 (表:服务、端口、启动器、日志、探针)
## 2. 计划任务 (表:任务名、状态、动作、触发器)
## 3. 目录结构 (裁剪后的树,三层)
## 4. 依赖版本 (表:子项目、框架、关键依赖)
## 5. 路由清单 (表:路径、类型、处理位置)
## 6. 环境事实与已知坑 (Node 路径、代理、Prisma 目录、权限限制)
## 7. 本次异常 (对不上的项,逐条列,不解释)
步骤 7 · 增量更新。 用指纹判断是否需要重扫,避免每次全量:
node -e "
const fs=require('fs'),c=require('crypto');
const files=['portal/server.js','notes/package.json','kaoyan-vocab/package.json','notes/prisma/schema.prisma'];
const h=c.createHash('sha1');
for(const f of files){ if(fs.existsSync(f)) h.update(fs.readFileSync(f)); }
const fp=h.digest('hex').slice(0,12);
const prev=fs.existsSync('ops/.scan-state.json')?JSON.parse(fs.readFileSync('ops/.scan-state.json','utf8')).fingerprint:null;
console.log(JSON.stringify({fingerprint:fp,changed:fp!==prev}));
fs.writeFileSync('ops/.scan-state.json',JSON.stringify({fingerprint:fp,at:new Date().toISOString()},null,2));
"
changed=false 时只更新"本次异常"与时间戳,其余章节原样保留——这一条能把第二次勘查的成本压到接近零。
五、输出与验收
产出 environment.md(约 150–250 行)+ 一段 JSON 摘要:
{"services":5,"tasks":7,"routes":38,"changed":true,"anomalies":1,"elapsedSec":42}
三条硬断言:
- 每个处于 Listen 的 3000–3200 端口,必须在文档里有对应行;有端口没记录即失败。
- 每个
Kaoyan*任务必须在文档里有状态,Disabled 的也要写(KaoyanNatapp就属于这一类)。 portal/server.js里每一处urlPath ===分支,都必须在路由表出现;缺一条说明扫描是抽样而非全量。
六、失败处理与回滚
| 现象 | 根因 | 动作 |
|---|---|---|
| 目录树几千行 | 排除规则没生效 | 检查正则转义,\\ 在 PowerShell 字符串里要写对 |
| 端口有监听但查不到进程 | 权限不足(SYSTEM 进程) | 记录端口与 PID,进程名写「需提权查看」 |
| 任务动作是乱码 | 输出编码 | chcp 65001 或改用 PowerShell 工具直接读对象 |
| 路由数明显偏少 | find 被 -o 优先级坑了 | 加括号 \( -name a -o -name b \) |
| 文档与实况不符 | 用了历史印象 | 重跑步骤 2–4,一切以实测为准 |
回滚:勘查只读,唯一产物是 environment.md。写坏了用 Move-Item environment.md environment.md.bak-<ts> 后重生成;ops/.scan-state.json 删掉即触发一次全量重扫。
七、收益测算
| 项目 | 现状 | 改造后 |
|---|---|---|
| 单次 token | 6850 万 | 1000 万 |
| 次数 | 2 次 | 2 次(第二次走增量,约 200 万) |
| 累计 | 1.37 亿 | 0.20 亿 |
| 单次耗时 | 40–90 分钟 | 1–3 分钟 |
| 文档时效 | 一次性 | 指纹变化才重扫 |
省的是"逐层读目录 + 反复确认版本"。裁剪规则把可读行数从数万压到百行,增量机制让第二次勘查几乎免费。节省 1.17 亿,约 85%,是七类里绝对金额最大的一项。