diff --git a/.github/workflows/github-issue-tracker.yml b/.github/workflows/github-issue-tracker.yml new file mode 100644 index 0000000000..9830c016d9 --- /dev/null +++ b/.github/workflows/github-issue-tracker.yml @@ -0,0 +1,175 @@ +name: GitHub Issue Tracker with Feishu Notification + +on: + issues: + types: [opened] + schedule: + # Run every day at 8:30 Beijing Time (00:30 UTC) + - cron: '30 0 * * *' + workflow_dispatch: + +jobs: + process-new-issue: + if: github.event_name == 'issues' + runs-on: ubuntu-latest + permissions: + issues: write + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check Beijing Time + id: check_time + run: | + # Get current time in Beijing timezone (UTC+8) + BEIJING_HOUR=$(TZ='Asia/Shanghai' date +%H) + BEIJING_MINUTE=$(TZ='Asia/Shanghai' date +%M) + + echo "Beijing Time: ${BEIJING_HOUR}:${BEIJING_MINUTE}" + + # Check if time is between 00:00 and 08:30 + if [ $BEIJING_HOUR -lt 8 ] || ([ $BEIJING_HOUR -eq 8 ] && [ $BEIJING_MINUTE -le 30 ]); then + echo "should_delay=true" >> $GITHUB_OUTPUT + echo "⏰ Issue created during quiet hours (00:00-08:30 Beijing Time)" + echo "Will schedule notification for 08:30" + else + echo "should_delay=false" >> $GITHUB_OUTPUT + echo "✅ Issue created during active hours, will notify immediately" + fi + + - name: Add pending label if in quiet hours + if: steps.check_time.outputs.should_delay == 'true' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: ['pending-feishu-notification'] + }); + + - name: Setup Node.js + if: steps.check_time.outputs.should_delay == 'false' + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Summarize issue with Claude + if: steps.check_time.outputs.should_delay == 'false' + id: summarize + uses: anthropics/claude-code-action@main + with: + anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }} + prompt: | + Please analyze this GitHub issue and provide a concise summary in Chinese (中文). + + Issue #${{ github.event.issue.number }}: ${{ github.event.issue.title }} + Author: ${{ github.event.issue.user.login }} + URL: ${{ github.event.issue.html_url }} + + Issue Body: + ${{ github.event.issue.body }} + + Please provide: + 1. A brief Chinese summary of the issue (2-3 sentences) + 2. The main problem or request + 3. Any important technical details mentioned + + Format your response in clean markdown, suitable for display in a notification card. + Keep it concise but informative. + env: + ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }} + + - name: Send to Feishu immediately + if: steps.check_time.outputs.should_delay == 'false' + env: + FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} + FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }} + ISSUE_URL: ${{ github.event.issue.html_url }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_AUTHOR: ${{ github.event.issue.user.login }} + ISSUE_LABELS: ${{ join(github.event.issue.labels.*.name, ',') }} + ISSUE_SUMMARY: ${{ steps.summarize.outputs.response }} + run: | + node scripts/feishu-notify.js + + process-pending-issues: + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + issues: write + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Process pending issues with Claude + uses: anthropics/claude-code-action@main + with: + anthropic_api_key: ${{ secrets.CLAUDE_TRANSLATOR_APIKEY }} + allowed_non_write_users: "*" + github_token: ${{ secrets.GITHUB_TOKEN }} + claude_args: "--allowed-tools Bash(gh issue:*),Bash(gh api:*),Bash(node scripts/feishu-notify.js)" + prompt: | + 你是一个GitHub Issue自动化处理助手。请完成以下任务: + + ## 任务说明 + 处理所有待发送飞书通知的GitHub Issues(标记为 `pending-feishu-notification` 的issues) + + ## 步骤 + + 1. **获取待处理的issues** + 使用以下命令获取所有带 `pending-feishu-notification` 标签的issues: + ```bash + gh api repos/${{ github.repository }}/issues?labels=pending-feishu-notification&state=open + ``` + + 2. **总结每个issue** + 对于每个找到的issue,用中文提供简洁的总结(2-3句话),包括: + - 问题的主要内容 + - 核心诉求 + - 重要的技术细节 + + 3. **发送飞书通知** + 对于每个issue,使用以下命令发送飞书通知: + ```bash + ISSUE_URL="" \ + ISSUE_NUMBER="" \ + ISSUE_TITLE="" \ + ISSUE_AUTHOR="" \ + ISSUE_LABELS="<逗号分隔的标签列表,排除pending-feishu-notification>" \ + ISSUE_SUMMARY="<你生成的中文总结>" \ + node scripts/feishu-notify.js + ``` + + 4. **移除标签** + 成功发送后,使用以下命令移除 `pending-feishu-notification` 标签: + ```bash + gh api -X DELETE repos/${{ github.repository }}/issues//labels/pending-feishu-notification + ``` + + ## 环境变量 + - Repository: ${{ github.repository }} + - Feishu webhook URL和密钥已在环境变量中配置好 + + ## 注意事项 + - 如果没有待处理的issues,输出提示信息后直接结束 + - 处理多个issues时,每个issue之间等待2-3秒,避免API限流 + - 如果某个issue处理失败,继续处理下一个,不要中断整个流程 + - 所有总结必须使用中文(简体中文) + + 请开始执行任务! + env: + ANTHROPIC_BASE_URL: ${{ secrets.CLAUDE_TRANSLATOR_BASEURL }} + FEISHU_WEBHOOK_URL: ${{ secrets.FEISHU_WEBHOOK_URL }} + FEISHU_WEBHOOK_SECRET: ${{ secrets.FEISHU_WEBHOOK_SECRET }} diff --git a/scripts/feishu-notify.js b/scripts/feishu-notify.js new file mode 100644 index 0000000000..aae9004a48 --- /dev/null +++ b/scripts/feishu-notify.js @@ -0,0 +1,228 @@ +/** + * Feishu (Lark) Webhook Notification Script + * Sends GitHub issue summaries to Feishu with signature verification + */ + +const crypto = require('crypto') +const https = require('https') + +/** + * Generate Feishu webhook signature + * @param {string} secret - Feishu webhook secret + * @param {number} timestamp - Unix timestamp in seconds + * @returns {string} Base64 encoded signature + */ +function generateSignature(secret, timestamp) { + const stringToSign = `${timestamp}\n${secret}` + const hmac = crypto.createHmac('sha256', stringToSign) + return hmac.digest('base64') +} + +/** + * Send message to Feishu webhook + * @param {string} webhookUrl - Feishu webhook URL + * @param {string} secret - Feishu webhook secret + * @param {object} content - Message content + * @returns {Promise} + */ +function sendToFeishu(webhookUrl, secret, content) { + return new Promise((resolve, reject) => { + const timestamp = Math.floor(Date.now() / 1000) + const sign = generateSignature(secret, timestamp) + + const payload = JSON.stringify({ + timestamp: timestamp.toString(), + sign: sign, + msg_type: 'interactive', + card: content + }) + + const url = new URL(webhookUrl) + const options = { + hostname: url.hostname, + path: url.pathname + url.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(payload) + } + } + + const req = https.request(options, (res) => { + let data = '' + res.on('data', (chunk) => { + data += chunk + }) + res.on('end', () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + console.log('✅ Successfully sent to Feishu:', data) + resolve() + } else { + reject(new Error(`Feishu API error: ${res.statusCode} - ${data}`)) + } + }) + }) + + req.on('error', (error) => { + reject(error) + }) + + req.write(payload) + req.end() + }) +} + +/** + * Create Feishu card message from issue data + * @param {object} issueData - GitHub issue data + * @returns {object} Feishu card content + */ +function createIssueCard(issueData) { + const { issueUrl, issueNumber, issueTitle, issueSummary, issueAuthor, labels } = issueData + + // Build labels section if labels exist + const labelElements = + labels && labels.length > 0 + ? labels.map((label) => ({ + tag: 'markdown', + content: `\`${label}\`` + })) + : [] + + return { + elements: [ + { + tag: 'div', + text: { + tag: 'lark_md', + content: `**🐛 New GitHub Issue #${issueNumber}**` + } + }, + { + tag: 'hr' + }, + { + tag: 'div', + text: { + tag: 'lark_md', + content: `**📝 Title:** ${issueTitle}` + } + }, + { + tag: 'div', + text: { + tag: 'lark_md', + content: `**👤 Author:** ${issueAuthor}` + } + }, + ...(labelElements.length > 0 + ? [ + { + tag: 'div', + text: { + tag: 'lark_md', + content: `**🏷️ Labels:** ${labels.join(', ')}` + } + } + ] + : []), + { + tag: 'hr' + }, + { + tag: 'div', + text: { + tag: 'lark_md', + content: `**📋 Summary:**\n${issueSummary}` + } + }, + { + tag: 'hr' + }, + { + tag: 'action', + actions: [ + { + tag: 'button', + text: { + tag: 'plain_text', + content: '🔗 View Issue' + }, + type: 'primary', + url: issueUrl + } + ] + } + ], + header: { + template: 'blue', + title: { + tag: 'plain_text', + content: '🆕 Cherry Studio - New Issue' + } + } + } +} + +/** + * Main function + */ +async function main() { + try { + // Get environment variables + const webhookUrl = process.env.FEISHU_WEBHOOK_URL + const secret = process.env.FEISHU_WEBHOOK_SECRET + const issueUrl = process.env.ISSUE_URL + const issueNumber = process.env.ISSUE_NUMBER + const issueTitle = process.env.ISSUE_TITLE + const issueSummary = process.env.ISSUE_SUMMARY + const issueAuthor = process.env.ISSUE_AUTHOR + const labelsStr = process.env.ISSUE_LABELS || '' + + // Validate required environment variables + if (!webhookUrl) { + throw new Error('FEISHU_WEBHOOK_URL environment variable is required') + } + if (!secret) { + throw new Error('FEISHU_WEBHOOK_SECRET environment variable is required') + } + if (!issueUrl || !issueNumber || !issueTitle || !issueSummary) { + throw new Error('Issue data environment variables are required') + } + + // Parse labels + const labels = labelsStr + ? labelsStr + .split(',') + .map((l) => l.trim()) + .filter(Boolean) + : [] + + // Create issue data object + const issueData = { + issueUrl, + issueNumber, + issueTitle, + issueSummary, + issueAuthor: issueAuthor || 'Unknown', + labels + } + + // Create card content + const card = createIssueCard(issueData) + + console.log('📤 Sending notification to Feishu...') + console.log(`Issue #${issueNumber}: ${issueTitle}`) + + // Send to Feishu + await sendToFeishu(webhookUrl, secret, card) + + console.log('✅ Notification sent successfully!') + } catch (error) { + console.error('❌ Error:', error.message) + process.exit(1) + } +} + +// Run main function +main()