├── cover.png ├── package.json ├── LICENSE ├── .gitignore ├── README.md ├── README_en.md └── scripts └── daily-summary.ts /cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanbingxyz/daily-commit-summarizer/HEAD/cover.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "daily-commit-summarizer", 3 | "version": "1.0.0", 4 | "description": "A GitHub Action that summarizes daily commits with AI and sends the report to your team via configurable webhooks.", 5 | "main": "scripts/daily-summary.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nanbingxyz/daily-commit-summarizer.git" 12 | }, 13 | "author": "Ironben ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/nanbingxyz/daily-commit-summarizer/issues" 17 | }, 18 | "homepage": "https://github.com/nanbingxyz/daily-commit-summarizer#readme" 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Ironben 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional stylelint cache 57 | .stylelintcache 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variable files 69 | .env 70 | .env.* 71 | !.env.example 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | .parcel-cache 76 | 77 | # Next.js build output 78 | .next 79 | out 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and not Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # vuepress v2.x temp and cache directory 95 | .temp 96 | .cache 97 | 98 | # Sveltekit cache directory 99 | .svelte-kit/ 100 | 101 | # vitepress build output 102 | **/.vitepress/dist 103 | 104 | # vitepress cache directory 105 | **/.vitepress/cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # Firebase cache directory 120 | .firebase/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v3 129 | .pnp.* 130 | .yarn/* 131 | !.yarn/patches 132 | !.yarn/plugins 133 | !.yarn/releases 134 | !.yarn/sdks 135 | !.yarn/versions 136 | 137 | # Vite logs files 138 | vite.config.js.timestamp-* 139 | vite.config.ts.timestamp-* 140 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Daily Commit Summarizer 2 | 3 | ![cover](./cover.png) 4 | 5 | [English](./README_en.md) 6 | 7 | ## 📌 使用场景 8 | 9 | 软件团队往往希望快速了解一天内代码库里发生了什么,而不是翻遍冗长的 git log 或大型 PR。 10 | 11 | 这个项目提供了一个 GitHub Actions 工作流 和 TypeScript 脚本,实现以下功能: 12 | 1. 每天北京时间 18:00(UTC+8)自动运行。 13 | 2. 收集当天在所有远程分支上的提交。 14 | 3. 借助 LLM(例如 OpenAI GPT-4.1-mini): 15 | * 将大型 diff 拆分为可管理的片段。 16 | * 为每个提交单独生成摘要(包含变更内容、影响、风险、测试建议)。 17 | * 最后合并成一份每日总结报告。 18 | 4. 通过 Webhook 将总结发送到飞书群聊。 19 | 20 | 这样,团队每天都能收到一份简明、人类可读的变更日志,提高透明度,减少代码审查的时间成本。 21 | 22 |
23 | 24 | ## 🚀 功能特点 25 | 1. 跨分支覆盖:支持分析所有 origin/* 分支上的提交。 26 | 2. 大 diff 切分:安全处理大规模提交,避免超出 LLM 上下文限制。 27 | 3. 多层次总结:单个 diff 片段 → 单次提交 → 每日汇总。 28 | 4. 飞书通知:每日简报自动推送至群聊。 29 | 5. 高度可配置:可调整模型、分支过滤、diff 拆分大小等参数。 30 | 31 |
32 | 33 | ## ⚙️ 使用方法 34 | 35 | **1. 克隆或 Fork 仓库** 36 | 37 | ```bash 38 | git clone https://github.com/nanbingxyz/daily-commit-summarizer.git 39 | cd daily-commit-summarizer 40 | ``` 41 | 42 | **2. 添加 GitHub Actions 工作流** 43 | 44 | 在 .github/workflows/daily-summary.yml 中加入: 45 | ```yaml 46 | name: Daily LLM Commit Summary 47 | 48 | on: 49 | schedule: 50 | - cron: "0 10 * * *" # 10:00 UTC = 18:00 北京时间 51 | workflow_dispatch: {} # 手动触发 52 | 53 | jobs: 54 | run: 55 | runs-on: ubuntu-latest 56 | env: 57 | TZ: Asia/Shanghai 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v4 61 | with: 62 | fetch-depth: 0 63 | 64 | - name: Setup Node.js 65 | uses: actions/setup-node@v4 66 | with: 67 | node-version: "20" 68 | 69 | - name: Install dependencies 70 | run: | 71 | npm install 72 | 73 | - name: Run summarizer 74 | env: 75 | OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} 76 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 77 | LARK_WEBHOOK_URL: ${{ secrets.LARK_WEBHOOK_URL }} 78 | REPO: ${{ secrets.REPO }} 79 | run: | 80 | npx tsx scripts/daily-summary.ts 81 | ``` 82 | 83 | **3. 添加仓库密钥** 84 | 85 | 进入 Repo → Settings → Secrets and variables → Actions → New repository secret: 86 | 1. OPENAI_API_KEY → OpenAI 或兼容 LLM 服务的 API Key 87 | 2. OPENAI_BASE_URL → LLM 服务的基础 URL 88 | 3. LARK_WEBHOOK_URL → 飞书群自定义机器人 Webhook 地址 89 | 4. REPO → 你的 GitHub 仓库名 90 | 91 | **4. 安装依赖** 92 | 93 | `npm install` 94 | 95 |
96 | 97 | ## 📄 脚本说明 98 | 99 | scripts/daily-summary.ts 是核心逻辑: 100 | 1. 获取所有远程分支 (git fetch --all)。 101 | 2. 收集当天的提交 (git log --since "midnight" --until "now" --all)。 102 | 3. 生成 diff 并进行切分。 103 | 4. 调用 LLM API,生成结构化的提交摘要。 104 | 5. 合并为当日报告。 105 | 6. 通过 Webhook 推送至飞书。 106 | 107 | ## 🖥 飞书示例输出 108 | ```markdown 109 | # 2025-08-22 每日提交报告 (your-repo) 110 | 111 | 1. 总览 112 | - 修复登录流程中的 bug 113 | - 新增发票相关 API 114 | - 调整开发流水线配置 115 | 116 | 2. 按分支的主要改动 117 | - origin/feature/auth: 新增 JWT 校验中间件 118 | - origin/hotfix/payment: 修复货币转换的舍入错误 119 | 120 | 3. 风险与回滚 121 | - 鉴权中间件可能影响旧客户端 → 建议在预发环境验证 122 | - 支付修复涉及公共工具 → 需要回归测试 123 | 124 | 4. 测试建议 125 | - 增加 JWT 过期的单元测试 126 | - 新增发票创建 API 的集成测试 127 | 128 | 5. 其他说明 129 | - 忽略了 lockfile 更新 130 | ``` 131 | 132 |
133 | 134 | ## 🔧 配置项 135 | 136 | |变量名|默认|说明| 137 | |---|---|---| 138 | MODEL_NAME|gpt-4.1-mini|使用的 LLM 模型 139 | PER_BRANCH_LIMIT|200|每个分支每日最多分析的提交数 140 | DIFF_CHUNK_MAX_CHARS|80000|每次请求最大 diff 字符数 141 | TZ|Asia/Shanghai|定义 “今天” 的时区 142 | 143 |
144 | 145 | ## 📌 注意事项 146 | * 飞书纯文本消息不支持 Markdown。 147 | * 如果需要富文本格式(标题、链接、列表等),请考虑在 postToLark() 中使用 msg_type: post。 148 | * 私有仓库需注意:不要将代码上传至第三方 LLM 服务,除非符合公司合规要求。可替换为内部 LLM 网关。 149 | * 由于本人使用的是 Azure OpenAI, 请求路径与 OpenAI API 的不同,若使用其他服务请自行调整。 150 | 151 |
152 | 153 | ## 🤝 贡献方式 154 | 155 | 欢迎贡献!一些扩展方向: 156 | * 增加 Slack / Discord / MS Teams 的适配器。 157 | * 除了每日摘要,还支持在 PR 中直接生成评论。 158 | * 输出扩展:比如展示修改文件数、代码行数统计等。 159 | 160 |
161 | 162 | ## 📜 许可证 163 | 164 | MIT License,自行承担使用风险。 165 | 166 | ## 🔍 你可能感兴趣 167 | 168 | [![issue2task](https://img.shields.io/badge/GitHub-issue2task-blue?logo=github)](https://github.com/nanbingxyz/issue2task) 169 | 170 | **[issue2task](https://github.com/nanbingxyz/issue2task)** —— 一个 Python 工具,可以把冗长的 GitHub Issue(含全部评论)通过 AI 总结为简洁、可执行的任务, 171 | 并可自动添加到 GitHub Project v2 看板中。 172 | 非常适合将复杂的讨论转化为清晰的下一步行动。 173 | -------------------------------------------------------------------------------- /README_en.md: -------------------------------------------------------------------------------- 1 | # Daily Commit Summarizer 2 | 3 | ![cover](./cover.png) 4 | 5 | ## 📌 Scenario 6 | 7 | Software teams often want a quick overview of what happened in their repositories each day without reading through raw git log or giant pull requests. 8 | 9 | This project provides a GitHub Actions workflow and a TypeScript script that: 10 | 1. Runs every day at 6:00 PM Beijing time (UTC+8). 11 | 2. Collects all commits from all remote branches made during the day. 12 | 3. Uses an LLM (e.g., OpenAI GPT-4.1-mini) to: 13 | - Split large diffs into manageable chunks. 14 | - Summarize each commit individually (changes, impact, risks, test suggestions). 15 | - Merge everything into a daily summary report. 16 | 4. Sends the summary to a Feishu (Lark) group chat via Webhook. 17 | 18 | This gives your team a concise, human-readable daily changelog that improves visibility and saves review time. 19 | 20 |
21 | 22 | ## 🚀 Features 23 | 24 | 1. Cross-branch coverage: analyzes commits from all origin/* branches. 25 | 2. Chunked diffs: handles large commits safely within LLM context limits. 26 | 3. Multi-level summarization:per-diff-chunk → per-commit → daily overview. 27 | 4. Feishu notifications: daily digest delivered to your chat group. 28 | 5. Configurable: choose model, branch filters, chunk sizes, etc. 29 | 30 |
31 | 32 | ## ⚙️ Setup 33 | 34 | **1. Fork or clone this repo** 35 | 36 | ```bash 37 | git clone https://github.com/nanbingxyz/daily-commit-summarizer.git 38 | cd daily-commit-summarizer 39 | ``` 40 | 41 | **2. Add GitHub Actions workflow** 42 | 43 | In .github/workflows/daily-summary.yml: 44 | 45 | ```yaml 46 | name: Daily LLM Commit Summary 47 | 48 | on: 49 | schedule: 50 | - cron: "0 10 * * *" # 10:00 UTC = 18:00 Beijing 51 | workflow_dispatch: {} # allow manual runs 52 | 53 | jobs: 54 | run: 55 | runs-on: ubuntu-latest 56 | env: 57 | TZ: Asia/Shanghai 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v4 61 | with: 62 | fetch-depth: 0 63 | 64 | - name: Setup Node.js 65 | uses: actions/setup-node@v4 66 | with: 67 | node-version: "20" 68 | 69 | - name: Install dependencies 70 | run: | 71 | npm install 72 | 73 | - name: Run summarizer 74 | env: 75 | OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }} 76 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 77 | LARK_WEBHOOK_URL: ${{ secrets.LARK_WEBHOOK_URL }} 78 | REPO: ${{ secrets.REPO }} 79 | run: | 80 | npx tsx scripts/daily-summary.ts 81 | ``` 82 | **3. Add repository secrets** 83 | 84 | Go to Repo → Settings → Secrets and variables → Actions → New repository secret: 85 | * OPENAI_API_KEY → Your OpenAI (or compatible LLM provider) API key. 86 | * OPENAI_BASE_URL → Your OpenAI (or compatible LLM provider) base URL. 87 | * LARK_WEBHOOK_URL → Feishu group Custom Bot Webhook URL. 88 | * REPO → Your GitHub repository name. 89 | 90 | **4. Install dependencies** 91 | 92 | `npm install` 93 | 94 |
95 | 96 | ## 📄 Script Overview 97 | 98 | scripts/daily-summary.ts does the heavy lifting: 99 | 1. Fetches all remote branches (git fetch --all). 100 | 2. Collects today’s commits (git log --since "midnight" --until "now" --all). 101 | 3. Builds diffs, splits them into chunks. 102 | 4. Calls the LLM API for structured commit summaries. 103 | 5. Merges everything into a daily report. 104 | 6. Posts the result to Feishu via Webhook. 105 | 106 |
107 | 108 | ## 🖥 Example Output (Feishu) 109 | 110 | ```markdown 111 | # 2025-08-22 Daily Commit Report (your-repo) 112 | 113 | 1. Overview 114 | - Bug fixes in login flow 115 | - New API endpoint for invoices 116 | - Config refactor in dev pipeline 117 | 118 | 2. Key Changes by Branch 119 | - origin/feature/auth: Added JWT validation middleware 120 | - origin/hotfix/payment: Fixed rounding error in currency converter 121 | 122 | 3. Risks & Rollback 123 | - Auth middleware may break legacy clients → verify with staging 124 | - Payment hotfix touches shared utility → regression test required 125 | 126 | 4. Testing Suggestions 127 | - Add unit tests for JWT expiry 128 | - Integration test for invoice creation API 129 | 130 | 5. Notes 131 | - Lockfile updates ignored 132 | ``` 133 | 134 |
135 | 136 | ## 🔧 Configuration 137 | 138 | You can tweak behavior with variables: 139 | 140 | |Variable|Default|Description| 141 | | ---- | ---- | ---- | 142 | |MODEL_NAME|gpt-4.1-mini|LLM model to call| 143 | |PER_BRANCH_LIMIT|200|Max commits per branch per day| 144 | |DIFF_CHUNK_MAX_CHARS|80000|Max diff characters per LLM request| 145 | |TZ|Asia/Shanghai|Timezone for “today”| 146 | 147 |
148 | 149 | ## 📌 Notes 150 | * Feishu text messages do not support Markdown. 151 | * For rich formatting (titles, links, lists), consider switching to msg_type: post in postToLark(). 152 | * For private repos: don’t leak code to third-party LLMs unless compliant. You can swap OpenAI with your internal LLM gateway. 153 | * Since I am using Azure OpenAI, the request path is different from the OpenAI API. If you are using another service, please adjust accordingly. 154 |
155 | 156 | ## 🤝 Contributing 157 | 158 | Contributions welcome! Ideas: 159 | * Add Slack/Discord/MS Teams adapters. 160 | * Support PR comments in addition to daily digest. 161 | * Extend output with diff statistics (files changed, LOC). 162 | 163 |
164 | 165 | ## 📜 License 166 | 167 | MIT License. Use at your own risk. 168 | 169 | ## 🔍 You May Also Like 170 | 171 | [![issue-to-project-task](https://img.shields.io/badge/GitHub-issue2task-blue?logo=github)](https://github.com/nanbingxyz/issue2task) 172 | 173 | **[issue2task](https://github.com/nanbingxyz/issue2task)** — A Python tool that turns long GitHub issues into concise, actionable tasks with AI, 174 | and can automatically add them to your Project v2 board. 175 | Perfect for transforming messy discussions into clear next steps. 176 | -------------------------------------------------------------------------------- /scripts/daily-summary.ts: -------------------------------------------------------------------------------- 1 | // scripts/daily-summary.ts 2 | // 运行前:确保在 GitHub Actions 或本地 shell 中已设置: 3 | // - OPENAI_API_KEY:LLM 密钥(可替换为企业网关) 4 | // - OPENAI_BASE_URL:LLM API 地址(可替换为自建网关) 5 | // - LARK_WEBHOOK_URL:飞书自定义机器人 Webhook (也可替换为其他通知 Webhook ) 6 | // 可选: 7 | // - PER_BRANCH_LIMIT:每个分支最多统计的“今日提交”条数(默认 200) 8 | // - DIFF_CHUNK_MAX_CHARS:单次送模的最大字符数(默认 80000) 9 | // - MODEL_NAME:指定模型名称(默认 gpt-4.1-mini) 10 | // - REPO:owner/repo(Actions 内自动注入) 11 | 12 | import { execSync } from "node:child_process"; 13 | import https from "node:https"; 14 | 15 | // ------- 环境变量 ------- 16 | const OPENAI_BASE_URL = process.env.OPENAI_BASE_URL || "https://api.openai.com"; 17 | const OPENAI_API_KEY = process.env.OPENAI_API_KEY || ""; 18 | const LARK_WEBHOOK_URL = process.env.LARK_WEBHOOK_URL || ""; 19 | const REPO = process.env.REPO || ""; // e.g. "org/repo" 20 | const MODEL_NAME = process.env.MODEL_NAME || "gpt-4.1-mini"; 21 | const PER_BRANCH_LIMIT = parseInt(process.env.PER_BRANCH_LIMIT || "200", 10); 22 | const DIFF_CHUNK_MAX_CHARS = parseInt( 23 | process.env.DIFF_CHUNK_MAX_CHARS || "80000", 24 | 10, 25 | ); 26 | 27 | if (!OPENAI_API_KEY) { 28 | console.error("Missing OPENAI_API_KEY"); 29 | process.exit(1); 30 | } 31 | 32 | // ------- 工具函数 ------- 33 | function sh(cmd: string) { 34 | return execSync(cmd, { 35 | stdio: ["ignore", "pipe", "pipe"], 36 | encoding: "utf8", 37 | }).trim(); 38 | } 39 | 40 | function safeArray(xs: T[] | undefined | null) { 41 | return Array.isArray(xs) ? xs : []; 42 | } 43 | 44 | // ------- 分支与提交收集(覆盖 origin/* 全分支)------- 45 | const since = "midnight"; // 受 TZ=America/Los_Angeles 影响 46 | const until = "now"; 47 | 48 | // 拉全远端(建议在 workflow 里执行:git fetch --all --prune --tags) 49 | // 这里再次保险 fetch 一次,避免本地调试遗漏 50 | try { 51 | sh(`git fetch --all --prune --tags`); 52 | } catch { 53 | // ignore 54 | } 55 | 56 | // 列出所有 origin/* 远端分支,排除 origin/HEAD 57 | const remoteBranches = sh( 58 | `git for-each-ref --format="%(refname:short)" refs/remotes/origin | grep -v "^origin/HEAD$" || true`, 59 | ) 60 | .split("\n") 61 | .map((s) => s.trim()) 62 | .filter(Boolean); 63 | 64 | // 分支白名单/黑名单(如需):在此可用正则筛选 remoteBranches 65 | 66 | type CommitMeta = { 67 | sha: string; 68 | title: string; 69 | author: string; 70 | url: string; 71 | branches: string[]; // 该提交归属的分支集合 72 | }; 73 | 74 | const branchToCommits = new Map(); 75 | for (const rb of remoteBranches) { 76 | const list = sh( 77 | `git log ${rb} --no-merges --since="${since}" --until="${until}" --pretty=format:%H --reverse || true`, 78 | ) 79 | .split("\n") 80 | .map((s) => s.trim()) 81 | .filter(Boolean); 82 | branchToCommits.set(rb, list.slice(-PER_BRANCH_LIMIT)); 83 | } 84 | 85 | // 反向映射:提交 → 出现的分支集合 86 | const shaToBranches = new Map>(); 87 | for (const [rb, shas] of branchToCommits) { 88 | for (const sha of shas) { 89 | if (!shaToBranches.has(sha)) shaToBranches.set(sha, new Set()); 90 | shaToBranches.get(sha)!.add(rb); 91 | } 92 | } 93 | 94 | // 在所有分支联合视图中获取今天的提交,按时间从早到晚,再与 shaToBranches 交集过滤 95 | const allShasOrdered = sh( 96 | `git log --no-merges --since="${since}" --until="${until}" --all --pretty=format:%H --reverse || true`, 97 | ) 98 | .split("\n") 99 | .map((s) => s.trim()) 100 | .filter(Boolean); 101 | 102 | const seen = new Set(); 103 | const commitShas = allShasOrdered.filter((sha) => { 104 | if (seen.has(sha)) return false; 105 | if (!shaToBranches.has(sha)) return false; // 仅统计出现在 origin/* 的提交 106 | seen.add(sha); 107 | return true; 108 | }); 109 | 110 | if (commitShas.length === 0) { 111 | console.log("📭 今天所有分支均无有效提交。结束。"); 112 | process.exit(0); 113 | } 114 | 115 | const serverUrl = "https://github.com"; 116 | 117 | const commitMetas: CommitMeta[] = commitShas.map((sha) => { 118 | const title = sh(`git show -s --format=%s ${sha}`); 119 | const author = sh(`git show -s --format=%an ${sha}`); 120 | const url = REPO 121 | ? `${serverUrl}/${REPO}/commit/${sha}` 122 | : `${serverUrl}/commit/${sha}`; 123 | const branches = Array.from(shaToBranches.get(sha) || []).sort(); 124 | return { sha, title, author, url, branches }; 125 | }); 126 | 127 | // ------- diff 获取与分片 ------- 128 | const FILE_EXCLUDES = [ 129 | ":!**/*.lock", 130 | ":!**/dist/**", 131 | ":!**/build/**", 132 | ":!**/.next/**", 133 | ":!**/.vite/**", 134 | ":!**/out/**", 135 | ":!**/coverage/**", 136 | ":!package-lock.json", 137 | ":!pnpm-lock.yaml", 138 | ":!yarn.lock", 139 | ":!**/*.min.*", 140 | ]; 141 | 142 | function getParentSha(sha: string) { 143 | const line = sh(`git rev-list --parents -n 1 ${sha} || true`); 144 | const parts = line.split(" ").filter(Boolean); 145 | // 非 merge 情况 parent 通常只有一个;root commit 无 parent 146 | return parts[1]; 147 | } 148 | 149 | function getDiff(sha: string) { 150 | const parent = getParentSha(sha); 151 | const base = parent || sh(`git hash-object -t tree /dev/null`); 152 | const excludes = FILE_EXCLUDES.join(" "); 153 | const diff = sh( 154 | `git diff --unified=0 --minimal ${base} ${sha} -- . ${excludes} || true`, 155 | ); 156 | return diff; 157 | } 158 | 159 | function splitPatchByFile(patch: string): string[] { 160 | if (!patch) return []; 161 | const parts = patch.split(/^diff --git.*$/m); 162 | return parts.map((p) => p.trim()).filter(Boolean); 163 | } 164 | 165 | function chunkBySize(parts: string[], limit = DIFF_CHUNK_MAX_CHARS): string[] { 166 | const out: string[] = []; 167 | let buf = ""; 168 | for (const p of parts) { 169 | const candidate = buf ? `${buf}\n\n${p}` : p; 170 | if (candidate.length > limit) { 171 | if (buf) out.push(buf); 172 | if (p.length > limit) { 173 | for (let i = 0; i < p.length; i += limit) { 174 | out.push(p.slice(i, i + limit)); 175 | } 176 | buf = ""; 177 | } else { 178 | buf = p; 179 | } 180 | } else { 181 | buf = candidate; 182 | } 183 | } 184 | if (buf) out.push(buf); 185 | return out; 186 | } 187 | 188 | // ------- OpenAI Chat API ------- 189 | type ChatPayload = { 190 | model: string; 191 | messages: { role: "system" | "user" | "assistant"; content: string }[]; 192 | temperature?: number; 193 | }; 194 | 195 | async function chat(prompt: string): Promise { 196 | const payload: ChatPayload = { 197 | model: MODEL_NAME, 198 | messages: [{ role: "user", content: prompt }], 199 | temperature: 0.2, 200 | }; 201 | const body = JSON.stringify(payload); 202 | 203 | return new Promise((resolve, reject) => { 204 | const url = new URL(OPENAI_BASE_URL); 205 | const req = https.request( 206 | { 207 | hostname: url.hostname, 208 | path: `/openai/deployments/${MODEL_NAME}/chat/completions?api-version=2024-12-01-preview`, 209 | method: "POST", 210 | headers: { 211 | "Content-Type": "application/json", 212 | Authorization: `Bearer ${OPENAI_API_KEY}`, 213 | "Content-Length": Buffer.byteLength(body), 214 | }, 215 | }, 216 | (res) => { 217 | let data = ""; 218 | res.on("data", (d) => (data += d)); 219 | res.on("end", () => { 220 | try { 221 | if ( 222 | res.statusCode && 223 | res.statusCode >= 200 && 224 | res.statusCode < 300 225 | ) { 226 | const json = JSON.parse(data); 227 | const content = 228 | json?.choices?.[0]?.message?.content?.trim() || ""; 229 | resolve(content); 230 | } else { 231 | reject(new Error(`OpenAI HTTP ${res.statusCode}: ${data}`)); 232 | } 233 | } catch (e) { 234 | reject(e); 235 | } 236 | }); 237 | }, 238 | ); 239 | req.on("error", reject); 240 | req.write(body); 241 | req.end(); 242 | }); 243 | } 244 | 245 | // ------- 提示词 ------- 246 | function commitChunkPrompt( 247 | meta: CommitMeta, 248 | partIdx: number, 249 | total: number, 250 | patch: string, 251 | ) { 252 | return `你是一名资深工程师与发布经理。以下是提交 ${meta.sha.slice(0, 7)}(${meta.title})的 diff 片段(第 ${partIdx}/${total} 段),请用中文输出结构化摘要: 253 | 254 | 提交信息: 255 | - SHA: ${meta.sha} 256 | - 标题: ${meta.title} 257 | - 作者: ${meta.author} 258 | - 分支: ${meta.branches.join(", ")} 259 | - 链接: ${meta.url} 260 | 261 | 要求输出: 262 | 1) 变更要点(面向工程师与产品):列出此片段涉及的主要改动与意图 263 | 2) 影响范围:模块/接口/关键文件 264 | 3) 风险&回滚点 265 | 4) 测试建议 266 | 注意:仅基于当前片段,不要臆测;不要贴长代码;如果只是格式化/重命名也请明确指出。 267 | 268 | === DIFF PART BEGIN === 269 | ${patch} 270 | === DIFF PART END ===`; 271 | } 272 | 273 | function commitMergePrompt(meta: CommitMeta, parts: string[]) { 274 | const joined = parts.map((p, i) => `【片段${i + 1}】\n${p}`).join("\n\n"); 275 | return `下面是提交 ${meta.sha.slice(0, 7)} 的各片段小结,请合并为**单条提交**的最终摘要(中文),输出以下小节: 276 | - 变更概述(不超过5条要点) 277 | - 影响范围(模块/接口/配置) 278 | - 风险与回滚点 279 | - 测试建议 280 | - 面向用户的可见影响(如有) 281 | 282 | 请避免重复、合并同类项,标注“可能不完整”当某些片段缺失或被截断。 283 | 284 | === 片段小结集合 BEGIN === 285 | ${joined} 286 | === 片段小结集合 END ===`; 287 | } 288 | 289 | function dailyMergePrompt( 290 | dateLabel: string, 291 | items: { meta: CommitMeta; summary: string }[], 292 | repo: string, 293 | ) { 294 | const body = items 295 | .map( 296 | (it) => 297 | `[${it.meta.sha.slice(0, 7)}] ${it.meta.title} — ${it.meta.author} — ${it.meta.branches.join(", ")} 298 | ${it.summary}`, 299 | ) 300 | .join("\n\n---\n\n"); 301 | 302 | return `请将以下“当日各提交摘要”整合成**当日开发变更日报(中文)**,输出结构如下: 303 | # ${dateLabel} 开发变更日报(${repo}) 304 | 1. 今日概览(不超过5条) 305 | 2. **按分支**的关键改动清单(每条含模块/影响、是否潜在破坏性) 306 | 3. 跨分支风险与回滚策略(如同一提交在多个分支、存在 cherry-pick/divergence) 307 | 4. 建议测试与验证清单 308 | 5. 其他备注(如重构/依赖升级/仅格式化) 309 | 310 | === 当日提交摘要 BEGIN === 311 | ${body} 312 | === 当日提交摘要 END ===`; 313 | } 314 | 315 | // ------- 飞书 Webhook ------- 316 | async function postToLark(text: string) { 317 | if (!LARK_WEBHOOK_URL) { 318 | console.log("LARK_WEBHOOK_URL 未配置,以下为最终日报文本:\n\n" + text); 319 | return; 320 | } 321 | const payload = JSON.stringify({ msg_type: "text", content: { text } }); 322 | await new Promise((resolve, reject) => { 323 | const url = new URL(LARK_WEBHOOK_URL); 324 | const req = https.request( 325 | { 326 | hostname: url.hostname, 327 | path: url.pathname + url.search, 328 | method: "POST", 329 | headers: { "Content-Type": "application/json" }, 330 | }, 331 | (res) => { 332 | res.on("data", () => {}); 333 | res.on("end", () => resolve()); 334 | }, 335 | ); 336 | req.on("error", reject); 337 | req.write(payload); 338 | req.end(); 339 | }); 340 | } 341 | 342 | // ------- 主流程 ------- 343 | (async () => { 344 | const perCommitFinal: { meta: CommitMeta; summary: string }[] = []; 345 | 346 | for (const meta of commitMetas) { 347 | const fullPatch = getDiff(meta.sha); 348 | 349 | if (!fullPatch || !fullPatch.trim()) { 350 | perCommitFinal.push({ 351 | meta, 352 | summary: `(无有效业务改动或改动已被过滤,例如 lockfile/构建产物/二进制,或空提交)`, 353 | }); 354 | continue; 355 | } 356 | 357 | const fileParts = splitPatchByFile(fullPatch); 358 | const chunks = chunkBySize(fileParts, DIFF_CHUNK_MAX_CHARS); 359 | 360 | const partSummaries: string[] = []; 361 | for (let i = 0; i < chunks.length; i++) { 362 | const prompt = commitChunkPrompt(meta, i + 1, chunks.length, chunks[i]); 363 | try { 364 | const sum = await chat(prompt); 365 | partSummaries.push(sum || `(片段${i + 1}摘要为空)`); 366 | } catch (e: any) { 367 | partSummaries.push(`(片段${i + 1}调用失败:${String(e)})`); 368 | } 369 | } 370 | 371 | // 合并为“单提交摘要” 372 | let merged = ""; 373 | try { 374 | merged = await chat(commitMergePrompt(meta, partSummaries)); 375 | } catch (e: any) { 376 | merged = partSummaries.join("\n\n"); 377 | } 378 | 379 | perCommitFinal.push({ meta, summary: merged }); 380 | } 381 | 382 | // 当地日期标签 YYYY-MM-DD 383 | const todayLabel = new Date().toLocaleDateString("en-CA", { 384 | timeZone: "America/Los_Angeles", 385 | }); 386 | 387 | // 汇总“当日总览” 388 | let daily = ""; 389 | try { 390 | daily = await chat( 391 | dailyMergePrompt(todayLabel, perCommitFinal, REPO || "repository"), 392 | ); 393 | } catch (e: any) { 394 | daily = 395 | `(当日汇总失败,以下为逐提交原始小结拼接)\n\n` + 396 | perCommitFinal 397 | .map( 398 | (it) => 399 | `[${it.meta.sha.slice(0, 7)}] ${it.meta.title} — ${it.meta.branches.join(", ")}\n${it.summary}`, 400 | ) 401 | .join("\n\n---\n\n"); 402 | } 403 | 404 | // 发送飞书 405 | await postToLark(daily); 406 | console.log("✅ 已发送飞书日报。"); 407 | })().catch((err) => { 408 | console.error(err); 409 | process.exit(1); 410 | }); 411 | --------------------------------------------------------------------------------