210 lines
8.0 KiB
JavaScript
210 lines
8.0 KiB
JavaScript
// GitHub桥接模块·github-bridge.js·v1.0
|
||
// HoloLake·M-DINGTALK Phase8
|
||
// DEV-004 之之×秋秋
|
||
//
|
||
// 功能:钉钉命令→GitHubAPI→结果回传钉钉
|
||
// 支持:查PR·查Issue·查仓库状态·触发部署
|
||
//
|
||
|
||
var axios = require('axios');
|
||
|
||
var GITHUB_TOKEN = process.env.GITHUB_TOKEN || '';
|
||
var GITHUB_OWNER = process.env.GITHUB_OWNER || 'qinfendebingshuo';
|
||
var GITHUB_REPO = process.env.GITHUB_REPO || 'guanghulab';
|
||
var GITHUB_API = 'https://api.github.com';
|
||
|
||
// ===== GitHub API 请求封装 =====
|
||
async function githubRequest(endpoint, method, data) {
|
||
method = method || 'GET';
|
||
var url = GITHUB_API + endpoint;
|
||
try {
|
||
var config = {
|
||
method: method,
|
||
url: url,
|
||
headers: {
|
||
'Authorization': GITHUB_TOKEN ? ('token ' + GITHUB_TOKEN) : undefined,
|
||
'Accept': 'application/vnd.github.v3+json',
|
||
'User-Agent': 'HoloLake-DingTalk-Bot'
|
||
},
|
||
timeout: 15000
|
||
};
|
||
if (data) config.data = data;
|
||
var res = await axios(config);
|
||
return { ok: true, data: res.data, status: res.status };
|
||
} catch (err) {
|
||
var status = err.response ? err.response.status : 0;
|
||
var msg = err.response ? (err.response.data.message || err.message) : err.message;
|
||
console.error('[GitHub] API错误: ' + status + ' ' + msg);
|
||
return { ok: false, error: msg, status: status };
|
||
}
|
||
}
|
||
|
||
// ====== /github status · 仓库状态 =====
|
||
async function getRepoStatus() {
|
||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO);
|
||
if (!res.ok) return '❌ 获取仓库状态失败: ' + res.error;
|
||
var repo = res.data;
|
||
|
||
// 获取最近提交
|
||
var commitsRes = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/commits?per_page=3');
|
||
var commitsText = '';
|
||
if (commitsRes.ok && commitsRes.data.length > 0) {
|
||
commitsText = '\n\n**最近提交:**\n';
|
||
commitsRes.data.forEach(function(c, i) {
|
||
var date = new Date(c.commit.author.date).toLocaleString('zh-CN');
|
||
var msg = (c.commit.message || '').split('\n')[0].substring(0, 60);
|
||
commitsText += (i + 1) + '. `' + msg + '` · ' + date + '\n';
|
||
});
|
||
}
|
||
|
||
return '### 📊 仓库状态\n\n'
|
||
+ '- **仓库**: ' + repo.full_name + '\n'
|
||
+ '- **默认分支**: ' + repo.default_branch + '\n'
|
||
+ '- **Star**: ' + repo.stargazers_count + ' · **Fork**: ' + repo.forks_count + '\n'
|
||
+ '- **最后更新**: ' + new Date(repo.updated_at).toLocaleString('zh-CN') + '\n'
|
||
+ '- **大小**: ' + Math.round(repo.size / 1024) + ' MB'
|
||
+ commitsText;
|
||
}
|
||
|
||
// ====== /github pr · 查看PR ======
|
||
async function listPRs(state) {
|
||
state = state || 'open';
|
||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/pulls?state=' + state + '&per_page=10');
|
||
if (!res.ok) return '❌ 获取PR列表失败: ' + res.error;
|
||
var prs = res.data;
|
||
if (prs.length === 0) return '### 📋 Pull Requests\n\n当前没有' + (state === 'open' ? '打开的' : '') + 'PR。';
|
||
|
||
var text = '### 📋 Pull Requests(' + state + ')\n\n';
|
||
prs.forEach(function(pr) {
|
||
var date = new Date(pr.created_at).toLocaleString('zh-CN');
|
||
text += '- **#' + pr.number + '** ' + pr.title + '\n';
|
||
text += ' 👤 ' + pr.user.login + ' · 📅 ' + date + '\n';
|
||
});
|
||
return text;
|
||
}
|
||
|
||
// ====== /github issue · 查看Issue ======
|
||
async function listIssues(state) {
|
||
state = state || 'open';
|
||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/issues?state=' + state + '&per_page=10');
|
||
if (!res.ok) return '❌ 获取Issue列表失败: ' + res.error;
|
||
var issues = res.data.filter(function(i) { return !i.pull_request; }); // 排除PR
|
||
if (issues.length === 0) return '### 📋 Issues\n\n当前没有' + (state === 'open' ? '打开的' : '') + 'Issue。';
|
||
|
||
var text = '### 📋 Issues(' + state + ')\n\n';
|
||
issues.forEach(function(issue) {
|
||
var labels = issue.labels.map(function(l) { return l.name; }).join(', ');
|
||
text += '- **#' + issue.number + '** ' + issue.title;
|
||
if (labels) text += ' `' + labels + '`';
|
||
text += '\n';
|
||
});
|
||
return text;
|
||
}
|
||
|
||
// ====== /github actions · 查看最近的Actions运行 ======
|
||
async function listActions() {
|
||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/actions/runs?per_page=5');
|
||
if (!res.ok) return '❌ 获取Actions状态失败: ' + res.error;
|
||
var runs = res.data.workflow_runs || [];
|
||
if (runs.length === 0) return '### GitHub Actions\n\n暂无运行记录。';
|
||
|
||
var text = '### GitHub Actions(最近5次)\n\n';
|
||
runs.forEach(function(run) {
|
||
var status = run.conclusion || run.status;
|
||
var icon = status === 'success' ? '✅' : status === 'failure' ? '❌' : '⏳';
|
||
var date = new Date(run.created_at).toLocaleString('zh-CN');
|
||
text += '- ' + icon + ' **' + run.name + '** ' + status + ' · ' + date + '\n';
|
||
});
|
||
return text;
|
||
}
|
||
|
||
// ====== /github deploy · 触发部署 (dispatch事件) =====
|
||
async function triggerDeploy(environment) {
|
||
environment = environment || 'production';
|
||
var res = await githubRequest(
|
||
'/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO + '/dispatches',
|
||
'POST',
|
||
{
|
||
event_type: 'deploy',
|
||
client_payload: {
|
||
environment: environment,
|
||
triggered_by: 'dingtalk-bot',
|
||
timestamp: new Date().toISOString()
|
||
}
|
||
}
|
||
);
|
||
|
||
if (res.status === 204 || res.ok) {
|
||
return '### 🚀 部署触发成功\n\n'
|
||
+ '- **环境**: ' + environment + '\n'
|
||
+ '- **触发时间**: ' + new Date().toLocaleString('zh-CN') + '\n'
|
||
+ '- **来源**: 钉钉机器人\n\n'
|
||
+ '> 请到 GitHub Actions 页面查看部署进度。';
|
||
}
|
||
return '❌ 触发部署失败: ' + (res.error || '未知错误') + '\n> 请确认仓库已配置 repository_dispatch 工作流。';
|
||
}
|
||
|
||
// ====== 命令路由 =====
|
||
async function handleGitHubCommand(args) {
|
||
var subCommand = (args[0] || 'help').toLowerCase();
|
||
|
||
switch (subCommand) {
|
||
case 'status':
|
||
case '状态':
|
||
return await getRepoStatus();
|
||
|
||
case 'pr':
|
||
case 'prs':
|
||
return await listPRs(args[1] || 'open');
|
||
|
||
case 'issue':
|
||
case 'issues':
|
||
return await listIssues(args[1] || 'open');
|
||
|
||
case 'actions':
|
||
case 'ci':
|
||
return await listActions();
|
||
|
||
case 'deploy':
|
||
case '部署':
|
||
return await triggerDeploy(args[1] || 'production');
|
||
|
||
case 'help':
|
||
case '帮助':
|
||
default:
|
||
return '### GitHub 桥接命令\n\n'
|
||
+ '| 命令 | 说明 |\n'
|
||
+ '|------|------|\n'
|
||
+ '| `/github status` | 查看仓库状态+最近提交 |\n'
|
||
+ '| `/github pr` | 查看打开的PR |\n'
|
||
+ '| `/github pr closed` | 查看已关闭的PR |\n'
|
||
+ '| `/github issue` | 查看打开的Issue |\n'
|
||
+ '| `/github actions` | 查看最近CI/CD运行 |\n'
|
||
+ '| `/github deploy` | 触发生产部署 |\n'
|
||
+ '| `/github help` | 显示本帮助 |\n\n'
|
||
+ '> HoloLake · M-DINGTALK Phase 8 · 钉钉+GitHub桥接';
|
||
}
|
||
}
|
||
|
||
// ===== 健康检查 =====
|
||
async function healthCheck() {
|
||
var res = await githubRequest('/repos/' + GITHUB_OWNER + '/' + GITHUB_REPO);
|
||
return {
|
||
status: res.ok ? 'ok' : 'error',
|
||
repo: GITHUB_OWNER + '/' + GITHUB_REPO,
|
||
hasToken: !!GITHUB_TOKEN,
|
||
apiReachable: res.ok,
|
||
error: res.ok ? null : res.error
|
||
};
|
||
}
|
||
|
||
module.exports = {
|
||
handleGitHubCommand: handleGitHubCommand,
|
||
getRepoStatus: getRepoStatus,
|
||
listPRs: listPRs,
|
||
listIssues: listIssues,
|
||
listActions: listActions,
|
||
triggerDeploy: triggerDeploy,
|
||
healthCheck: healthCheck
|
||
};
|