feat: コミットレポートツール(コミネコ)初期コミット
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
---
|
||||
name: slack-formatting
|
||||
description: Slackチャンネルに複数画像をまとめて投稿する。Slack投稿、レポート投稿、画像アップロード時に使用。curlは使わずpost-report.jsを実行すること。
|
||||
allowed-tools: Bash(node:*), Read
|
||||
---
|
||||
|
||||
# Slack Formatting ガイド
|
||||
|
||||
Slack への投稿方法を説明します。
|
||||
|
||||
## ⚠️ 重要: 必ずスクリプトを使用すること
|
||||
|
||||
**Slack投稿は必ず以下のスクリプトを使用してください。**
|
||||
curlで直接APIを呼び出したり、独自の投稿ロジックを書いたりしないでください。
|
||||
|
||||
```bash
|
||||
# 必ずこのスクリプトを使用
|
||||
node .claude/skills/slack-formatting/scripts/post-report.js \
|
||||
--message "メッセージテキスト" \
|
||||
/tmp/image1.png /tmp/image2.png ...
|
||||
```
|
||||
|
||||
## スクリプト使用方法
|
||||
|
||||
### 基本的な使い方
|
||||
|
||||
```bash
|
||||
node .claude/skills/slack-formatting/scripts/post-report.js \
|
||||
--message "📊 今日のコミットレポート
|
||||
|
||||
期間: 2026-01-06 00:00 〜 23:59 (JST)
|
||||
対象: 1ブランチ / 18コミット / 5名
|
||||
|
||||
🐱 コミネコ で自動生成" \
|
||||
/tmp/daily-summary.png \
|
||||
/tmp/branch-summary-main.png \
|
||||
/tmp/by-app.png \
|
||||
/tmp/timeline.png \
|
||||
/tmp/tips.png
|
||||
```
|
||||
|
||||
### 環境変数
|
||||
|
||||
| 環境変数 | 用途 | 必須 |
|
||||
|----------|------|------|
|
||||
| `SLACK_BOT_TOKEN` | Bot User OAuth Token | ✅ |
|
||||
| `SLACK_CHANNEL` | 投稿先チャンネルID | ※未設定時はデバッグチャンネル |
|
||||
|
||||
**デバッグ用チャンネル**: `YOUR_DEBUG_CHANNEL_ID`
|
||||
|
||||
### スクリプトが行うこと
|
||||
|
||||
- ✅ 複数画像を**1メッセージにまとめて**投稿
|
||||
- ✅ **チャンネルに直接投稿**(スレッドではない)
|
||||
- ✅ 詳細なログ出力(原因特定用)
|
||||
- ✅ エラーハンドリング
|
||||
|
||||
### スクリプトの出力例
|
||||
|
||||
```
|
||||
========================================
|
||||
=== SLACK投稿開始 ===
|
||||
========================================
|
||||
[2026-01-07T07:15:00.000Z] 投稿先チャンネル: C0XXXXXXXXX
|
||||
[2026-01-07T07:15:00.000Z] 画像数: 5
|
||||
[2026-01-07T07:15:00.000Z] メッセージ: 📊 今日のコミットレポート...
|
||||
[2026-01-07T07:15:00.000Z] [1] daily-summary.png
|
||||
[2026-01-07T07:15:00.000Z] [2] branch-summary-main.png
|
||||
...
|
||||
========================================
|
||||
=== SLACK投稿完了 ===
|
||||
========================================
|
||||
[2026-01-07T07:15:10.000Z] チャンネル: C0XXXXXXXXX
|
||||
[2026-01-07T07:15:10.000Z] 画像数: 5
|
||||
[2026-01-07T07:15:10.000Z] ✅ 投稿成功!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 禁止事項
|
||||
|
||||
以下の方法は**絶対に使用しないでください**:
|
||||
|
||||
```bash
|
||||
# ❌ 禁止: curlで直接APIを呼び出す
|
||||
curl -X POST https://slack.com/api/chat.postMessage ...
|
||||
|
||||
# ❌ 禁止: スレッドに投稿する
|
||||
curl ... -d '{"thread_ts": "xxx"}' ...
|
||||
|
||||
# ❌ 禁止: テキストと画像を別々に投稿
|
||||
curl ... chat.postMessage # テキスト
|
||||
curl ... files.upload # 画像(スレッドに)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Block Kit 構造(参考)
|
||||
|
||||
テキストメッセージのフォーマットに使用できます。
|
||||
|
||||
### mrkdwn 記法
|
||||
|
||||
| 書式 | 記法 | 例 |
|
||||
|------|------|-----|
|
||||
| 太字 | `*text*` | *太字* |
|
||||
| イタリック | `_text_` | _イタリック_ |
|
||||
| 取消線 | `~text~` | ~取消線~ |
|
||||
| コード | `` `code` `` | `code` |
|
||||
| リンク | `<URL\|テキスト>` | <https://github.com\|GitHub> |
|
||||
| メンション | `<@USER_ID>` | <@U1234> |
|
||||
| チャンネル | `<#CHANNEL_ID>` | <#C1234> |
|
||||
|
||||
---
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
| 症状 | 原因 | 対処 |
|
||||
|------|------|------|
|
||||
| 画像がスレッドに投稿された | スクリプトを使っていない | `post-report.js` を使用する |
|
||||
| 画像が1枚しか投稿されない | スクリプトを使っていない | `post-report.js` を使用する |
|
||||
| 3回投稿された | 投稿処理が複数回呼ばれた | ログで `SLACK投稿開始` の回数を確認 |
|
||||
| `not_authed` エラー | 環境変数が渡されていない | `SLACK_BOT_TOKEN` を確認 |
|
||||
@@ -0,0 +1,260 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Slack レポート投稿スクリプト
|
||||
*
|
||||
* 複数の画像を1メッセージにまとめてSlackチャンネルに投稿します。
|
||||
* ※スレッドではなく、チャンネルに直接投稿します
|
||||
*
|
||||
* Required Environment Variables:
|
||||
* SLACK_BOT_TOKEN - Bot User OAuth Token (xoxb-...)
|
||||
* SLACK_CHANNEL - Channel ID (C...)
|
||||
*
|
||||
* Usage:
|
||||
* node post-report.js --message "テキスト" <image1.png> <image2.png> ...
|
||||
*
|
||||
* Example:
|
||||
* node post-report.js --message "📊 今日のレポート" \
|
||||
* /tmp/daily-summary.png /tmp/by-app.png /tmp/timeline.png
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
|
||||
// 環境変数(SLACK_CHANNEL または SLACK_CHANNEL_ID をサポート)
|
||||
const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN;
|
||||
const CHANNEL_ID = process.env.SLACK_CHANNEL || process.env.SLACK_CHANNEL_ID;
|
||||
const DEBUG_CHANNEL = 'YOUR_DEBUG_CHANNEL_ID';
|
||||
|
||||
// ========================================
|
||||
// ログ出力(原因特定用)
|
||||
// ========================================
|
||||
function log(message) {
|
||||
const timestamp = new Date().toISOString();
|
||||
console.log(`[${timestamp}] ${message}`);
|
||||
}
|
||||
|
||||
function logSection(title) {
|
||||
console.log('');
|
||||
console.log('========================================');
|
||||
console.log(`=== ${title} ===`);
|
||||
console.log('========================================');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// HTTPS リクエスト
|
||||
// ========================================
|
||||
function httpsRequest(options, postData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(options, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
resolve(JSON.parse(data));
|
||||
} catch (e) {
|
||||
resolve({ ok: false, error: 'parse_error', raw: data });
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
if (postData) req.write(postData, 'utf8');
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Slack API 関数
|
||||
// ========================================
|
||||
async function getUploadUrl(filename, fileSize) {
|
||||
const params = new URLSearchParams({
|
||||
filename,
|
||||
length: fileSize.toString()
|
||||
});
|
||||
|
||||
return await httpsRequest({
|
||||
hostname: 'slack.com',
|
||||
path: '/api/files.getUploadURLExternal',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${SLACK_TOKEN}`,
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
}, params.toString());
|
||||
}
|
||||
|
||||
async function uploadFile(uploadUrl, filePath) {
|
||||
const fileContent = fs.readFileSync(filePath);
|
||||
const filename = path.basename(filePath);
|
||||
|
||||
const boundary = '----FormBoundary' + Math.random().toString(36).substring(2);
|
||||
const header = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="${filename}"\r\nContent-Type: application/octet-stream\r\n\r\n`;
|
||||
const footer = `\r\n--${boundary}--\r\n`;
|
||||
|
||||
const body = Buffer.concat([
|
||||
Buffer.from(header, 'utf-8'),
|
||||
fileContent,
|
||||
Buffer.from(footer, 'utf-8')
|
||||
]);
|
||||
|
||||
const url = new URL(uploadUrl);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request({
|
||||
hostname: url.hostname,
|
||||
path: url.pathname + url.search,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': `multipart/form-data; boundary=${boundary}`,
|
||||
'Content-Length': body.length
|
||||
}
|
||||
}, (res) => {
|
||||
res.setEncoding('utf8');
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
res.on('end', () => resolve(data));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function completeUpload(fileIds, message, channelId) {
|
||||
const files = fileIds.map(id => ({ id }));
|
||||
|
||||
const body = JSON.stringify({
|
||||
files,
|
||||
channel_id: channelId,
|
||||
initial_comment: message || ''
|
||||
});
|
||||
|
||||
return await httpsRequest({
|
||||
hostname: 'slack.com',
|
||||
path: '/api/files.completeUploadExternal',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${SLACK_TOKEN}`,
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
}
|
||||
}, body);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// メイン処理
|
||||
// ========================================
|
||||
async function postReport(imagePaths, message) {
|
||||
const channelId = CHANNEL_ID || DEBUG_CHANNEL;
|
||||
|
||||
logSection('SLACK投稿開始');
|
||||
log(`投稿先チャンネル: ${channelId}`);
|
||||
log(`画像数: ${imagePaths.length}`);
|
||||
log(`メッセージ: ${message.length > 50 ? message.substring(0, 50) + '...' : message}`);
|
||||
|
||||
// 画像一覧を表示
|
||||
imagePaths.forEach((p, i) => {
|
||||
log(` [${i + 1}] ${path.basename(p)}`);
|
||||
});
|
||||
|
||||
const fileIds = [];
|
||||
|
||||
// 各画像をアップロード
|
||||
for (let i = 0; i < imagePaths.length; i++) {
|
||||
const imagePath = imagePaths[i];
|
||||
const filename = path.basename(imagePath);
|
||||
const fileSize = fs.statSync(imagePath).size;
|
||||
|
||||
log(`アップロード中: ${filename} (${Math.round(fileSize / 1024)}KB)`);
|
||||
|
||||
// Step 1: Get upload URL
|
||||
const urlResponse = await getUploadUrl(filename, fileSize);
|
||||
if (!urlResponse.ok) {
|
||||
log(` エラー: ${urlResponse.error}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Step 2: Upload file
|
||||
await uploadFile(urlResponse.upload_url, imagePath);
|
||||
log(` 完了: ${urlResponse.file_id}`);
|
||||
|
||||
fileIds.push(urlResponse.file_id);
|
||||
}
|
||||
|
||||
if (fileIds.length === 0) {
|
||||
logSection('SLACK投稿エラー');
|
||||
log('アップロードされたファイルがありません');
|
||||
return { ok: false, error: 'no_files_uploaded' };
|
||||
}
|
||||
|
||||
// Step 3: Complete upload (posts all files at once to channel, NOT thread)
|
||||
log('');
|
||||
log('チャンネルに投稿中...');
|
||||
const result = await completeUpload(fileIds, message, channelId);
|
||||
|
||||
if (result.ok) {
|
||||
logSection('SLACK投稿完了');
|
||||
log(`チャンネル: ${channelId}`);
|
||||
log(`画像数: ${fileIds.length}`);
|
||||
log('✅ 投稿成功!');
|
||||
} else {
|
||||
logSection('SLACK投稿エラー');
|
||||
log(`エラー: ${result.error}`);
|
||||
if (result.raw) log(`詳細: ${result.raw}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 環境変数チェック
|
||||
if (!SLACK_TOKEN) {
|
||||
console.error('エラー: SLACK_BOT_TOKEN 環境変数が必要です');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!CHANNEL_ID) {
|
||||
log(`警告: SLACK_CHANNEL が未設定のため、デバッグチャンネル (${DEBUG_CHANNEL}) に投稿します`);
|
||||
}
|
||||
|
||||
// 引数パース
|
||||
const args = process.argv.slice(2);
|
||||
let message = '';
|
||||
const imagePaths = [];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--message' || args[i] === '-m') {
|
||||
message = args[++i] || '';
|
||||
} else if (args[i].startsWith('-')) {
|
||||
console.error(`不明なオプション: ${args[i]}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
imagePaths.push(args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 使い方表示
|
||||
if (imagePaths.length === 0) {
|
||||
console.error('使い方: node post-report.js --message "テキスト" <image1.png> ...');
|
||||
console.error('');
|
||||
console.error('必須環境変数:');
|
||||
console.error(' SLACK_BOT_TOKEN - Bot OAuth Token');
|
||||
console.error(' SLACK_CHANNEL - チャンネルID');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ファイル存在確認
|
||||
const missingFiles = imagePaths.filter(p => !fs.existsSync(p));
|
||||
if (missingFiles.length > 0) {
|
||||
console.error('ファイルが見つかりません:');
|
||||
missingFiles.forEach(f => console.error(` - ${f}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 投稿実行
|
||||
const result = await postReport(imagePaths, message);
|
||||
process.exit(result.ok ? 0 : 1);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user