首頁所有文章關於本站
cover image
發佈 May 1, 2025 更新 May 6, 2025

AI 一直重寫你寫過的功能?「快照」幫你省下解釋專案的工夫

先講結論

如果能對專案產生一個「快照」,快照的資訊足以讓 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` 的程式碼,並在註解中標示每個區塊的功能用途。
文章目錄
首頁所有文章關於本站

© 2024 Jackle Chen. All rights reserved.