Compare commits

...

3 Commits

@ -0,0 +1,330 @@
(() => {
const API_ENDPOINT = 'http://localhost:4001/api/code/evaluate';
const qualityTextMap = {
excellent: '优秀',
good: '良好',
average: '一般',
poor: '需改进',
};
const qualityStyleMap = {
excellent: { background: 'rgba(0, 255, 0, 0.2)', border: '1px solid rgba(0, 255, 0, 0.3)', color: '#66ff99' },
good: { background: 'rgba(0, 212, 255, 0.2)', border: '1px solid rgba(0, 212, 255, 0.3)', color: '#00d4ff' },
average: { background: 'rgba(255, 165, 0, 0.2)', border: '1px solid rgba(255, 165, 0, 0.3)', color: '#ffcc66' },
poor: { background: 'rgba(255, 0, 0, 0.2)', border: '1px solid rgba(255, 0, 0, 0.3)', color: '#ff6b6b' },
};
const dashboardCtor = globalThis && globalThis.OpenRankDashboard ? globalThis.OpenRankDashboard : undefined;
if (!dashboardCtor) {
return;
}
const { document } = globalThis;
const escapeHtml = (value) => {
if (typeof value !== 'string') return '';
return value
.replaceAll('&', '&')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
};
const getLanguageLabel = (language) => {
const key = (language || '').toLowerCase();
if (key === 'c') return 'C';
if (key === 'cpp' || key === 'c++' || key === 'cxx') return 'C++';
return 'Python';
};
const getToolLabel = (tool) => {
const key = (tool || '').toLowerCase();
if (key === 'cppcheck') return 'Cppcheck';
if (key === 'pylint') return 'Pylint';
return tool || '未知工具';
};
const applyQualityStyles = (element, level) => {
if (!element) return;
const styles = qualityStyleMap[level] || qualityStyleMap.average;
element.style.background = styles.background;
element.style.border = styles.border;
element.style.color = styles.color;
};
const normalizeLineEndings = (text) => {
if (typeof text !== 'string') return '';
return text.replaceAll('\r\n', '\n');
};
const renderScoreQuality = (scoreElement, qualityElement, score, qualityKey, qualityText) => {
if (scoreElement) scoreElement.textContent = score.toFixed(1);
if (!qualityElement) return;
qualityElement.textContent = qualityText;
qualityElement.className = `score-quality ${qualityKey}`;
applyQualityStyles(qualityElement, qualityKey);
};
const buildIssuesMarkup = (issues) => {
return issues.map((issue) => {
let label = '信息';
if (issue.type === 'error') label = '错误';
else if (issue.type === 'warning') label = '警告';
const ruleTag = issue.rule ? `<span style="font-size:0.75rem;opacity:0.65;">${escapeHtml(issue.rule)}</span>` : '';
return `
<div class="issue-item">
<div class="issue-header">
<span class="issue-type ${escapeHtml(issue.type || 'info')}">${label}</span>
<span class="issue-line"> ${(issue.line || 0)} </span>
</div>
<div class="issue-message">${escapeHtml(issue.message || '检测到潜在问题')}</div>
${ruleTag ? `<div>${ruleTag}</div>` : ''}
</div>`;
}).join('');
};
const renderSummary = (summaryElement, summaryInfo) => {
if (!summaryElement) return;
const {
score,
qualityText,
languageLabel,
toolLabel,
runtimeLabel,
errors,
warnings,
infos,
lineCount,
} = summaryInfo;
summaryElement.innerHTML = `
<ul>
<li><i class="fas fa-check-circle"></i> : ${score.toFixed(1)}/10 (${qualityText})</li>
<li><i class="fas fa-microchip"></i> : ${languageLabel}</li>
<li><i class="fas fa-robot"></i> : ${toolLabel}</li>
<li><i class="fas fa-tachometer-alt"></i> : ${runtimeLabel}</li>
<li><i class="fas fa-exclamation-circle"></i> : ${errors} </li>
<li><i class="fas fa-exclamation-triangle"></i> : ${warnings} </li>
<li><i class="fas fa-info-circle"></i> : ${infos} </li>
<li><i class="fas fa-code"></i> : ${lineCount}</li>
</ul>
<p style="margin-top: 1rem;">
${score >= 7 ? '代码整体质量良好,继续保持!' : '建议根据提示逐项优化代码质量。'}
</p>`;
};
const exampleByLanguage = {
c: `#include <stdio.h>
int sum(const int *values, int length) {
if (values == NULL || length <= 0) {
return 0;
}
int total = 0;
for (int i = 0; i < length; ++i) {
total += values[i];
}
return total;
}
int main(void) {
int data[] = {1, 2, 3, 4, 5};
printf("total = %d\n", sum(data, 5));
return 0;
}`,
cpp: `#include <iostream>
#include <numeric>
#include <vector>
double average(const std::vector<int>& scores) {
if (scores.empty()) {
return 0.0;
}
int total = std::accumulate(scores.begin(), scores.end(), 0);
return static_cast<double>(total) / scores.size();
}
int main() {
std::vector<int> scores{85, 90, 78, 92, 88};
std::cout << "Average score: " << average(scores) << std::endl;
return 0;
}`,
python: `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 getExampleCode = (language) => {
const normalized = (language || '').toLowerCase();
if (normalized === 'c') return exampleByLanguage.c;
if (normalized === 'cpp' || normalized === 'c++' || normalized === 'cxx') return exampleByLanguage.cpp;
return exampleByLanguage.python;
};
const syncExampleIfUsingTemplate = (language, force = false) => {
const codeInput = document.getElementById('python-code-input');
if (!codeInput) return;
const nextExample = getExampleCode(language);
const isTemplate = codeInput.dataset.isTemplate === '1';
const currentValue = normalizeLineEndings(codeInput.value);
const sampleContent = normalizeLineEndings(codeInput.dataset.sampleContent || '');
const hasUserContent = currentValue.length > 0 && (!isTemplate && currentValue !== sampleContent);
if (!force && hasUserContent) return;
codeInput.value = nextExample;
codeInput.dataset.sampleLanguage = language;
codeInput.dataset.sampleContent = normalizeLineEndings(nextExample);
codeInput.dataset.isTemplate = '1';
};
const originalSetup = dashboardCtor.prototype.setupCodeReviewPage;
dashboardCtor.prototype.setupCodeReviewPage = function codeReviewSetupOverride() {
if (typeof originalSetup === 'function') {
originalSetup.call(this);
}
const languageSelect = document.getElementById('code-language-select');
const codeInput = document.getElementById('python-code-input');
codeInput?.addEventListener('input', () => {
codeInput.dataset.isTemplate = '0';
});
languageSelect?.addEventListener('change', () => {
const nextLanguage = languageSelect.value;
syncExampleIfUsingTemplate(nextLanguage);
this.resetCodeReviewResults();
});
// 初次挂载时如果文本框为空则填充默认示例Python
syncExampleIfUsingTemplate(languageSelect?.value || 'python');
};
dashboardCtor.prototype.loadExampleCode = function loadExampleCodeOverride() {
const language = document.getElementById('code-language-select')?.value || 'python';
syncExampleIfUsingTemplate(language, true);
this.resetCodeReviewResults();
};
dashboardCtor.prototype.resetCodeReviewResults = function resetCodeReviewResultsOverride() {
const scoreElement = document.getElementById('code-score');
const qualityElement = document.getElementById('score-quality');
const issuesCount = document.getElementById('issues-count');
const issuesList = document.getElementById('issues-list');
const summary = document.getElementById('lint-summary');
if (scoreElement) scoreElement.textContent = '0';
if (qualityElement) {
qualityElement.textContent = '待评分';
qualityElement.className = 'score-quality';
qualityElement.style.background = '';
qualityElement.style.border = '';
qualityElement.style.color = '';
}
if (issuesCount) issuesCount.textContent = '0';
if (issuesList) issuesList.innerHTML = '<p class="empty-message">请运行测评查看代码中的问题</p>';
if (summary) summary.innerHTML = '<p class="empty-message">请运行测评查看代码质量总结</p>';
};
dashboardCtor.prototype.runCodeLinting = function runCodeLintingOverride() {
const codeInput = document.getElementById('python-code-input');
const languageSelect = document.getElementById('code-language-select');
const language = languageSelect?.value || 'python';
if (!codeInput || !codeInput.value.trim()) {
this.showNotification('请先输入代码再运行测评', 'error');
return;
}
this.resetCodeReviewResults();
this.submitCodeForAnalysis(language, codeInput.value);
};
dashboardCtor.prototype.submitCodeForAnalysis = async function submitCodeForAnalysis(language, code) {
const runButton = document.getElementById('run-lint-btn');
const originalLabel = runButton ? runButton.innerHTML : '';
if (runButton) {
runButton.disabled = true;
runButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 分析中...';
}
try {
this.showNotification('正在运行代码测评...', 'info');
const response = await fetch(API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ language, code })
});
const payload = await response.json();
if (!response.ok) {
const message = payload?.message || '代码测评失败';
throw new Error(message);
}
this.displayLintResults(payload);
this.showNotification('代码测评完成', 'success');
} catch (error) {
const message = error instanceof Error ? error.message : '未知错误';
this.showNotification(`代码测评失败: ${message}`, 'negative');
} finally {
if (runButton) {
runButton.disabled = false;
runButton.innerHTML = originalLabel;
}
}
};
dashboardCtor.prototype.displayLintResults = function displayLintResultsOverride(result) {
const score = typeof result?.score === 'number' ? result.score : 0;
const qualityKey = result?.quality || 'average';
const qualityText = qualityTextMap[qualityKey] || qualityTextMap.average;
const issues = Array.isArray(result?.issues) ? [...result.issues] : [];
const totals = result?.totals || { errors: 0, warnings: 0, infos: 0 };
const scoreElement = document.getElementById('code-score');
const qualityElement = document.getElementById('score-quality');
const issuesCount = document.getElementById('issues-count');
const issuesList = document.getElementById('issues-list');
const summary = document.getElementById('lint-summary');
const lineCount = document.getElementById('python-code-input')?.value.split('\n').length || 0;
renderScoreQuality(scoreElement, qualityElement, score, qualityKey, qualityText);
if (issuesCount) issuesCount.textContent = issues.length.toString();
if (issuesList) {
if (issues.length === 0) {
issuesList.innerHTML = '<p class="empty-message">代码质量良好,未发现问题</p>';
} else {
issues.sort((a, b) => (a.line || 0) - (b.line || 0));
issuesList.innerHTML = buildIssuesMarkup(issues);
}
}
const languageLabel = getLanguageLabel(result?.language);
const toolLabel = getToolLabel(result?.tool);
const runtimeLabel = typeof result?.runtimeMs === 'number' ? `${Math.max(1, Math.round(result.runtimeMs))} ms` : '未知';
const errors = typeof totals.errors === 'number' ? totals.errors : issues.filter((item) => item.type === 'error').length;
const warnings = typeof totals.warnings === 'number' ? totals.warnings : issues.filter((item) => item.type === 'warning').length;
const infos = typeof totals.infos === 'number' ? totals.infos : issues.filter((item) => item.type === 'info').length;
renderSummary(summary, {
score,
qualityText,
languageLabel,
toolLabel,
runtimeLabel,
errors,
warnings,
infos,
lineCount,
});
};
})();

@ -43,12 +43,20 @@
<!-- 代码测评面板 -->
<div class="tab-content" id="code-review">
<div class="code-review-container">
<h2>Python代码测评</h2>
<h2>多语言代码测评</h2>
<div class="code-input-section">
<div class="code-editor-controls">
<div class="code-language-select-wrapper">
<i class="fas fa-code"></i>
<select id="code-language-select" aria-label="选择代码语言" style="background:transparent;border:none;color:#ffffff;font-size:0.85rem;font-weight:500;outline:none;appearance:none;cursor:pointer;">
<option value="python">Python</option>
<option value="c">C</option>
<option value="cpp">C++</option>
</select>
</div>
<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>
@ -60,15 +68,7 @@
</button>
</div>
<div class="code-editor-wrapper">
<textarea id="python-code-input" placeholder="请在此输入Python代码...
例如:
def hello():
print('Hello World')
if __name__ == '__main__':
hello()"></textarea>
<textarea id="python-code-input" placeholder="请在此输入代码,选择左侧语言后运行测评..."></textarea>
</div>
</div>
<div class="results-section">
@ -238,15 +238,15 @@ if __name__ == '__main__':
<div class="setting-card">
<h3>全局参数</h3>
<div class="setting-item">
<label>衰减因子</label>
<label for="attenuation-factor">衰减因子</label>
<input type="number" id="attenuation-factor" value="0.85" step="0.01" min="0" max="1">
</div>
<div class="setting-item">
<label>开发者继承比例</label>
<label for="developer-retention">开发者继承比例</label>
<input type="number" id="developer-retention" value="0.5" step="0.01" min="0" max="1">
</div>
<div class="setting-item">
<label>仓库继承比例</label>
<label for="repo-retention">仓库继承比例</label>
<input type="number" id="repo-retention" value="0.3" step="0.01" min="0" max="1">
</div>
</div>
@ -254,15 +254,15 @@ if __name__ == '__main__':
<div class="setting-card">
<h3>活动权重</h3>
<div class="setting-item">
<label>Issue 评论</label>
<label for="issue-comment-weight">Issue 评论</label>
<input type="number" id="issue-comment-weight" value="0.5252" step="0.0001">
</div>
<div class="setting-item">
<label>创建 Issue</label>
<label for="open-issue-weight">创建 Issue</label>
<input type="number" id="open-issue-weight" value="2.2235" step="0.0001">
</div>
<div class="setting-item">
<label>创建 PR</label>
<label for="open-pull-weight">创建 PR</label>
<input type="number" id="open-pull-weight" value="4.0679" step="0.0001">
</div>
</div>
@ -318,24 +318,24 @@ if __name__ == '__main__':
<p style="font-size:.8rem;opacity:.7;margin:.25rem 0 1rem;">请输入仓库 owner 与 repo并指定起止日期任意跨度。如不填则默认最近 30 天。可使用快捷按钮快速填充最近 30/90/180/365 天。</p>
<form id="calc-form" style="display:flex;flex-direction:column;gap:.9rem;">
<div style="display:flex;flex-direction:column;gap:.4rem;">
<label style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">Owner (组织 / 用户)</label>
<label for="calc-owner" style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">Owner (组织 / 用户)</label>
<input id="calc-owner" type="text" placeholder="例如: demo-org" autocomplete="off" required style="background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;padding:.55rem .7rem;border-radius:8px;outline:none;" />
</div>
<div style="display:flex;flex-direction:column;gap:.4rem;">
<label style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">Repo (仓库名)</label>
<label for="calc-repo" style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">Repo (仓库名)</label>
<input id="calc-repo" type="text" placeholder="例如: awesome-project" autocomplete="off" required style="background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;padding:.55rem .7rem;border-radius:8px;outline:none;" />
</div>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:.75rem;align-items:end;">
<div style="display:flex;flex-direction:column;gap:.4rem;">
<label style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">开始日期 (start)</label>
<label for="calc-start" style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">开始日期 (start)</label>
<input id="calc-start" type="date" style="background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;padding:.55rem .6rem;border-radius:8px;outline:none;font-size:.75rem;" />
</div>
<div style="display:flex;flex-direction:column;gap:.4rem;">
<label style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">结束日期 (end)</label>
<label for="calc-end" style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">结束日期 (end)</label>
<input id="calc-end" type="date" style="background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.2);color:#fff;padding:.55rem .6rem;border-radius:8px;outline:none;font-size:.75rem;" />
</div>
<div style="display:flex;flex-direction:column;gap:.4rem;">
<label style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">快捷范围</label>
<span style="font-size:.7rem;letter-spacing:.5px;opacity:.75;">快捷范围</span>
<div style="display:flex;flex-wrap:wrap;gap:.35rem;">
<button type="button" class="quick-range-btn" data-range="30">30天</button>
<button type="button" class="quick-range-btn" data-range="90">90天</button>
@ -357,5 +357,6 @@ if __name__ == '__main__':
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="app.js"></script>
<script src="code-review.js"></script>
</body>
</html>

@ -1,3 +1,4 @@
/* stylelint-disable no-duplicate-selectors, no-commented-out-code */
/* 基础样式重置 */
* {
margin: 0;
@ -126,6 +127,55 @@ body {
flex-wrap: wrap;
}
.code-language-select-wrapper {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 0.45rem 0.85rem;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 10px;
color: #ffffff;
min-width: 170px;
transition: border-color 0.2s ease, background 0.2s ease;
}
.code-language-select-wrapper:hover {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.35);
}
.code-language-select-wrapper i {
color: rgba(255, 255, 255, 0.7);
font-size: 0.95rem;
}
#code-language-select {
background: transparent;
border: none;
color: #ffffff;
font-size: 0.85rem;
font-weight: 600;
outline: none;
cursor: pointer;
appearance: none;
padding-right: 1.5rem;
}
.code-language-select-wrapper::after {
content: '\25BE';
position: absolute;
right: 0.75rem;
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.65);
pointer-events: none;
}
#code-language-select option {
color: #1f2937;
}
.btn-secondary {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
@ -1612,7 +1662,7 @@ body.project-mode-active .panel-ranking .table-wrap-scroll { max-height: 420px;
/* 模态窗口 (仓库详情) */
.modal {
position: fixed;
inset: 0; /* top:0; right:0; bottom:0; left:0 */
inset: 0;
display: none;
align-items: center;
justify-content: center;

@ -10,7 +10,7 @@
"lint": "eslint src/**/*.ts",
"start": "node dist/index.js",
"ab:repo-events": "ts-node scripts/ab_evaluate_repo_events.ts",
"start:api": "ts-node src/api/server.ts"
"start:api": "ts-node src/api/serverWithCodeQuality.ts"
},
"keywords": [
"openrank",

@ -0,0 +1,248 @@
import { promises as fs } from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import { spawn } from 'node:child_process';
export type SupportedLanguage = 'python' | 'c' | 'cpp';
export interface CodeQualityIssue {
line: number;
message: string;
type: 'error' | 'warning' | 'info';
rule?: string;
}
export interface CodeQualityResult {
language: SupportedLanguage;
tool: string;
score: number;
quality: 'excellent' | 'good' | 'average' | 'poor';
issues: CodeQualityIssue[];
totals: {
errors: number;
warnings: number;
infos: number;
};
runtimeMs: number;
meta: {
command: string;
exitCode: number;
};
}
export class CodeQualityError extends Error {
public readonly code: string;
public readonly status: number;
public readonly details?: Record<string, unknown>;
constructor(message: string, code: string, status = 500, details?: Record<string, unknown>) {
super(message);
this.name = 'CodeQualityError';
this.code = code;
this.status = status;
this.details = details;
}
}
interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
}
function runCommand(command: string, args: string[]): Promise<CommandResult> {
return new Promise((resolve, reject) => {
const child = spawn(command, args, { shell: false });
let stdout = '';
let stderr = '';
child.stdout.on('data', (chunk) => {
stdout += chunk.toString();
});
child.stderr.on('data', (chunk) => {
stderr += chunk.toString();
});
child.on('error', (err) => {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') {
reject(new CodeQualityError(`未找到命令 ${command},请确认已安装并添加到 PATH。`, 'tool_not_found', 400, { command }));
return;
}
reject(new CodeQualityError(`执行命令失败: ${err.message}`, 'command_failed', 500, { command }));
});
child.on('close', (code) => {
resolve({ stdout, stderr, exitCode: typeof code === 'number' ? code : 0 });
});
});
}
function computeScore(totals: { errors: number; warnings: number; infos: number }): number {
let score = 10;
score -= totals.errors * 3;
score -= totals.warnings * 1;
score -= totals.infos * 0.5;
score = Math.max(0, Math.min(10, score));
return Math.round(score * 10) / 10;
}
function mapScoreToQuality(score: number): 'excellent' | 'good' | 'average' | 'poor' {
if (score >= 9) return 'excellent';
if (score >= 7) return 'good';
if (score >= 5) return 'average';
return 'poor';
}
function normalizeLanguage(language: string): SupportedLanguage {
const lower = language.toLowerCase();
if (lower === 'python' || lower === 'py') return 'python';
if (lower === 'c' || lower === 'clang') return 'c';
if (lower === 'c++' || lower === 'cpp' || lower === 'cxx') return 'cpp';
throw new CodeQualityError(`暂不支持的语言: ${language}`, 'unsupported_language', 400);
}
async function evaluatePython(filePath: string): Promise<{ tool: string; issues: CodeQualityIssue[]; totals: CodeQualityResult['totals']; meta: { exitCode: number; command: string } }> {
const args = ['--output-format=json', '--score=yes', '--exit-zero', filePath];
const result = await runCommand('pylint', args);
let parsed: Array<{ type: string; message: string; line: number; symbol?: string; messageId?: string }> = [];
try {
parsed = JSON.parse(result.stdout || '[]');
} catch (err) {
const error = err as Error;
throw new CodeQualityError('无法解析 pylint 输出,请确认代码格式正确。', 'parse_failed', 500, {
stdout: result.stdout,
stderr: result.stderr,
cause: error.message,
});
}
const issues: CodeQualityIssue[] = parsed.map((item) => {
const severity = (() => {
switch (item.type) {
case 'fatal':
case 'error':
return 'error';
case 'warning':
case 'convention':
return 'warning';
default:
return 'info';
}
})();
return {
line: item.line || 0,
message: item.message,
type: severity,
rule: item.symbol || item.messageId,
};
});
const totals = issues.reduce((acc, issue) => {
if (issue.type === 'error') acc.errors += 1;
else if (issue.type === 'warning') acc.warnings += 1;
else acc.infos += 1;
return acc;
}, { errors: 0, warnings: 0, infos: 0 });
return {
tool: 'pylint',
issues,
totals,
meta: { exitCode: result.exitCode, command: `pylint ${args.join(' ')}` },
};
}
async function evaluateCpp(filePath: string, language: SupportedLanguage): Promise<{ tool: string; issues: CodeQualityIssue[]; totals: CodeQualityResult['totals']; meta: { exitCode: number; command: string } }> {
const template = '{file}|{line}|{severity}|{id}|{message}';
const args = ['--enable=style,warning,performance,portability', `--template=${template}`, '--inline-suppr'];
if (language === 'c') {
args.push('--language=c');
} else {
args.push('--language=c++');
}
args.push(filePath);
const result = await runCommand('cppcheck', args);
const combinedOutput = `${result.stdout}\n${result.stderr}`;
const lines = combinedOutput.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.includes('|'));
const issues: CodeQualityIssue[] = lines.map((line) => {
const [ , lineNumber, severity, rule, message ] = line.split('|');
let mappedSeverity: CodeQualityIssue['type'];
if (severity === 'error') mappedSeverity = 'error';
else if (severity === 'warning' || severity === 'style') mappedSeverity = 'warning';
else mappedSeverity = 'info';
return {
line: Number(lineNumber) || 0,
message: message?.trim() || '检测到潜在问题',
type: mappedSeverity,
rule: rule || undefined,
};
});
const totals = issues.reduce((acc, issue) => {
if (issue.type === 'error') acc.errors += 1;
else if (issue.type === 'warning') acc.warnings += 1;
else acc.infos += 1;
return acc;
}, { errors: 0, warnings: 0, infos: 0 });
return {
tool: 'cppcheck',
issues,
totals,
meta: { exitCode: result.exitCode, command: `cppcheck ${args.join(' ')}` },
};
}
export async function evaluateCodeQuality(params: { language: string; code: string }): Promise<CodeQualityResult> {
const language = normalizeLanguage(params.language);
const code = (params.code || '').trim();
if (!code) {
throw new CodeQualityError('代码内容不能为空。', 'empty_code', 400);
}
const tmpRoot = path.join(os.tmpdir(), 'openrank-code-quality');
await fs.mkdir(tmpRoot, { recursive: true });
const workingDir = path.join(tmpRoot, randomUUID());
await fs.mkdir(workingDir, { recursive: true });
let extension: string;
if (language === 'python') extension = 'py';
else if (language === 'c') extension = 'c';
else extension = 'cpp';
const filePath = path.join(workingDir, `snippet.${extension}`);
const headerComment = language === 'python'
? '# -*- coding: utf-8 -*-\n'
: '/* OpenRank 临时代码质量检测文件 */\n';
await fs.writeFile(filePath, `${headerComment}${code}`, 'utf8');
const start = Date.now();
try {
const analysis = language === 'python'
? await evaluatePython(filePath)
: await evaluateCpp(filePath, language);
const score = computeScore(analysis.totals);
const quality = mapScoreToQuality(score);
return {
language,
tool: analysis.tool,
score,
quality,
issues: analysis.issues,
totals: analysis.totals,
runtimeMs: Date.now() - start,
meta: analysis.meta,
};
} finally {
await fs.rm(workingDir, { recursive: true, force: true }).catch(() => undefined);
}
}

@ -0,0 +1,31 @@
import express, { Request, Response } from 'express';
import app from './server';
import { evaluateCodeQuality, CodeQualityError } from './codeQuality';
const router = express.Router();
router.post('/evaluate', async (req: Request, res: Response) => {
const { language, code } = req.body || {};
if (typeof language !== 'string' || !language.trim()) {
return res.status(400).json({ error: 'missing_language', message: '请提供需要检测的语言类型。' });
}
if (typeof code !== 'string' || !code.trim()) {
return res.status(400).json({ error: 'missing_code', message: '请提供需要检测的代码内容。' });
}
try {
const result = await evaluateCodeQuality({ language, code });
return res.json(result);
} catch (err) {
if (err instanceof CodeQualityError) {
return res.status(err.status).json({ error: err.code, message: err.message, details: err.details });
}
console.error('[code-evaluate] unexpected error', err);
const message = err instanceof Error ? err.message : '代码检测失败';
return res.status(500).json({ error: 'code_analysis_failed', message });
}
});
app.use('/api/code', router);

@ -19,7 +19,7 @@ if (process.env.DB_ENV_DEBUG === '1') {
const host = process.env.DB_HOST || '127.0.0.1';
const port = Number(process.env.DB_PORT || 3306);
const user = process.env.DB_USER || 'root';
const password = process.env.DB_PASS || '';
const password = process.env.DB_PASS || 'root';
const database = process.env.DB_NAME || 'openrank';
const poolMax = Number(process.env.DB_POOL_MAX || 10);

Loading…
Cancel
Save