Compare commits

..

24 Commits
dev_1 ... main

Author SHA1 Message Date
p5i4afnyx 02bf0c89c7 Merge pull request '需求创意文档' (#23) from cxf into main
1 month ago
chenxiaofu b0565cd5b6 上传了需求创意文档
1 month ago
p5i4afnyx 7e0d804ccc Merge pull request '最终版' (#22) from cxf into main
1 month ago
chenxiaofu f6f0f71305 部分细节修改
1 month ago
p5i4afnyx f8807a0353 Merge pull request '汇报ppt、视频、软件用户手册' (#21) from cxf into main
1 month ago
chenxiaofu 0faf294fff 最终汇报PPT-基于软件工厂的群智协作价值评估系统.pptx 以及软件用户手册、演示视频
1 month ago
p5i4afnyx ea0852b7b8 Merge pull request '完善了READEME文件' (#20) from cxf into main
2 months ago
chenxiaofu ff0e88ba39 完善了README.md文件
2 months ago
p5i4afnyx 4cee1f479b Merge pull request '文档和uml图(最终版本)' (#19) from cxf into main
2 months ago
chenxiaofu 135d0f4569 最终版本的测试文档,需求规格说明书,设计规格说明书(同步了三个文档,调整了部分细节)
2 months ago
chenxiaofu e3144b6e27 删除了冗余数据图
2 months ago
chenxiaofu e9acf78a4a 绘制的uml图合集
2 months ago
p5i4afnyx 9841112827 Merge pull request '第一版软件测试报告' (#18) from cxf into main
2 months ago
chenxiaofu 65aee1e35d 第一版软件测试报告
2 months ago
p5i4afnyx 1d884069e3 Merge pull request '初版测试报告' (#17) from cxf into main
2 months ago
chenxiaofu abd5d7ff2e 初版测试报告
2 months ago
p5i4afnyx f2a7ccf2cf Merge pull request '文档部分基本完善' (#16) from cxf into main
2 months ago
chenxiaofu dd7f896e19 同步了软件需求规格说明书和需求构思文档,同时上传了文档中的图文件
2 months ago
p5i4afnyx 91fe671a40 Merge pull request '需求规格说明书和论文' (#15) from cxf into main
2 months ago
chenxiaofu 3c7bcef3a9 对需求规格说明书进行了初步的完善
2 months ago
chenxiaofu f320554ea0 修改了本论文部分细节
2 months ago
chenxiaofu e96e9251d6 第2版设计文档
2 months ago
p5i4afnyx 62040df04d Merge pull request '增加代码评分的前端' (#14) from cxf into main
2 months ago
chenxiaofu 65e3b5f927 增加了py代码评分功能的前端页面,目前正常上传代码并评分
2 months ago

@ -1,2 +1,219 @@
# openrank
# OpenRank 算法改进项目
这是一个基于 open-digger 和 openrank-neo4j-gds 项目的 OpenRank 算法实现,用于开发者贡献度量和开源社区分析。
## 项目架构
本项目采用模块化架构设计,主要包括以下几个核心部分:
1. **算法核心** (`src/src/algorithm/`):实现了 OpenRank 算法的核心逻辑,包括图构建、迭代计算和结果生成
2. **数据层** (`src/src/data/`):提供数据加载和管理功能,支持模拟数据和真实数据
3. **配置系统** (`src/src/config/`):管理算法参数和环境配置
4. **API 接口** (`src/src/api/`):提供统一的访问接口和查询功能
5. **工具函数** (`src/src/utils/`):通用工具和辅助函数
## 安装
```bash
# 克隆项目
git clone <repository-url>
cd openrank
# 安装依赖
npm install
# 构建项目
npm run build
```
## 快速开始
### 基础使用
```typescript
import { OpenRank } from './src';
// 创建 OpenRank 实例
const openrank = new OpenRank('./data');
// 运行 OpenRank 计算
const startDate = new Date('2024-01-01');
const endDate = new Date('2024-12-31');
const results = await openrank.calculate(startDate, endDate);
// 获取 Top 10 仓库 OpenRank
const topRepos = await openrank.getRepoOpenrank({
startYear: 2024,
startMonth: 1,
endYear: 2024,
endMonth: 12,
limit: 10,
order: 'DESC'
});
console.log('Top 10 仓库:', topRepos);
```
### 高级查询
```typescript
import { MetricsCalculator, MockDataSource } from './src';
const dataSource = new MockDataSource('./data');
const calculator = new MetricsCalculator(dataSource);
// 获取分布统计
const distribution = await calculator.getOpenrankDistribution({
startYear: 2024,
startMonth: 1,
endYear: 2024,
endMonth: 12,
});
// 比较不同时期
const comparison = await calculator.compareOpenrank(
{ startYear: 2024, startMonth: 1, endYear: 2024, endMonth: 6 },
{ startYear: 2024, startMonth: 7, endYear: 2024, endMonth: 12 },
'repo'
);
```
## 配置
### 配置文件
`config/openrank.yml` 中配置算法参数:
```yaml
global:
developerRetentionFactor: 0.5 # 开发者继承比例
repositoryRetentionFactor: 0.3 # 仓库继承比例
attenuationFactor: 0.85 # OpenRank 衰减系数
tolerance: 0.01 # 收敛容差
maxIterations: 100 # 最大迭代次数
activityWeights:
issueComment: 0.5252 # Issue 评论权重
openIssue: 2.2235 # 创建 Issue 权重
openPull: 4.0679 # 创建 PR 权重
reviewComment: 0.7427 # 代码评审权重
mergedPull: 2.0339 # 合入 PR 权重
projectActivityWeights:
# 活动类型权重(项目级 OpenRank 用)
open: 2.0
comment: 0.5
review: 1.0
close: 0.3
commit: 1.5
# 反刷与密度抑制(推荐开启):
antiGaming:
enabled: true
commentTransform: sqrt # 对高频评论做亚线性变换,降低刷量影响
commitTransform: sqrt
linearThresholds: # 前 N 条线性累加超过部分按变换sqrt/log
comment: 3
reviewComment: 3
commit: 1
perItemCap: # 每个 Issue/PR 的单项计数上限,防极端
comment: 50
reviewComment: 40
commit: 20
```
### 环境变量
```bash
# 设置全局收敛容差
export OPENRANK_GLOBAL_TOLERANCE=0.01
# 设置最大迭代次数
export OPENRANK_GLOBAL_MAX_ITERATIONS=100
```
## 算法原理
### 全域 OpenRank
全域 OpenRank 基于全局协作网络计算,考虑以下因素:
1. **网络构建**:以开发者和仓库为节点,活动关系为边
2. **权重计算**:使用活动度指标作为边权重
3. **历史继承**:节点部分继承上个月的 OpenRank 值
4. **迭代收敛**:使用改进的 PageRank 算法计算
### 项目级 OpenRank
项目级 OpenRank 在项目内部计算,包含更多节点类型:
1. **节点类型**开发者、仓库、Issue、Pull Request
2. **复杂网络**:多种关系类型和权重配置
3. **精细参数**:不同节点类型的不同继承因子
## 运行示例
```bash
# 运行基础示例
npm run dev
# 运行测试
npm test
# 检查代码质量
npm run lint
```
## A/B 评估:仓库事件影响
为评估是否引入仓库层事件Star/Fork/Release 等)对项目级 OpenRank 的影响,本项目提供了 A/B 对比脚本:
- 脚本:`scripts/ab_evaluate_repo_events.ts`
- 运行方式:`npm run ab:repo-events`
- 环境变量(可选):
- `GITHUB_TOKEN`GitHub 访问令牌,避免触发未认证的频率限制
- `OR_AB_OWNER`/`OR_AB_REPO`:目标仓库,默认 `FISCO-BCOS/FISCO-BCOS`
## 项目结构
```
openrank/
├── doc/ # 项目文档和规格说明书
├── front/ # 前端子系统
├── model/ # 设计模型和图表
├── src/ # 源代码目录
│ ├── config/ # 配置文件
│ ├── data/ # 数据存储
│ ├── scripts/ # 辅助脚本
│ ├── src/ # 核心源代码
│ ├── test/ # 测试文件
│ └── test_data/ # 测试数据
└── 文献/ # 参考论文和资料
```
## 开发指南
### 添加新的数据源
1. 实现 `DataSource` 接口
2. 在 `src/src/data/` 目录下创建新的数据源类
3. 更新导出文件
### 自定义算法参数
1. 修改 `config/openrank.yml` 配置文件
2. 或使用环境变量覆盖特定参数
3. 或在代码中动态设置配置
## 测试
```bash
# 运行单元测试
npm test
# 运行覆盖率测试
npm run test:coverage
# 运行集成测试
npm run test:integration
```

Binary file not shown.

@ -23,10 +23,268 @@ class OpenRankDashboard {
this.updateLastUpdateTime();
this.setupTabNavigation();
this.setupModeSwitch();
this.setupCodeReviewPage();
// 默认显示开发者分析数据
this.displayDeveloperAnalysis(this.mockData.developers);
this.hydrateFromURL();
}
// 设置代码测评页面功能
setupCodeReviewPage() {
// 运行Pylint测评按钮
document.getElementById('run-lint-btn')?.addEventListener('click', () => {
this.runCodeLinting();
});
// 清空代码按钮
document.getElementById('clear-code-btn')?.addEventListener('click', () => {
const codeInput = document.getElementById('python-code-input');
if (codeInput) {
codeInput.value = '';
this.resetCodeReviewResults();
}
});
// 加载示例按钮
document.getElementById('load-example-btn')?.addEventListener('click', () => {
this.loadExampleCode();
});
}
// 加载示例代码
loadExampleCode() {
const exampleCode = `def calculate_average(numbers):
"""计算数字列表的平均值"""
if not numbers:
return 0
total = sum(numbers)
return total / len(numbers)
def get_user_name():
name = input("请输入您的名字: ")
print(f"你好, {name}!")
return name
# 主程序
if __name__ == "__main__":
user = get_user_name()
scores = [85, 90, 78, 92, 88]
avg = calculate_average(scores)
print(f"平均分数是: {avg}")`;
const codeInput = document.getElementById('python-code-input');
if (codeInput) {
codeInput.value = exampleCode;
this.resetCodeReviewResults();
}
}
// 重置代码测评结果
resetCodeReviewResults() {
document.getElementById('code-score').textContent = '0';
document.getElementById('score-quality').textContent = '待评分';
document.getElementById('score-quality').className = 'score-quality';
document.getElementById('issues-count').textContent = '0';
document.getElementById('issues-list').innerHTML = '<p class="empty-message">请运行测评查看代码中的问题</p>';
document.getElementById('lint-summary').innerHTML = '<p class="empty-message">请运行测评查看代码质量总结</p>';
}
// 运行代码测评
runCodeLinting() {
const codeInput = document.getElementById('python-code-input');
if (!codeInput || !codeInput.value.trim()) {
this.showNotification('请先输入Python代码', 'error');
return;
}
// 模拟Pylint测评过程
this.simulatePylintAnalysis(codeInput.value);
}
// 模拟Pylint分析由于浏览器限制使用模拟数据
simulatePylintAnalysis(code) {
// 这里使用模拟数据实际项目中应该调用后端API
const codeLines = code.split('\n');
// 根据代码内容生成模拟的Pylint问题
const issues = [];
// 检查是否有未使用的导入(模拟)
if (code.includes('import') && !code.includes('from ')) {
issues.push({
type: 'info',
message: '导入了模块但未使用',
line: 1
});
}
// 检查是否有函数缺少文档字符串(模拟)
let inFunction = false;
let functionStartLine = 0;
codeLines.forEach((line, index) => {
if (line.trim().startsWith('def ')) {
inFunction = true;
functionStartLine = index + 1;
} else if (inFunction && line.trim().startsWith('#') || line.trim().startsWith('"""')) {
inFunction = false;
} else if (inFunction && line.trim() && !line.trim().startsWith(' ') && !line.trim().startsWith('#')) {
issues.push({
type: 'warning',
message: '缺少函数文档字符串',
line: functionStartLine
});
inFunction = false;
}
});
// 检查行长度(模拟)
codeLines.forEach((line, index) => {
if (line.length > 79) {
issues.push({
type: 'warning',
message: '行长度超过79个字符',
line: index + 1
});
}
});
// 检查是否有print语句模拟
codeLines.forEach((line, index) => {
if (line.includes('print(') && !line.includes('"""') && !line.includes("''")) {
issues.push({
type: 'info',
message: '考虑使用logging模块替代print语句',
line: index + 1
});
}
});
// 检查是否有input语句模拟
codeLines.forEach((line, index) => {
if (line.includes('input(')) {
issues.push({
type: 'warning',
message: '生产环境中应避免使用input()函数',
line: index + 1
});
}
});
// 生成随机的额外问题(增加模拟真实性)
const randomIssues = [
{ type: 'info', message: '考虑添加类型提示以提高代码可读性', line: Math.floor(Math.random() * codeLines.length) + 1 },
{ type: 'warning', message: '变量名可以更具描述性', line: Math.floor(Math.random() * codeLines.length) + 1 },
{ type: 'info', message: '考虑添加单元测试', line: 1 }
];
// 随机添加1-2个额外问题
const additionalIssues = Math.floor(Math.random() * 2) + 1;
for (let i = 0; i < additionalIssues; i++) {
issues.push(randomIssues[i]);
}
// 计算分数(基于问题数量和严重性)
let score = 10;
issues.forEach(issue => {
if (issue.type === 'error') score -= 3;
else if (issue.type === 'warning') score -= 1;
else if (issue.type === 'info') score -= 0.5;
});
score = Math.max(0, Math.min(10, score));
score = Math.round(score * 10) / 10; // 保留一位小数
// 显示结果
this.displayLintResults(score, issues);
}
// 显示测评结果
displayLintResults(score, issues) {
// 更新分数显示
const scoreElement = document.getElementById('code-score');
const qualityElement = document.getElementById('score-quality');
scoreElement.textContent = score.toString();
// 设置质量等级
let qualityText = '优秀';
let qualityClass = 'excellent';
if (score >= 9) {
qualityText = '优秀';
qualityClass = 'excellent';
} else if (score >= 7) {
qualityText = '良好';
qualityClass = 'good';
} else if (score >= 5) {
qualityText = '一般';
qualityClass = 'average';
} else {
qualityText = '需改进';
qualityClass = 'poor';
}
qualityElement.textContent = qualityText;
qualityElement.className = `score-quality ${qualityClass}`;
// 更新CSS样式以反映不同的质量等级
if (qualityClass === 'excellent') {
qualityElement.style.background = 'rgba(0, 255, 0, 0.2)';
qualityElement.style.border = '1px solid rgba(0, 255, 0, 0.3)';
qualityElement.style.color = '#66ff99';
} else if (qualityClass === 'good') {
qualityElement.style.background = 'rgba(0, 212, 255, 0.2)';
qualityElement.style.border = '1px solid rgba(0, 212, 255, 0.3)';
qualityElement.style.color = '#00d4ff';
} else if (qualityClass === 'average') {
qualityElement.style.background = 'rgba(255, 165, 0, 0.2)';
qualityElement.style.border = '1px solid rgba(255, 165, 0, 0.3)';
qualityElement.style.color = '#ffcc66';
} else {
qualityElement.style.background = 'rgba(255, 0, 0, 0.2)';
qualityElement.style.border = '1px solid rgba(255, 0, 0, 0.3)';
qualityElement.style.color = '#ff6b6b';
}
// 更新问题列表
document.getElementById('issues-count').textContent = issues.length.toString();
const issuesList = document.getElementById('issues-list');
if (issues.length === 0) {
issuesList.innerHTML = '<p class="empty-message">代码质量良好,未发现问题</p>';
} else {
// 按行号排序
issues.sort((a, b) => a.line - b.line);
issuesList.innerHTML = issues.map(issue => `
<div class="issue-item">
<div class="issue-header">
<span class="issue-type ${issue.type}">${issue.type === 'error' ? '错误' : issue.type === 'warning' ? '警告' : '信息'}</span>
<span class="issue-line"> ${issue.line} </span>
</div>
<div class="issue-message">${issue.message}</div>
</div>
`).join('');
}
// 生成总结
const summaryElement = document.getElementById('lint-summary');
const errorCount = issues.filter(i => i.type === 'error').length;
const warningCount = issues.filter(i => i.type === 'warning').length;
const infoCount = issues.filter(i => i.type === 'info').length;
summaryElement.innerHTML = `
<ul>
<li><i class="fas fa-check-circle"></i> : ${score}/10 (${qualityText})</li>
<li><i class="fas fa-exclamation-circle"></i> ${errorCount} </li>
<li><i class="fas fa-exclamation-triangle"></i> ${warningCount} </li>
<li><i class="fas fa-info-circle"></i> ${infoCount} </li>
<li><i class="fas fa-code"></i> : ${document.getElementById('python-code-input').value.split('\n').length}</li>
</ul>
<p style="margin-top: 1rem;">
${score >= 7 ? '代码整体质量良好,继续保持!' : '建议根据提示改进代码质量。'}
</p>
`;
}
// 设置事件监听器
setupEventListeners() {
@ -894,93 +1152,139 @@ class OpenRankDashboard {
// 初始化分布图表
initDistributionChart() {
const ctx = document.getElementById('distribution-canvas').getContext('2d');
this.charts.distribution = new Chart(ctx, {
type: 'bar',
data: {
labels: this.mockData.openrankDistribution.map(d => d.range),
datasets: [{
label: '开发者数量',
data: this.mockData.openrankDistribution.map(d => d.count),
backgroundColor: 'rgba(0, 212, 255, 0.6)',
borderColor: 'rgba(0, 212, 255, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#ffffff'
}
}
try {
const canvas = document.getElementById('distribution-canvas');
if (!canvas) {
console.warn('Distribution canvas not found');
return;
}
const ctx = canvas.getContext('2d');
if (typeof Chart === 'undefined') {
console.error('Chart.js is not loaded');
// 显示错误信息
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#666';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText('图表库加载失败', canvas.width/2, canvas.height/2 - 10);
ctx.font = '12px Arial';
ctx.fillText('图表功能暂不可用', canvas.width/2, canvas.height/2 + 10);
return;
}
this.charts.distribution = new Chart(ctx, {
type: 'bar',
data: {
labels: this.mockData.openrankDistribution.map(d => d.range),
datasets: [{
label: '开发者数量',
data: this.mockData.openrankDistribution.map(d => d.count),
backgroundColor: 'rgba(0, 212, 255, 0.6)',
borderColor: 'rgba(0, 212, 255, 1)',
borderWidth: 1
}]
},
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#ffffff'
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: '#ffffff'
}
}
},
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
scales: {
y: {
beginAtZero: true,
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#ffffff'
}
},
ticks: {
color: '#ffffff'
x: {
grid: {
color: 'rgba(255, 255, 255, 0.1)'
},
ticks: {
color: '#ffffff'
}
}
}
}
}
});
});
} catch (error) {
console.error('Error initializing distribution chart:', error);
}
}
// 初始化活动类型图表
initActivityChart() {
const ctx = document.getElementById('activity-canvas').getContext('2d');
this.charts.activity = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Issue 评论', '创建 Issue', '创建 PR', '代码评审', '合入 PR'],
datasets: [{
data: [
this.mockData.activityDistribution.issueComment,
this.mockData.activityDistribution.openIssue,
this.mockData.activityDistribution.openPull,
this.mockData.activityDistribution.reviewComment,
this.mockData.activityDistribution.mergedPull
],
backgroundColor: [
'rgba(0, 212, 255, 0.8)',
'rgba(0, 255, 136, 0.8)',
'rgba(255, 193, 7, 0.8)',
'rgba(156, 39, 176, 0.8)',
'rgba(255, 87, 34, 0.8)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
color: '#ffffff',
font: {
size: 12
try {
const canvas = document.getElementById('activity-canvas');
if (!canvas) {
console.warn('Activity canvas not found');
return;
}
const ctx = canvas.getContext('2d');
if (typeof Chart === 'undefined') {
console.error('Chart.js is not loaded');
// 显示错误信息
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = '#666';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText('图表库加载失败', canvas.width/2, canvas.height/2 - 10);
ctx.font = '12px Arial';
ctx.fillText('图表功能暂不可用', canvas.width/2, canvas.height/2 + 10);
return;
}
this.charts.activity = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Issue 评论', '创建 Issue', '创建 PR', '代码评审', '合入 PR'],
datasets: [{
data: [
this.mockData.activityDistribution.issueComment,
this.mockData.activityDistribution.openIssue,
this.mockData.activityDistribution.openPull,
this.mockData.activityDistribution.reviewComment,
this.mockData.activityDistribution.mergedPull
],
backgroundColor: [
'rgba(0, 212, 255, 0.8)',
'rgba(0, 255, 136, 0.8)',
'rgba(255, 193, 7, 0.8)',
'rgba(156, 39, 176, 0.8)',
'rgba(255, 87, 34, 0.8)'
],
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'right',
labels: {
color: '#ffffff',
font: {
size: 12
}
}
}
}
}
}
});
});
} catch (error) {
console.error('Error initializing activity chart:', error);
}
}
// 初始化网络图谱

@ -27,6 +27,7 @@
<button class="nav-btn" data-tab="repositories">仓库分析</button>
<button class="nav-btn" data-tab="developers">开发者分析</button>
<button class="nav-btn" data-tab="network">网络图谱</button>
<button class="nav-btn" data-tab="code-review">代码测评</button>
<button class="nav-btn" data-tab="settings">配置</button>
</nav>
<div class="header-actions">
@ -39,6 +40,61 @@
<!-- 主要内容区域 -->
<main class="main-content" id="global-root">
<!-- 代码测评面板 -->
<div class="tab-content" id="code-review">
<div class="code-review-container">
<h2>Python代码测评</h2>
<div class="code-input-section">
<div class="code-editor-controls">
<button id="run-lint-btn" class="btn-primary">
<i class="fas fa-check-circle"></i>
运行Pylint测评
</button>
<button id="clear-code-btn" class="btn-secondary">
<i class="fas fa-trash"></i>
清空代码
</button>
<button id="load-example-btn" class="btn-secondary">
<i class="fas fa-file-code"></i>
加载示例
</button>
</div>
<div class="code-editor-wrapper">
<textarea id="python-code-input" placeholder="请在此输入Python代码...
例如:
def hello():
print('Hello World')
if __name__ == '__main__':
hello()"></textarea>
</div>
</div>
<div class="results-section">
<div class="score-card">
<h3>代码评分</h3>
<div class="score-display">
<span id="code-score">0</span>
<div class="score-label">/ 10</div>
</div>
<div class="score-quality" id="score-quality">待评分</div>
</div>
<div class="issues-container">
<h3>问题列表 <span id="issues-count">0</span></h3>
<div id="issues-list" class="issues-list">
<p class="empty-message">请运行测评查看代码中的问题</p>
</div>
</div>
</div>
<div class="summary-section">
<h3>测评总结</h3>
<div id="lint-summary" class="summary-content">
<p class="empty-message">请运行测评查看代码质量总结</p>
</div>
</div>
</div>
</div>
<!-- 总览面板 -->
<div class="tab-content active" id="overview">
<div class="stats-grid">
@ -299,7 +355,47 @@
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<!-- Chart.js CDN with fallback -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
<script>
// Fallback to local Chart.js if CDN fails
if (typeof Chart === 'undefined') {
document.write('<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.0/chart.umd.js"><\/script>');
}
// Final fallback - load minimal mock if both CDN fail
setTimeout(function() {
if (typeof Chart === 'undefined') {
console.warn('Chart.js failed to load from CDN, creating minimal mock');
window.Chart = function(ctx, config) {
this.ctx = ctx;
this.config = config;
// Mock implementation to prevent errors
this.destroy = function() {};
this.update = function() {};
this.resize = function() {};
// Draw a simple placeholder
if (ctx && ctx.canvas) {
const canvas = ctx.canvas;
const width = canvas.width || 300;
const height = canvas.height || 200;
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = '#666';
ctx.font = '14px Arial';
ctx.textAlign = 'center';
ctx.fillText('Chart.js 加载失败', width/2, height/2 - 10);
ctx.font = '12px Arial';
ctx.fillText('图表功能暂不可用', width/2, height/2 + 10);
}
};
// Mock chart types
Chart.defaults = {};
Chart.controllers = {};
}
}, 3000);
</script>
<script src="app.js"></script>
</body>
</html>

@ -98,6 +98,273 @@ body {
.project-header-inline { display:flex; align-items:flex-start; justify-content:space-between; gap:1rem; flex-wrap:wrap; margin-bottom:1rem; }
.project-header-inline h2 { margin:0; font-size:1.25rem; font-weight:700; }
.badge { display:inline-block; padding:.25rem .6rem; font-size:.65rem; letter-spacing:.5px; background:rgba(0,212,255,.15); border:1px solid rgba(0,212,255,.4); border-radius:6px; color:#00d4ff; font-weight:600; }
/* 代码测评页面样式 */
.code-review-container {
padding: 2rem 0;
}
.code-review-container h2 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 1.5rem;
color: #ffffff;
}
.code-input-section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.code-editor-controls {
display: flex;
gap: 1rem;
margin-bottom: 1rem;
flex-wrap: wrap;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
color: #ffffff;
padding: 0.5rem 1rem;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.5rem;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.3);
}
.code-editor-wrapper {
position: relative;
}
#python-code-input {
width: 100%;
min-height: 300px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
color: #ffffff;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 0.9rem;
line-height: 1.5;
padding: 1rem;
resize: vertical;
outline: none;
}
#python-code-input:focus {
border-color: #00d4ff;
box-shadow: 0 0 0 2px rgba(0, 212, 255, 0.25);
}
.results-section {
display: grid;
grid-template-columns: 1fr 3fr;
gap: 1.5rem;
margin-bottom: 1.5rem;
}
.score-card {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.score-card h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.8);
}
.score-display {
display: flex;
align-items: baseline;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1rem;
}
#code-score {
font-size: 3rem;
font-weight: 700;
color: #00d4ff;
}
.score-label {
font-size: 1.25rem;
color: rgba(255, 255, 255, 0.6);
}
#score-quality {
font-size: 1rem;
font-weight: 600;
padding: 0.25rem 0.75rem;
border-radius: 6px;
background: rgba(0, 212, 255, 0.2);
border: 1px solid rgba(0, 212, 255, 0.3);
color: #00d4ff;
}
.issues-container {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.issues-container h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: space-between;
}
#issues-count {
font-size: 0.85rem;
padding: 0.2rem 0.5rem;
background: rgba(0, 212, 255, 0.2);
border-radius: 4px;
color: #00d4ff;
}
.issues-list {
max-height: 400px;
overflow-y: auto;
}
.issue-item {
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 6px;
padding: 0.75rem;
margin-bottom: 0.75rem;
}
.issue-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.issue-type {
font-size: 0.75rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.issue-type.error {
background: rgba(255, 0, 0, 0.2);
color: #ff6b6b;
border: 1px solid rgba(255, 0, 0, 0.3);
}
.issue-type.warning {
background: rgba(255, 165, 0, 0.2);
color: #ffcc66;
border: 1px solid rgba(255, 165, 0, 0.3);
}
.issue-type.info {
background: rgba(0, 128, 0, 0.2);
color: #66ff99;
border: 1px solid rgba(0, 128, 0, 0.3);
}
.issue-line {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.6);
}
.issue-message {
font-size: 0.85rem;
color: rgba(255, 255, 255, 0.8);
line-height: 1.4;
}
.summary-section {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
padding: 1.5rem;
}
.summary-section h3 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 1rem;
color: rgba(255, 255, 255, 0.8);
}
.summary-content {
font-size: 0.9rem;
line-height: 1.6;
color: rgba(255, 255, 255, 0.7);
}
.summary-content ul {
list-style: none;
padding: 0;
}
.summary-content li {
margin-bottom: 0.75rem;
display: flex;
align-items: flex-start;
gap: 0.75rem;
}
.summary-content li i {
color: #00d4ff;
margin-top: 0.25rem;
min-width: 16px;
}
.empty-message {
color: rgba(255, 255, 255, 0.5);
font-style: italic;
text-align: center;
padding: 2rem 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.results-section {
grid-template-columns: 1fr;
}
.code-editor-controls {
flex-direction: column;
}
.btn-primary, .btn-secondary {
width: 100%;
justify-content: center;
}
}
.badge.approx { background:rgba(255,193,7,.15); border-color:rgba(255,193,7,.4); color:#ffc107; }
.badge.full { background:rgba(0,255,136,.15); border-color:rgba(0,255,136,.4); color:#00ff88; }

@ -0,0 +1,90 @@
classDiagram
direction TB
class OpenRankDashboard {
-currentTab: string
-mode: string
-charts: object
-mockData: object
-projectState: object
+init()
+setupEventListeners()
+setupTabNavigation()
+switchTab(tab: string)
+switchMode(mode: string)
+loadMockData()
+initCharts()
+generateMockData()
}
class Overview {
+updateStats()
+updateRankingTable()
+initDistributionChart()
+initActivityChart()
}
class Repositories {
+updateRepositoryGrid()
+filterRepositories(filter: string)
+onViewRepoDetails(repoName: string)
+onViewRepoContributions(repoName: string)
+showRepoDetails(owner: string, repo: string)
}
class Developers {
+displayDeveloperAnalysis(developers: array)
+filterDevelopers(keyword: string)
}
class Network {
+initNetworkGraph()
}
class CodeReview {
+setupCodeReviewPage()
+loadExampleCode()
+runCodeLinting()
+simulatePylintAnalysis(code: string)
+displayLintResults(score: number, issues: array)
+resetCodeReviewResults()
}
class Settings {
+setupConfigListeners()
+updateConfig(id: string, value: any)
}
class ProjectMode {
+buildProjectListFromGlobal()
+renderProjectList(keyword: string)
+renderApproxProject(fullName: string, days: number)
+mountProjectView(snapshot: object)
+triggerFullProjectCalculation(fullName: string)
+autoTriggerFull(fullName: string, days: number)
+pollTask(taskId: string, fullName: string, silent: boolean)
}
OpenRankDashboard --> Overview: contains
OpenRankDashboard --> Repositories: contains
OpenRankDashboard --> Developers: contains
OpenRankDashboard --> Network: contains
OpenRankDashboard --> CodeReview: contains
OpenRankDashboard --> Settings: contains
OpenRankDashboard --> ProjectMode: contains
class DataLoader {
+fetchProjectOverview(owner: string, repo: string)
+fetchRepoDetails(owner: string, repo: string)
+fetchContributors(owner: string, repo: string)
}
class UIHelper {
+showNotification(message: string, type: string)
+updateLastUpdateTime()
+pushURLState()
+hydrateFromURL()
}
OpenRankDashboard --> DataLoader: uses
OpenRankDashboard --> UIHelper: uses

@ -0,0 +1,166 @@
classDiagram
direction TB
%% 核心算法类
class OpenRankCalculator {
- config: OpenRankConfig
- graph: Graph
- calculationStatus: CalculationStatus
+ calculate(activityData: ActivityData[], lastMonthOpenRank: Map~string, number~): Promise~OpenRankResult[]~
- buildGraph(activityData: ActivityData[], lastMonthOpenRank: Map~string, number~): void
- iterativeCalculation(): Promise~void~
- generateResults(): OpenRankResult[]
+ getCalculationStatus(): CalculationStatus
+ getGraphStats()
+ cleanup(): void
}
class ProjectOpenRankCalculator {
- config: OpenRankConfig
- graph: Graph
- calculationStatus: CalculationStatus
- weightingFramework: WeightingFramework
+ calculateProjectOpenRank(issueData: IssueData[], pullRequestData: PullRequestData[], lastMonthOpenRank: Map~string, number~, repoEvents?: RepoEventActivity[]): Promise~OpenRankResult[]~
- buildProjectGraph(issueData: IssueData[], pullRequestData: PullRequestData[], lastMonthOpenRank: Map~string, number~, repoEvents?: RepoEventActivity[]): void
- computeCodeImpactPools(pr: any, totalLines: number, filesChanged: number, ...): object
}
class SimpleGraph {
+ nodes: Map~string, GraphNode~
+ edges: GraphEdge[]
- adjacencyList: Map~string, Set~string~~
- incomingEdges: Map~string, GraphEdge[]~
- outgoingEdges: Map~string, GraphEdge[]~
+ addNode(node: GraphNode): void
+ addEdge(edge: GraphEdge): void
+ getNode(id: string): GraphNode | undefined
+ getNeighbors(nodeId: string): GraphNode[]
+ getIncomingEdges(nodeId: string): GraphEdge[]
+ getOutgoingEdges(nodeId: string): GraphEdge[]
+ clear(): void
+ getStats()
+ isConnected(): boolean
+ getDegree(nodeId: string)
+ getNodesByType(type: string): GraphNode[]
}
%% 数据收集类
class ProjectData {
issues: IssueData[]
pullRequests: PullRequestData[]
}
class GitHubDataCollector {
- config: GitHubConfig
- baseUrl: string
+ collectProjectData(repo: GitHubRepo, startDate: Date, endDate: Date): Promise~ProjectData~
- listIssues(repo: GitHubRepo, startDate: Date, endDate: Date): Promise~number[]~
- listPulls(repo: GitHubRepo, startDate: Date, endDate: Date): Promise~number[]~
- collectIssueData(repo: GitHubRepo, issueIds: number[], events: any[]): Promise~IssueData[]~
- collectPullRequestData(repo: GitHubRepo, prIds: number[], events: any[]): Promise~PullRequestData[]~
}
%% API服务类
class APIServer {
- app: Express.Application
- calculationCache: Record~string, CalcCacheEntry~
- projectCache: Record~string, ProjectSnapshotCacheEntry~
- tasks: Record~string, TaskEntry~
+ start(): void
- cleanupCaches(): void
- createTask(): TaskEntry
- getTask(id: string): TaskEntry | null
- updateTask(id: string, patch: Partial~TaskEntry~): void
}
%% 数据库类
class MySQLConnection {
+ pool: Pool
+ closePool(): Promise~void~
}
%% 接口和类型
class ActivityData {
platform: PlatformType
repoId: number
repoName: string
orgId?: number
orgName?: string
actorId: number
actorLogin: string
issueComment: number
openIssue: number
openPull: number
reviewComment: number
mergedPull: number
createdAt: Date
}
class IssueData {
id: number
number?: number
platform: PlatformType
repoId: number
repoName: string
title: string
authorId: number
authorLogin: string
createdAt: Date
closedAt?: Date
state: 'open' | 'closed'
activities: any[]
reactions: any
}
class PullRequestData {
id: number
number?: number
platform: PlatformType
repoId: number
repoName: string
title: string
authorId: number
authorLogin: string
createdAt: Date
mergedAt?: Date
closedAt?: Date
state: 'open' | 'closed' | 'merged'
activities: any[]
reactions: any
filesChanged?: number
additions?: number
deletions?: number
}
class GraphNode {
id: string
type: NodeType
platform: PlatformType
name: string
openrank: number
lastOpenrank: number
initValue: number
retentionFactor: number
converged: boolean
metadata?: Record~string, any~
}
class GraphEdge {
source: string
target: string
weight: number
type: 'activity' | 'belong' | 'reverse_belong' | 'reverse_activity'
activityDetails?: any
}
%% 关系
OpenRankCalculator --> SimpleGraph: uses
ProjectOpenRankCalculator --> SimpleGraph: uses
APIServer --> OpenRankCalculator: calls
APIServer --> ProjectOpenRankCalculator: calls
APIServer --> GitHubDataCollector: uses
APIServer --> MySQLConnection: uses
OpenRankCalculator --> ActivityData: processes
ProjectOpenRankCalculator --> IssueData: processes
ProjectOpenRankCalculator --> PullRequestData: processes
SimpleGraph --> GraphNode: contains
SimpleGraph --> GraphEdge: contains

@ -0,0 +1,135 @@
@startuml
skinparam class {
BackgroundColor<<boundary>> LightSkyBlue
BackgroundColor<<control>> LightGreen
BackgroundColor<<entity>> Gold
}
class MainUI <<boundary>> {
- checkUI: CheckUI
+ startPage()
+ showMainInterface()
+ navigateToCodeAssessmentPage()
}
class CheckUI <<boundary>> {
- pylint: Pylint
- currentAssessment: CodeAssessment
+ showCodeAssessmentPage()
+ uploadCodeToBeTested(code: CodeFile): boolean
+ displayAssessmentResult(result: AssessmentResult)
}
class Pylint <<control>> {
- database: DateBase
+ assessCodeQuality(code: CodeFile): AssessmentResult
+ generateQualityReport(assessment: AssessmentResult): QualityReport
}
class DateBase <<entity>> {
- codeAssessments: List<CodeAssessment>
- qualityScores: List<QualityScore>
- assessmentHistory: List<AssessmentHistory>
+ storeCodeScore(assessment: CodeAssessment): boolean
+ getAssessmentHistory(codeId: String): List<AssessmentHistory>
+ getAverageQualityScore(projectId: String): double
}
class CodeFile <<entity>> {
- codeId: String
- fileName: String
- filePath: String
- language: String
- content: String
- size: long
+ getCodeId(): String
+ getFileName(): String
+ getLanguage(): String
+ getContent(): String
}
class AssessmentResult <<entity>> {
- assessmentId: String
- codeId: String
- qualityScore: double
- issues: List<CodeIssue>
- assessmentTime: Date
+ getQualityScore(): double
+ getIssues(): List<CodeIssue>
+ generateSummary(): String
}
class CodeAssessment <<entity>> {
- assessmentId: String
- codeFile: CodeFile
- result: AssessmentResult
- assessmentDate: Date
+ getAssessmentId(): String
+ getAssessmentDate(): Date
+ getResult(): AssessmentResult
}
class CodeIssue <<entity>> {
- issueId: String
- type: String
- severity: String
- lineNumber: int
- description: String
- suggestion: String
+ getIssueId(): String
+ getType(): String
+ getSeverity(): String
+ getDescription(): String
}
class QualityScore <<entity>> {
- scoreId: String
- codeId: String
- overallScore: double
- maintainabilityScore: double
- reliabilityScore: double
- efficiencyScore: double
+ getOverallScore(): double
+ getMaintainabilityScore(): double
+ calculateCompositeScore(): double
}
class QualityReport <<entity>> {
- reportId: String
- assessmentId: String
- summary: String
- detailedAnalysis: String
- recommendations: List<String>
+ generateReport(): String
+ exportAsPDF(): void
}
class AssessmentHistory <<entity>> {
- historyId: String
- codeId: String
- assessmentDate: Date
- qualityScore: double
- trend: String
+ getHistoryId(): String
+ getAssessmentDate(): Date
+ getQualityScore(): double
+ calculateTrend(): String
}
' 关联关系
MainUI --> CheckUI : "跳转"
CheckUI --> Pylint : "调用测评"
Pylint --> DateBase : "存储分数"
Pylint ..> AssessmentResult : "生成"
DateBase "1" *-- "many" CodeAssessment : "存储"
DateBase "1" *-- "many" QualityScore : "包含"
DateBase "1" *-- "many" AssessmentHistory : "记录历史"
CodeAssessment "1" *-- "1" CodeFile : "测评"
CodeAssessment "1" *-- "1" AssessmentResult : "包含结果"
AssessmentResult "1" *-- "many" CodeIssue : "包含问题"
AssessmentResult "1" *-- "1" QualityScore : "关联分数"
Pylint ..> QualityReport : "生成报告"
@enduml

@ -1,64 +0,0 @@
@startuml
skinparam dpi 180
skinparam shadowing false
skinparam ArrowColor #00AEEF
skinparam DefaultFontName Microsoft YaHei
skinparam ArrowThickness 1.2
title 界面跳转图OpenRank 可视化系统)
[*] --> 主界面 : 进入系统
state 主界面
state 开发者分析
state 仓库概览
state 网络图谱
state 项目模式概览
state 计算弹窗
state 算法配置界面
state "仓库详情 Modal" as 仓库详情
state "项目 Overview (approx)" as 项目Approx
state "项目 Overview (full)" as 项目Full
' 主界面导航
主界面 --> 开发者分析
主界面 --> 仓库概览
主界面 --> 网络图谱
主界面 --> 项目模式概览
主界面 --> 计算弹窗 : 一键计算
' 计算弹窗返回刷新各视图
计算弹窗 --> 主界面 : 提交 owner/repo
计算弹窗 --> 开发者分析 : 结果刷新
计算弹窗 --> 仓库概览 : 结果刷新
计算弹窗 --> 项目模式概览 : 可切换模式
' 仓库概览交互
仓库概览 --> 仓库详情 : 查看详情
仓库概览 --> 开发者分析 : 贡献分析
仓库概览 --> 仓库概览 : 关键词/筛选
' 仓库详情交互
仓库详情 --> 仓库详情 : 时间窗口/粒度
仓库详情 --> 仓库概览 : 关闭
仓库详情 --> 开发者分析 : 跳转贡献者
' 项目模式approx/full交互
项目模式概览 --> 项目Approx : 选择仓库
项目Approx --> 项目Full : 触发正式重算
项目Full --> 项目Approx : 计算完成 替换 approx
项目Approx --> 项目Approx : 贡献者 Top/活动占比
项目Approx --> 开发者分析 : 跳转贡献者分析
' 网络图谱与算法配置
网络图谱 --> 算法配置界面 : 参数调节
算法配置界面 --> 网络图谱 : 保存参数
算法配置界面 --> 主界面 : 近似预览/正式重算
' 返回导航
开发者分析 --> 主界面 : 返回
仓库概览 --> 主界面 : 返回
网络图谱 --> 主界面 : 返回
项目模式概览 --> 主界面 : 返回
@enduml

@ -1,396 +0,0 @@
# OpenRank 复现项目
这是一个基于 open-digger 和 openrank-neo4j-gds 项目的 OpenRank 算法复现实现,用于开发者贡献度量和开源社区分析。
## 项目概述
OpenRank 是由 X-lab 开发的开源项目价值评估算法,基于 PageRank 改进而来,专门用于评估开源生态中开发者和项目的贡献价值。本项目提供了一个完整的 OpenRank 算法复现实现。
## 特性
- ✅ **完整的 OpenRank 算法实现**:基于原始论文和开源代码的忠实复现
- ✅ **支持多种计算模式**:全域 OpenRank 和项目级 OpenRank
- ✅ **灵活的数据源接口**:支持桩函数模拟和真实数据源
- ✅ **丰富的指标计算**:仓库、用户、社区等多维度分析
- ✅ **高性能图计算**:优化的图数据结构和迭代算法
- ✅ **完善的配置系统**:支持参数调优和环境适配
- ✅ **TypeScript 支持**:完整的类型定义和代码提示
## 安装
```bash
# 克隆项目
git clone <repository-url>
cd openrank
# 安装依赖
npm install
# 构建项目
npm run build
```
## 快速开始
### 基础使用
```typescript
import { OpenRank } from './src';
// 创建 OpenRank 实例
const openrank = new OpenRank('./data');
// 运行 OpenRank 计算
const startDate = new Date('2024-01-01');
const endDate = new Date('2024-12-31');
const results = await openrank.calculate(startDate, endDate);
// 获取 Top 10 仓库 OpenRank
const topRepos = await openrank.getRepoOpenrank({
startYear: 2024,
startMonth: 1,
endYear: 2024,
endMonth: 12,
limit: 10,
order: 'DESC'
});
console.log('Top 10 仓库:', topRepos);
```
### 高级查询
```typescript
import { MetricsCalculator, MockDataSource } from './src';
const dataSource = new MockDataSource('./data');
const calculator = new MetricsCalculator(dataSource);
// 获取分布统计
const distribution = await calculator.getOpenrankDistribution({
startYear: 2024,
startMonth: 1,
endYear: 2024,
endMonth: 12,
});
// 比较不同时期
const comparison = await calculator.compareOpenrank(
{ startYear: 2024, startMonth: 1, endYear: 2024, endMonth: 6 },
{ startYear: 2024, startMonth: 7, endYear: 2024, endMonth: 12 },
'repo'
);
```
## 配置
### 配置文件
`config/openrank.yml` 中配置算法参数:
```yaml
global:
developerRetentionFactor: 0.5 # 开发者继承比例
repositoryRetentionFactor: 0.3 # 仓库继承比例
attenuationFactor: 0.85 # OpenRank 衰减系数
tolerance: 0.01 # 收敛容差
maxIterations: 100 # 最大迭代次数
activityWeights:
issueComment: 0.5252 # Issue 评论权重
openIssue: 2.2235 # 创建 Issue 权重
openPull: 4.0679 # 创建 PR 权重
reviewComment: 0.7427 # 代码评审权重
mergedPull: 2.0339 # 合入 PR 权重
projectActivityWeights:
# 活动类型权重(项目级 OpenRank 用)
open: 2.0
comment: 0.5
review: 1.0
close: 0.3
commit: 1.5
# 反刷与密度抑制(推荐开启):
antiGaming:
enabled: true
commentTransform: sqrt # 对高频评论做亚线性变换,降低刷量影响
commitTransform: sqrt
linearThresholds: # 前 N 条线性累加超过部分按变换sqrt/log
comment: 3
reviewComment: 3
commit: 1
perItemCap: # 每个 Issue/PR 的单项计数上限,防极端
comment: 50
reviewComment: 40
commit: 20
# PR 贡献类型与角色建模
contributionTypeMultipliers:
open: 1.0
comment: 0.9
review: 1.1
close: 1.0
commit: 1.05
reviewerChangeRequestBonus: 1.03 # 存在 change requests 时对评审者的轻量加成
roleBonus: # 角色轻量加成(叠乘后会被 roleClamp 限制)
author: 1.05
reviewer: 1.05
committer: 1.03
commenter: 1.0
roleClamp: # 角色乘子钳制,避免叠乘过大
min: 1.0
max: 1.2
clamp: # 总贡献钳制,相对原始分项总和
min: 0.7
max: 1.6
# 仓库层事件Star/Fork/Release可选接入
repoEventWeights:
enabled: false
star: 0.5
fork: 1.0
release: 1.5
activityRatio: 0.2
reverseRatio: 0.1
```
提示:
- activityDetails.roles 会在活动边上标注作者/评审者/提交者/评论者,便于后续分析与报表。
- 通过 `getGraphSnapshot()` 可以导出包含 activityDetails 的只读快照,用于检查来源与角色细节。
### 环境变量
```bash
# 设置全局收敛容差
export OPENRANK_GLOBAL_TOLERANCE=0.01
# 设置最大迭代次数
export OPENRANK_GLOBAL_MAX_ITERATIONS=100
```
## API 参考
### 核心类
#### OpenRank
主要的 OpenRank 计算接口。
```typescript
class OpenRank {
constructor(dataPath?: string)
// 计算 OpenRank
async calculate(startDate: Date, endDate: Date): Promise<OpenRankResult[]>
// 获取仓库 OpenRank
async getRepoOpenrank(config: QueryConfig): Promise<RepoOpenRankResult[]>
// 获取用户 OpenRank
async getUserOpenrank(config: QueryConfig): Promise<UserOpenRankResult[]>
// 获取社区 OpenRank
async getCommunityOpenrank(config: QueryConfig): Promise<CommunityOpenRankResult[]>
}
```
#### OpenRankCalculator
核心算法实现。
```typescript
class OpenRankCalculator {
constructor(config: OpenRankConfig)
async calculate(
activityData: ActivityData[],
lastMonthOpenRank: Map<string, number>
): Promise<OpenRankResult[]>
getCalculationStatus(): CalculationStatus
getGraphStats(): GraphStats
}
```
#### MetricsCalculator
指标计算器。
```typescript
class MetricsCalculator {
constructor(dataSource: DataSource)
async getRepoOpenrank(config: QueryConfig): Promise<RepoOpenRankResult[]>
async getUserOpenrank(config: QueryConfig): Promise<UserOpenRankResult[]>
async getCommunityOpenrank(config: QueryConfig): Promise<CommunityOpenRankResult[]>
async getOpenrankDistribution(config: QueryConfig): Promise<DistributionStats>
async compareOpenrank(config1: QueryConfig, config2: QueryConfig): Promise<ComparisonResult>
}
```
### 查询配置
```typescript
interface QueryConfig {
startYear: number;
startMonth: number;
endYear: number;
endMonth: number;
order?: 'DESC' | 'ASC';
limit: number;
precision: number;
options?: Record<string, any>;
}
```
## 算法原理
### 全域 OpenRank
全域 OpenRank 基于全局协作网络计算,考虑以下因素:
1. **网络构建**:以开发者和仓库为节点,活动关系为边
2. **权重计算**:使用活动度指标作为边权重
3. **历史继承**:节点部分继承上个月的 OpenRank 值
4. **迭代收敛**:使用改进的 PageRank 算法计算
### 项目级 OpenRank
项目级 OpenRank 在项目内部计算,包含更多节点类型:
1. **节点类型**开发者、仓库、Issue、Pull Request
2. **复杂网络**:多种关系类型和权重配置
3. **精细参数**:不同节点类型的不同继承因子
### 关键参数
- **继承因子**:控制历史价值的保留程度
- **衰减因子**:控制不活跃节点的价值衰减
- **活动权重**:不同活动类型的重要性权重
- **收敛容差**:算法收敛的精度要求
## 运行示例
```bash
# 运行基础示例
npm run dev
# 运行测试
npm test
# 检查代码质量
npm run lint
```
## A/B 评估仓库事件影响Repo Events
为评估是否引入仓库层事件Star/Fork/Release 等)对项目级 OpenRank 的影响,本项目提供了 A/B 对比脚本:
- 脚本:`scripts/ab_evaluate_repo_events.ts`
- 运行方式:`npm run ab:repo-events`
- 环境变量(可选):
- `GITHUB_TOKEN`GitHub 访问令牌,避免触发未认证的频率限制
- `OR_AB_OWNER`/`OR_AB_REPO`:目标仓库,默认 `FISCO-BCOS/FISCO-BCOS`
- `OR_AB_MONTHS`:时间窗口(月),默认 `3`
脚本将输出:
- A不启用 repo events与 B启用 repo events之间的用户 OpenRank 相关性(皮尔逊)
- 贡献构成按事件来源的占比对比collaboration vs repo_event
提示:若遇到 GitHub API 频率限制,请设置 `GITHUB_TOKEN`,或将 `OR_AB_MONTHS` 调小(如设为 1
## 项目结构
```
openrank/
├── src/
│ ├── types/ # TypeScript 类型定义
│ ├── config/ # 配置管理
│ ├── utils/ # 工具函数
│ ├── data/ # 数据层(桩函数)
│ ├── algorithm/ # 核心算法
│ ├── metrics/ # 指标计算
│ └── index.ts # 主入口
├── config/ # 配置文件
├── examples/ # 使用示例
├── data/ # 数据存储目录
└── docs/ # 文档
```
## 开发指南
### 添加新的数据源
1. 实现 `DataSource` 接口
2. 在 `src/data/` 目录下创建新的数据源类
3. 更新导出文件
```typescript
export class CustomDataSource implements DataSource {
async loadActivityData(startDate: Date, endDate: Date): Promise<ActivityData[]> {
// 实现数据加载逻辑
}
// 实现其他必需方法...
}
```
### 自定义算法参数
1. 修改 `config/openrank.yml` 配置文件
2. 或使用环境变量覆盖特定参数
3. 或在代码中动态设置配置
```typescript
import { setConfig } from './src/config';
setConfig({
global: {
tolerance: 0.001,
maxIterations: 200,
}
});
```
## 测试
```bash
# 运行单元测试
npm test
# 运行覆盖率测试
npm run test:coverage
# 运行集成测试
npm run test:integration
```
## 贡献
欢迎提交 Issue 和 Pull Request
1. Fork 本仓库
2. 创建特性分支 (`git checkout -b feature/amazing-feature`)
3. 提交更改 (`git commit -m 'Add some amazing feature'`)
4. 推送到分支 (`git push origin feature/amazing-feature`)
5. 开启 Pull Request
## 许可证
本项目采用 Apache-2.0 许可证,详见 [LICENSE](LICENSE) 文件。
## 参考资料
- [open-digger](https://github.com/X-lab2017/open-digger) - 原始项目和数据平台
- [openrank-neo4j-gds](https://github.com/X-lab2017/openrank-neo4j-gds) - Neo4j 插件实现
- [OpenRank 算法论文](https://blog.frankzhao.cn/openrank_in_project/) - 算法设计思路
- [X-lab 开放实验室](https://x-lab.info) - 项目发起方
## 联系方式
如有问题或建议,请通过以下方式联系:
- 提交 GitHub Issue
- 发送邮件至 [contact@example.com]
- 加入讨论群组 [链接]
Loading…
Cancel
Save