先講結論

如果能對專案產生一個「快照」,快照的資訊足以讓 AI 快速進入狀況,但又不會耗很多 token,在很多開發狀況下很好用,也能大幅減少 AI 出乎意料的行為

本篇文章結尾我會提供我的 snapshot.js 腳本程式碼與 prompt 模板,你可以依照你的需求調整使用

為什麼 AI 老是重寫你已經寫過的功能?

你是不是常遇到請 AI 幫忙改個功能,結果它不知道你已經寫過哪些工具函式,又再寫了一個一樣的?

不是 AI 蠢,是你沒讓它知道

AI 並不會「自己去看你的整個專案」,它只看你丟給它的內容。你如果沒提供,它自然不知道你有哪些函式、哪些工具已經寫好,他只是盡責的自作主張再寫一次

懶人的做法就是「整包都餵給 AI」搭配我之前介紹的 Gitingest,但這做法有可能 context window 爆掉(Gemini 表示 😅)

省 context 的做法就是「自己勤勞點整理文件」,平常有在維護文檔這時候就很好用,但這做法還是有問題:

  1. 會忘記同步更新整理文件
  2. 你要拉給 AI 的檔案也會漏東漏西
  3. AI 永遠不知道你漏了什麼
  4. 所以還是會亂寫

因此,我們需要一個「快速生成,一個頂好幾個」的專案摘要快照 snapshot

Snapshot 是什麼?

這要先感謝受到海總理的電子報的啟發

簡單說,用一個自動化腳本幫你:

  • 掃描整個專案目錄(排除像 node_modules.git 這些無用資料夾)
  • 把每個檔案的結構與函式列出來(含註解)
  • 整理出專案使用的套件依賴
  • 輸出成一份 snapshot.md

接下來找 AI 幫忙時,就把這個檔案丟給他就好

snapshot示範圖
結構大概就長這樣

使用體驗

AI 如果不知道你專案有哪些東西,它只能亂猜。

snapshot 就是給 AI 看的地圖:讓它知道你寫過哪些東西、用了哪些工具、怎麼接起來。

我自己使用經驗是:除非我只需要拉一個檔案,不然拉三個以上檔案參考,我還要想,那我還不如丟 snapshot,直接懶出新高度

snapshot 是一個夠好用的 context 內容,像萬金油一樣

他就是 Jimmy Butler,兼容性高,有他很難輸,但不是穩贏(這不是廢話)

Snapshot.js 範例程式碼

同步更新在 Github

以下是我的 snapshot 腳本:

/**
 * snapshot.js
 * 用途:掃描任意 JS/TS/Vue 專案,輸出專案結構、函式清單與依賴清單至 snapshot.md
 * 使用方式:在專案根目錄執行 `node snapshot.js`
 * 僅依賴 Node.js 內建模組:fs, path
 */
///////////////////////////////////////
// 1. 可編輯設定區
///////////////////////////////////////
/**
 * 要排除掃描的檔案或資料夾(相對於專案根目錄)
 * 使用者可自行新增或移除
 */
const EXCLUDES = ["node_modules", ".git", "dist", "build"];
/** 支援的檔案副檔名,可按需擴充 */
const FILE_EXTENSIONS = [".js", ".ts", ".vue"];
/** 目錄樹最大深度,0 或 null 表示不限 */
const MAX_DEPTH = 0;
/** 解析規則:Controller 物件模式與 Export 函式模式 */
const PARSERS = {
  controller: {
    test: /(?:module\\.exports\\s*=\\s*|\\bexport\\s+default\\s*)\\{/,
    // 方法名稱與參數
    regex: /(\\w+)\\s*:\\s*(?:async\\s*)?function\\s*(?:\\w*)\\s*\\(([^)]*)\\)/g,
    comment: /\\/\\/\\s*(.*)/,
  },
  export: {
    test: /export\\s+(?:async\\s+)?(?:function|const)/,
    // 匹配 export function foo(...) 或 export const useXxx = (...) =>
    regex:
      /export\\s+(?:async\\s+)?function\\s+(\\w+)\\s*\\(([^)]*)\\)|export\\s+const\\s+(\\w+)\\s*=\\s*(?:async\\s*)?\\(?([^)]*)\\)?\\s*=>/g,
    comment: /\\/\\/\\s*(.*)/,
  },
};
///////////////////////////////////////
// 2. 引入 Node 內建模組
///////////////////////////////////////
const fs = require("fs");
const path = require("path");
///////////////////////////////////////
// 3. 判斷是否排除路徑
///////////////////////////////////////
function isExcluded(filePath) {
  return EXCLUDES.some((ex) => filePath.includes(ex));
}
///////////////////////////////////////
// 4. 遞迴掃描檔案
///////////////////////////////////////
function scanFiles(dir, fileList = []) {
  const entries = fs.readdirSync(dir);
  for (const name of entries) {
    const fullPath = path.join(dir, name);
    if (isExcluded(fullPath)) continue;
    const stat = fs.statSync(fullPath);
    if (stat.isDirectory()) {
      scanFiles(fullPath, fileList);
    } else if (FILE_EXTENSIONS.includes(path.extname(name))) {
      fileList.push(fullPath);
    }
  }
  return fileList;
}
///////////////////////////////////////
// 5. 解析檔案中函式/方法
///////////////////////////////////////
function parseFile(filePath) {
  const content = fs.readFileSync(filePath, "utf-8");
  let parser = null;
  if (PARSERS.controller.test.test(content)) {
    parser = PARSERS.controller;
  } else if (PARSERS.export.test.test(content)) {
    parser = PARSERS.export;
  } else {
    return []; // 無匹配解析模式
  }
  const lines = content.split(/\\r?\\n/);
  const results = [];
  lines.forEach((line, idx) => {
    parser.regex.lastIndex = 0;
    let match;
    while ((match = parser.regex.exec(line)) !== null) {
      // 取出函式名稱與參數
      const name = match[1] || match[3];
      const params = match[2] || match[4] || "";
      // 嘗試讀取上一行的單行註解
      let comment = "";
      if (idx > 0) {
        const prev = lines[idx - 1].match(parser.comment);
        if (prev) comment = prev[1].trim();
      }
      results.push({ name, params, comment });
    }
  });
  return results;
}
///////////////////////////////////////
// 6. 生成 ASCII 目錄樹
///////////////////////////////////////
function buildTree(dir, prefix = "", depth = 1) {
  if (MAX_DEPTH && depth > MAX_DEPTH) return "";
  let tree = "";
  const entries = fs
    .readdirSync(dir)
    .filter((name) => !isExcluded(path.join(dir, name)))
    .sort();
  entries.forEach((name, idx) => {
    const fullPath = path.join(dir, name);
    const isDir = fs.statSync(fullPath).isDirectory();
    const connector = idx === entries.length - 1 ? "└── " : "├── ";
    tree += `${prefix}${connector}${name}\\n`;
    if (isDir) {
      const newPrefix = prefix + (idx === entries.length - 1 ? "    " : "│   ");
      tree += buildTree(fullPath, newPrefix, depth + 1);
    }
  });
  return tree;
}
///////////////////////////////////////
// 7. 搜尋各子專案的 package.json
///////////////////////////////////////
function findPackages(dir, list = []) {
  const entries = fs.readdirSync(dir);
  for (const name of entries) {
    const fullPath = path.join(dir, name);
    if (isExcluded(fullPath)) continue;
    const stat = fs.statSync(fullPath);
    if (stat.isDirectory()) {
      findPackages(fullPath, list);
    } else if (name === "package.json") {
      list.push(fullPath);
    }
  }
  return list;
}
///////////////////////////////////////
// 8. 主流程:掃描、解析、組合 Markdown
///////////////////////////////////////
function main() {
  const root = process.cwd();
  // 目錄樹
  const tree = buildTree(root);
  // 掃描所有檔案並解析函式
  const files = scanFiles(root);
  const funcMap = {}; // filePath -> [ {name, params, comment}, ... ]
  files.forEach((fp) => {
    const rel = path.relative(root, fp);
    const funcs = parseFile(fp);
    if (funcs.length) funcMap[rel] = funcs;
  });
  // 收集依賴
  const pkgFiles = findPackages(root);
  const depsMap = {}; // projectName -> { dependencies, devDependencies }
  pkgFiles.forEach((pf) => {
    try {
      const data = JSON.parse(fs.readFileSync(pf, "utf-8"));
      const proj = data.name || path.basename(path.dirname(pf));
      depsMap[proj] = {
        dependencies: data.dependencies || {},
        devDependencies: data.devDependencies || {},
      };
    } catch (err) {
      console.warn(`解析 ${pf} 時發生錯誤:${err.message}`);
    }
  });
  // 組合 Markdown
  let md = "";
  // 1. 專案目錄結構
  md += "## 專案目錄結構\\n\\n";
  md += "```text\\n" + tree + "```\\n\\n";
  // 2. 函式清單
  md += "## 函式清單\\n\\n";
  for (const [file, funcs] of Object.entries(funcMap)) {
    md += `### ${file}\\n`;
    funcs.forEach((f) => {
      md += `- **${f.name}(${f.params})**${
        f.comment ? " - " + f.comment : ""
      }\\n`;
    });
    md += "\\n";
  }
  // 3. 依賴清單
  md += "## 依賴清單\\n\\n";
  for (const [proj, info] of Object.entries(depsMap)) {
    md += `## ${proj}\\n\\n`;
    md += "### devDependencies\\n";
    if (Object.keys(info.devDependencies).length) {
      md +=
        "```json\\n" + JSON.stringify(info.devDependencies, null, 2) + "\\n```\\n";
    } else {
      md += "無\\n";
    }
    md += "\\n### dependencies\\n";
    if (Object.keys(info.dependencies).length) {
      md +=
        "```json\\n" + JSON.stringify(info.dependencies, null, 2) + "\\n```\\n";
    } else {
      md += "無\\n";
    }
    md += "\\n";
  }
  // 輸出至 snapshot.md
  fs.writeFileSync(path.join(root, "snapshot.md"), md, "utf-8");
  console.log("已生成 snapshot.md");
}
// 執行主流程
main();

你可以根據需求修改這支腳本,或是叫 AI 幫你改,你也可以重新生成這個腳本

修改這個 Prompt,生成你的 Snapshot 腳本

請幫我撰寫一支名為 `snapshot.js` 的 Node.js 單檔腳本,用於任意 JavaScript/TypeScript 專案,生成完整專案結構與程式函式快照,並匯出成 Markdown 文件 `snapshot.md`。請確保它具備以下通用功能,並且易於客製:
1. **排除清單設定**
   - 腳本開頭定義一個可編輯的陣列 `EXCLUDES`,列出所有要忽略的檔案或目錄(相對專案根目錄的名稱或路徑片段)。使用者可自由新增或刪除。
2. **檔案掃描**
   - 遞迴尋訪整個專案,搜尋所有副檔名為 `.js`、`.ts`、`.vue`(可依需求自行擴充)的檔案。
   - 自動跳過在 `EXCLUDES` 裡設定的路徑或檔案。
3. **程式碼解析(可自訂)**
   - 預設支援兩種解析模式,可根據檔案特性自訂:
     - **Controller 物件模式**:若檔案匯出一個物件(如 `module.exports = { … }` 或 `export default { … }`),只擷取該物件內的方法名稱與簽名,並可抓取上方的單行註解。
     - **Export 函式模式**:對於一般模組或 Vue `<script>`,擷取所有以 `export function`、`export async function` 或 `export const useXxx =` 等形式定義的函式/composable,並可抓取上方的單行註解。
   - 使用者只要在程式中標註或調整對應的正則或檢測邏輯,即可快速替換成自訂解析規則。
4. **目錄樹生成**
   - 以純文字 ASCII 樹狀圖形式顯示整個專案結構,深度與格式均可在程式中設定。
5. **多專案依賴收集**
   - 自動搜尋所有子目錄下名為 `package.json` 的檔案,讀取其 `"name"` 欄位(或根據需要自訂映射),並擷取其中的 `dependencies` 與 `devDependencies`。
   - 輸出格式為:
     ```md
     ## 專案名稱
     ### devDependencies
         "套件A": "版本",
         ...
     ### dependencies
         "套件X": "版本",
         ...
     ```
6. **Markdown 輸出**
   - 統一將結果寫入 `snapshot.md`,結構清晰分段:
     1. `## 專案目錄結構`(樹狀圖)
     2. `## 函式清單`(按檔案分組列出函式與註解)
     3. `## 依賴清單`(按專案分組列出套件版本)
7. **執行環境與相依**
   - 僅使用 Node.js 內建模組(`fs`, `path`),不依賴第三方套件;
   - 在腳本最上方加入 shebang (`#!/usr/bin/env node`);
   - 支援同步與非同步皆可,自由選擇;
   - 使用者只需放到專案根目錄,執行:
     ```bash
     node snapshot.js
     ```
   - 每個功能區塊都請加上清楚註解,方便使用者按需求開關或修改。
---
請依照上述通用規格,完整生成 `snapshot.js` 的程式碼,並在註解中標示每個區塊的功能用途。