@wangqq/q-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +23 -0
- package/src/commands/cr.ts +61 -0
- package/src/commands/dashboard.ts +78 -0
- package/src/commands/ra.ts +66 -0
- package/src/core/analyzer.ts +82 -0
- package/src/core/prompts.ts +80 -0
- package/src/core/storage.ts +59 -0
- package/src/dashboard/server.ts +88 -0
- package/src/dashboard/template.ts +241 -0
- package/src/plugin.ts +59 -0
- package/src/test.js +3 -0
- package/src/types/index.ts +51 -0
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wangqq/q-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "OpenCode intelligent agent for requirement risk assessment and code review",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"module": "src/plugin.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"clean": "rm -rf dist"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@opencode-ai/plugin": "^1.18.23"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/node": "^22.20.1",
|
|
17
|
+
"typescript": "^5.7.0"
|
|
18
|
+
},
|
|
19
|
+
"files": ["src"],
|
|
20
|
+
"opencode": {
|
|
21
|
+
"plugin": true
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { parseCRResult, CR_SYSTEM_PROMPT } from "../core/analyzer.js";
|
|
3
|
+
import type { CRReport } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
const severityIcon: Record<string, string> = {
|
|
6
|
+
info: "ℹ️",
|
|
7
|
+
warning: "⚠️",
|
|
8
|
+
error: "❌",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function formatCROutput(report: CRReport): string {
|
|
12
|
+
let output = `\n## 代码审查报告\n\n`;
|
|
13
|
+
output += `**评分**: ${report.score}/100\n`;
|
|
14
|
+
output += `**审查时间**: ${report.timestamp}\n`;
|
|
15
|
+
output += `**报告ID**: ${report.id}\n\n`;
|
|
16
|
+
|
|
17
|
+
if (report.summary) {
|
|
18
|
+
output += `### 摘要\n\n${report.summary}\n\n`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (report.issues.length > 0) {
|
|
22
|
+
output += `### 发现问题 (${report.issues.length})\n\n`;
|
|
23
|
+
for (const issue of report.issues) {
|
|
24
|
+
const loc = issue.file ? ` \`${issue.file}${issue.line ? `:${issue.line}` : ""}\`` : "";
|
|
25
|
+
output += `${severityIcon[issue.severity] || ""} **[${issue.severity.toUpperCase()}]** ${issue.title}${loc}\n`;
|
|
26
|
+
output += ` ${issue.description}\n`;
|
|
27
|
+
if (issue.suggestion) {
|
|
28
|
+
output += ` 💡 建议: ${issue.suggestion}\n`;
|
|
29
|
+
}
|
|
30
|
+
output += `\n`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
output += `---\n报告已保存至 \`.q/cr/${report.id}.json\`\n`;
|
|
35
|
+
return output;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const qCrTool = tool({
|
|
39
|
+
description: "代码审查 - 对代码进行质量、安全、性能等多维度审查,将结果保存到 .q/cr/ 目录",
|
|
40
|
+
args: {
|
|
41
|
+
code: tool.schema.string().describe("待审查的代码内容"),
|
|
42
|
+
language: tool.schema.string().optional().describe("编程语言(可选,用于更精准的审查)"),
|
|
43
|
+
file_path: tool.schema.string().optional().describe("文件路径(可选,用于定位问题)"),
|
|
44
|
+
llm_output: tool.schema.string().optional().describe("LLM 已生成的审查结果(可选,若提供则直接解析保存)"),
|
|
45
|
+
},
|
|
46
|
+
async execute(args, _ctx) {
|
|
47
|
+
const llmOutput = args.llm_output || "";
|
|
48
|
+
const report = parseCRResult(llmOutput || args.code, args.code);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
title: `代码审查 - ${report.score}/100`,
|
|
52
|
+
output: formatCROutput(report),
|
|
53
|
+
metadata: {
|
|
54
|
+
reportId: report.id,
|
|
55
|
+
score: report.score,
|
|
56
|
+
issueCount: report.issues.length,
|
|
57
|
+
type: "cr",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { startDashboardServer, stopDashboardServer, getActiveServer } from "../dashboard/server.js";
|
|
3
|
+
|
|
4
|
+
export const qDashboardTool = tool({
|
|
5
|
+
description: "启动 Q Dashboard 本地服务器,可视化展示 .q/ 目录下的历史分析结果(RA 和 CR 报告)",
|
|
6
|
+
args: {
|
|
7
|
+
port: tool.schema.number().optional().describe("服务器端口号(默认 3210)"),
|
|
8
|
+
host: tool.schema.string().optional().describe("服务器主机地址(默认 localhost)"),
|
|
9
|
+
action: tool.schema.enum(["start", "stop", "restart"]).optional().describe("操作类型:start/stop/restart(默认 start)"),
|
|
10
|
+
},
|
|
11
|
+
async execute(args, _ctx) {
|
|
12
|
+
const action = args.action || "start";
|
|
13
|
+
const port = args.port || 3210;
|
|
14
|
+
const host = args.host || "localhost";
|
|
15
|
+
|
|
16
|
+
if (action === "stop") {
|
|
17
|
+
const server = getActiveServer();
|
|
18
|
+
if (server) {
|
|
19
|
+
await stopDashboardServer(server);
|
|
20
|
+
return {
|
|
21
|
+
title: "Q Dashboard 已停止",
|
|
22
|
+
output: "Dashboard 服务器已停止",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
title: "Q Dashboard",
|
|
27
|
+
output: "没有正在运行的 Dashboard 服务器",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (action === "restart") {
|
|
32
|
+
const server = getActiveServer();
|
|
33
|
+
if (server) {
|
|
34
|
+
await stopDashboardServer(server);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (action === "start" || action === "restart") {
|
|
39
|
+
const existing = getActiveServer();
|
|
40
|
+
if (existing && action === "start") {
|
|
41
|
+
return {
|
|
42
|
+
title: "Q Dashboard 已在运行",
|
|
43
|
+
output: "Dashboard 服务器已在运行,使用 action: restart 重启,或 action: stop 停止",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await startDashboardServer(port, host);
|
|
49
|
+
const url = `http://${host}:${port}`;
|
|
50
|
+
return {
|
|
51
|
+
title: `Q Dashboard 已启动 - ${url}`,
|
|
52
|
+
output: [
|
|
53
|
+
`## Q Dashboard\n`,
|
|
54
|
+
`服务器已启动: ${url}\n`,
|
|
55
|
+
`- 首页: ${url}/`,
|
|
56
|
+
`- RA 报告 API: ${url}/api/ra`,
|
|
57
|
+
`- CR 报告 API: ${url}/api/cr\n`,
|
|
58
|
+
`使用 q_dashboard(action: "stop") 停止服务器`,
|
|
59
|
+
].join("\n"),
|
|
60
|
+
metadata: { url, port, host },
|
|
61
|
+
};
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
if (err.code === "EADDRINUSE") {
|
|
64
|
+
return {
|
|
65
|
+
title: "启动失败",
|
|
66
|
+
output: `端口 ${port} 已被占用,请使用其他端口`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
title: "启动失败",
|
|
71
|
+
output: `启动服务器失败: ${err.message}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { output: "未知操作" };
|
|
77
|
+
},
|
|
78
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { parseRAResult, RA_SYSTEM_PROMPT } from "../core/analyzer.js";
|
|
3
|
+
import type { RAReport } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
const riskEmoji: Record<string, string> = {
|
|
6
|
+
low: "🟢",
|
|
7
|
+
medium: "🟡",
|
|
8
|
+
high: "🟠",
|
|
9
|
+
critical: "🔴",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function formatRAOutput(report: RAReport): string {
|
|
13
|
+
let output = `\n## 需求风险评估报告\n\n`;
|
|
14
|
+
output += `**风险等级**: ${riskEmoji[report.riskLevel] || ""} ${report.riskLevel.toUpperCase()}\n`;
|
|
15
|
+
output += `**评估时间**: ${report.timestamp}\n`;
|
|
16
|
+
output += `**报告ID**: ${report.id}\n\n`;
|
|
17
|
+
|
|
18
|
+
if (report.categories.length > 0) {
|
|
19
|
+
output += `### 各维度评估\n\n`;
|
|
20
|
+
for (const cat of report.categories) {
|
|
21
|
+
output += `- **${cat.name}** ${riskEmoji[cat.riskLevel] || ""} ${cat.riskLevel}\n`;
|
|
22
|
+
output += ` ${cat.description}\n`;
|
|
23
|
+
if (cat.suggestions.length > 0) {
|
|
24
|
+
output += ` 建议:\n`;
|
|
25
|
+
for (const s of cat.suggestions) {
|
|
26
|
+
output += ` - ${s}\n`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
output += `\n`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (report.output) {
|
|
34
|
+
output += `### 摘要\n\n${report.output}\n`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
output += `\n---\n报告已保存至 \`.q/ra/${report.id}.json\`\n`;
|
|
38
|
+
return output;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const qRaTool = tool({
|
|
42
|
+
description: "需求风险评估 - 分析需求的技术可行性、安全、兼容性等风险维度,将结果保存到 .q/ra/ 目录",
|
|
43
|
+
args: {
|
|
44
|
+
requirement: tool.schema.string().describe("需求描述文本"),
|
|
45
|
+
context: tool.schema.string().optional().describe("附加上下文信息(可选)"),
|
|
46
|
+
llm_output: tool.schema.string().optional().describe("LLM 已生成的分析结果(可选,若提供则直接解析保存)"),
|
|
47
|
+
},
|
|
48
|
+
async execute(args, _ctx) {
|
|
49
|
+
const llmOutput = args.llm_output || "";
|
|
50
|
+
const requirementText = args.context
|
|
51
|
+
? `${args.requirement}\n\n附加上下文:\n${args.context}`
|
|
52
|
+
: args.requirement;
|
|
53
|
+
|
|
54
|
+
const report = parseRAResult(llmOutput || requirementText, requirementText);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
title: `需求风险评估 - ${report.riskLevel.toUpperCase()}`,
|
|
58
|
+
output: formatRAOutput(report),
|
|
59
|
+
metadata: {
|
|
60
|
+
reportId: report.id,
|
|
61
|
+
riskLevel: report.riskLevel,
|
|
62
|
+
type: "ra",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { saveResult, generateId } from "./storage.js";
|
|
2
|
+
import { RA_SYSTEM_PROMPT, CR_SYSTEM_PROMPT } from "./prompts.js";
|
|
3
|
+
import type { RAReport, CRReport, RACategory, CRIssue } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
function extractJSON(text: string): string {
|
|
6
|
+
const codeBlockMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
7
|
+
if (codeBlockMatch) {
|
|
8
|
+
return codeBlockMatch[1].trim();
|
|
9
|
+
}
|
|
10
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
11
|
+
if (jsonMatch) {
|
|
12
|
+
return jsonMatch[0];
|
|
13
|
+
}
|
|
14
|
+
return text;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function parseRAResult(llmOutput: string, requirement: string): RAReport {
|
|
18
|
+
const id = generateId();
|
|
19
|
+
const timestamp = new Date().toISOString();
|
|
20
|
+
|
|
21
|
+
let riskLevel: RAReport["riskLevel"] = "medium";
|
|
22
|
+
let categories: RACategory[] = [];
|
|
23
|
+
let summary = "";
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(extractJSON(llmOutput));
|
|
27
|
+
riskLevel = parsed.riskLevel || "medium";
|
|
28
|
+
categories = parsed.categories || [];
|
|
29
|
+
summary = parsed.summary || "";
|
|
30
|
+
} catch {
|
|
31
|
+
summary = llmOutput;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const report: RAReport = {
|
|
35
|
+
id,
|
|
36
|
+
type: "ra",
|
|
37
|
+
timestamp,
|
|
38
|
+
input: requirement,
|
|
39
|
+
output: summary,
|
|
40
|
+
metadata: {},
|
|
41
|
+
riskLevel,
|
|
42
|
+
categories,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
saveResult(report);
|
|
46
|
+
return report;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseCRResult(llmOutput: string, code: string): CRReport {
|
|
50
|
+
const id = generateId();
|
|
51
|
+
const timestamp = new Date().toISOString();
|
|
52
|
+
|
|
53
|
+
let score = 0;
|
|
54
|
+
let summary = "";
|
|
55
|
+
let issues: CRIssue[] = [];
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(extractJSON(llmOutput));
|
|
59
|
+
score = parsed.score || 0;
|
|
60
|
+
summary = parsed.summary || "";
|
|
61
|
+
issues = parsed.issues || [];
|
|
62
|
+
} catch {
|
|
63
|
+
summary = llmOutput;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const report: CRReport = {
|
|
67
|
+
id,
|
|
68
|
+
type: "cr",
|
|
69
|
+
timestamp,
|
|
70
|
+
input: code,
|
|
71
|
+
output: summary,
|
|
72
|
+
metadata: {},
|
|
73
|
+
summary,
|
|
74
|
+
issues,
|
|
75
|
+
score,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
saveResult(report);
|
|
79
|
+
return report;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { RA_SYSTEM_PROMPT, CR_SYSTEM_PROMPT };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export const RA_SYSTEM_PROMPT = `你是一位资深的需求风险评估专家。你的任务是分析给定的需求描述,从以下维度进行全面风险评估:
|
|
2
|
+
|
|
3
|
+
## 评估维度
|
|
4
|
+
|
|
5
|
+
1. **技术可行性风险** - 需求在技术上是否可行,是否存在技术难点或依赖风险
|
|
6
|
+
2. **范围蔓延风险** - 需求边界是否清晰,是否容易扩展超出原始范围
|
|
7
|
+
3. **兼容性风险** - 是否与现有系统、API、数据结构兼容
|
|
8
|
+
4. **性能风险** - 实现后是否可能影响系统性能、响应时间、资源消耗
|
|
9
|
+
5. **安全风险** - 是否涉及敏感数据、权限变更、潜在安全漏洞
|
|
10
|
+
6. **时间进度风险** - 实现复杂度评估,是否可能延期
|
|
11
|
+
7. **依赖风险** - 是否依赖外部服务、第三方库、其他团队交付
|
|
12
|
+
|
|
13
|
+
## 输出格式
|
|
14
|
+
|
|
15
|
+
请以 JSON 格式输出评估结果,结构如下:
|
|
16
|
+
|
|
17
|
+
\`\`\`json
|
|
18
|
+
{
|
|
19
|
+
"riskLevel": "low|medium|high|critical",
|
|
20
|
+
"categories": [
|
|
21
|
+
{
|
|
22
|
+
"name": "维度名称",
|
|
23
|
+
"riskLevel": "low|medium|high|critical",
|
|
24
|
+
"description": "风险描述",
|
|
25
|
+
"suggestions": ["缓解建议1", "缓解建议2"]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"summary": "整体风险评估摘要"
|
|
29
|
+
}
|
|
30
|
+
\`\`\`
|
|
31
|
+
|
|
32
|
+
请确保分析全面、客观,给出具体可操作的缓解建议。`;
|
|
33
|
+
|
|
34
|
+
export const CR_SYSTEM_PROMPT = `你是一位资深的代码审查专家。你的任务是对给定的代码进行专业审查,从以下维度进行全面分析:
|
|
35
|
+
|
|
36
|
+
## 审查维度
|
|
37
|
+
|
|
38
|
+
1. **代码质量** - 代码可读性、命名规范、结构清晰度
|
|
39
|
+
2. **潜在缺陷** - 空指针、边界条件、并发问题、资源泄漏
|
|
40
|
+
3. **安全漏洞** - 注入攻击、XSS、敏感数据暴露、权限绕过
|
|
41
|
+
4. **性能问题** - 不必要的计算、内存分配、N+1查询、缓存缺失
|
|
42
|
+
5. **最佳实践** - 设计模式、SOLID原则、DRY原则、错误处理
|
|
43
|
+
6. **可维护性** - 耦合度、测试覆盖、文档完整性
|
|
44
|
+
7. **架构一致性** - 与项目架构风格、分层规范的一致性
|
|
45
|
+
|
|
46
|
+
## 输出格式
|
|
47
|
+
|
|
48
|
+
请以 JSON 格式输出审查结果,结构如下:
|
|
49
|
+
|
|
50
|
+
\`\`\`json
|
|
51
|
+
{
|
|
52
|
+
"score": 85,
|
|
53
|
+
"summary": "代码审查整体摘要",
|
|
54
|
+
"issues": [
|
|
55
|
+
{
|
|
56
|
+
"severity": "info|warning|error",
|
|
57
|
+
"file": "文件路径",
|
|
58
|
+
"line": 行号,
|
|
59
|
+
"title": "问题标题",
|
|
60
|
+
"description": "问题描述",
|
|
61
|
+
"suggestion": "修复建议"
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
\`\`\`
|
|
66
|
+
|
|
67
|
+
评分范围 0-100,90+ 为优秀,70-89 为良好,50-69 为需改进,50以下为严重问题。
|
|
68
|
+
请确保审查专业、客观,给出具体可操作的修复建议。`;
|
|
69
|
+
|
|
70
|
+
export const RA_COMMAND_TEMPLATE = `请对以下需求进行风险评估,使用 q_ra 工具完成分析并将结果保存:
|
|
71
|
+
|
|
72
|
+
{{args}}
|
|
73
|
+
|
|
74
|
+
请调用 q_ra 工具,将上述需求作为 requirement 参数传入。`;
|
|
75
|
+
|
|
76
|
+
export const CR_COMMAND_TEMPLATE = `请对以下代码进行审查,使用 q_cr 工具完成分析并将结果保存:
|
|
77
|
+
|
|
78
|
+
{{args}}
|
|
79
|
+
|
|
80
|
+
请调用 q_cr 工具,将上述代码作为 code 参数传入。`;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type { AnalysisResult, AnalysisType } from "../types/index.js";
|
|
4
|
+
|
|
5
|
+
function ensureDir(dir: string): void {
|
|
6
|
+
if (!existsSync(dir)) {
|
|
7
|
+
mkdirSync(dir, { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function getQBaseDir(): string {
|
|
12
|
+
const baseDir = join(process.cwd(), ".q");
|
|
13
|
+
ensureDir(baseDir);
|
|
14
|
+
return baseDir;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function getDirForType(type: AnalysisType): string {
|
|
18
|
+
const dir = join(getQBaseDir(), type);
|
|
19
|
+
ensureDir(dir);
|
|
20
|
+
return dir;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function saveResult<T extends AnalysisResult>(result: T): string {
|
|
24
|
+
const dir = getDirForType(result.type);
|
|
25
|
+
const filePath = join(dir, `${result.id}.json`);
|
|
26
|
+
writeFileSync(filePath, JSON.stringify(result, null, 2), "utf-8");
|
|
27
|
+
return filePath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function loadResult<T extends AnalysisResult>(type: AnalysisType, id: string): T | null {
|
|
31
|
+
const dir = getDirForType(type);
|
|
32
|
+
const filePath = join(dir, `${id}.json`);
|
|
33
|
+
if (!existsSync(filePath)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return JSON.parse(readFileSync(filePath, "utf-8")) as T;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function listResults<T extends AnalysisResult>(type: AnalysisType): T[] {
|
|
40
|
+
const dir = getDirForType(type);
|
|
41
|
+
if (!existsSync(dir)) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return readdirSync(dir)
|
|
45
|
+
.filter((f: string) => f.endsWith(".json"))
|
|
46
|
+
.sort()
|
|
47
|
+
.reverse()
|
|
48
|
+
.map((f: string) => JSON.parse(readFileSync(join(dir, f), "utf-8")) as T);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function generateId(): string {
|
|
52
|
+
const d = new Date();
|
|
53
|
+
const p = (n: number) => n.toString().padStart(2, "0");
|
|
54
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function getStorageBaseDir(): string {
|
|
58
|
+
return getQBaseDir();
|
|
59
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { createServer, type Server, type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import { listResults, loadResult } from "../core/storage.js";
|
|
3
|
+
import { renderDashboard } from "./template.js";
|
|
4
|
+
import type { RAReport, CRReport } from "../types/index.js";
|
|
5
|
+
|
|
6
|
+
let activeServer: Server | null = null;
|
|
7
|
+
|
|
8
|
+
export function getActiveServer(): Server | null {
|
|
9
|
+
return activeServer;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseUrl(req: IncomingMessage): { pathname: string } {
|
|
13
|
+
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
14
|
+
return { pathname: url.pathname };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sendJson(res: ServerResponse, data: unknown, status = 200): void {
|
|
18
|
+
res.writeHead(status, {
|
|
19
|
+
"Content-Type": "application/json",
|
|
20
|
+
"Access-Control-Allow-Origin": "*",
|
|
21
|
+
});
|
|
22
|
+
res.end(JSON.stringify(data));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function sendHtml(res: ServerResponse, html: string): void {
|
|
26
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
27
|
+
res.end(html);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sendNotFound(res: ServerResponse): void {
|
|
31
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
32
|
+
res.end(JSON.stringify({ error: "Not found" }));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function startDashboardServer(port = 3210, host = "localhost"): Promise<Server> {
|
|
36
|
+
const server = createServer((req, res) => {
|
|
37
|
+
const { pathname } = parseUrl(req);
|
|
38
|
+
|
|
39
|
+
if (pathname === "/" || pathname === "/dashboard") {
|
|
40
|
+
const raReports = listResults<RAReport>("ra");
|
|
41
|
+
const crReports = listResults<CRReport>("cr");
|
|
42
|
+
sendHtml(res, renderDashboard(raReports, crReports));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (pathname === "/api/ra") {
|
|
47
|
+
sendJson(res, listResults<RAReport>("ra"));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (pathname === "/api/cr") {
|
|
52
|
+
sendJson(res, listResults<CRReport>("cr"));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const raMatch = pathname.match(/^\/api\/ra\/(\d+)$/);
|
|
57
|
+
if (raMatch) {
|
|
58
|
+
const report = loadResult<RAReport>("ra", raMatch[1]);
|
|
59
|
+
return report ? sendJson(res, report) : sendNotFound(res);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const crMatch = pathname.match(/^\/api\/cr\/(\d+)$/);
|
|
63
|
+
if (crMatch) {
|
|
64
|
+
const report = loadResult<CRReport>("cr", crMatch[1]);
|
|
65
|
+
return report ? sendJson(res, report) : sendNotFound(res);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
sendNotFound(res);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
server.on("error", reject);
|
|
73
|
+
server.listen(port, host, () => {
|
|
74
|
+
activeServer = server;
|
|
75
|
+
resolve(server);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function stopDashboardServer(server: Server): Promise<void> {
|
|
81
|
+
activeServer = null;
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
server.close((err) => {
|
|
84
|
+
if (err) reject(err);
|
|
85
|
+
else resolve();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type { AnalysisResult, RAReport, CRReport } from "../types/index.js";
|
|
2
|
+
|
|
3
|
+
function riskBadge(level: string): string {
|
|
4
|
+
const colors: Record<string, string> = {
|
|
5
|
+
low: "#22c55e",
|
|
6
|
+
medium: "#eab308",
|
|
7
|
+
high: "#f97316",
|
|
8
|
+
critical: "#ef4444",
|
|
9
|
+
};
|
|
10
|
+
const color = colors[level] || "#6b7280";
|
|
11
|
+
return `<span style="display:inline-block;padding:2px 10px;border-radius:12px;color:#fff;background:${color};font-size:12px;font-weight:600;">${level.toUpperCase()}</span>`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function severityBadge(severity: string): string {
|
|
15
|
+
const colors: Record<string, string> = {
|
|
16
|
+
info: "#3b82f6",
|
|
17
|
+
warning: "#eab308",
|
|
18
|
+
error: "#ef4444",
|
|
19
|
+
};
|
|
20
|
+
const color = colors[severity] || "#6b7280";
|
|
21
|
+
return `<span style="display:inline-block;padding:2px 10px;border-radius:12px;color:#fff;background:${color};font-size:12px;font-weight:600;">${severity.toUpperCase()}</span>`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function scoreColor(score: number): string {
|
|
25
|
+
if (score >= 90) return "#22c55e";
|
|
26
|
+
if (score >= 70) return "#eab308";
|
|
27
|
+
if (score >= 50) return "#f97316";
|
|
28
|
+
return "#ef4444";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function formatRAReport(report: RAReport): string {
|
|
32
|
+
let html = `
|
|
33
|
+
<div class="report-card ra-card">
|
|
34
|
+
<div class="report-header">
|
|
35
|
+
<span class="report-type">RA</span>
|
|
36
|
+
${riskBadge(report.riskLevel)}
|
|
37
|
+
<span class="report-time">${report.timestamp}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="report-body">
|
|
40
|
+
<h4>需求输入</h4>
|
|
41
|
+
<pre class="report-input">${escapeHtml(report.input)}</pre>`;
|
|
42
|
+
|
|
43
|
+
if (report.categories.length > 0) {
|
|
44
|
+
html += `<h4>风险评估维度</h4><div class="categories">`;
|
|
45
|
+
for (const cat of report.categories) {
|
|
46
|
+
html += `
|
|
47
|
+
<div class="category">
|
|
48
|
+
<div class="category-header">${escapeHtml(cat.name)} ${riskBadge(cat.riskLevel)}</div>
|
|
49
|
+
<p>${escapeHtml(cat.description)}</p>
|
|
50
|
+
${cat.suggestions.length > 0 ? `<ul>${cat.suggestions.map((s) => `<li>${escapeHtml(s)}</li>`).join("")}</ul>` : ""}
|
|
51
|
+
</div>`;
|
|
52
|
+
}
|
|
53
|
+
html += `</div>`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
html += `
|
|
57
|
+
</div>
|
|
58
|
+
<div class="report-footer">
|
|
59
|
+
<span class="report-id">ID: ${report.id}</span>
|
|
60
|
+
<a href="/api/ra/${report.id}" class="download-link">下载 JSON</a>
|
|
61
|
+
</div>
|
|
62
|
+
</div>`;
|
|
63
|
+
return html;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatCRReport(report: CRReport): string {
|
|
67
|
+
let html = `
|
|
68
|
+
<div class="report-card cr-card">
|
|
69
|
+
<div class="report-header">
|
|
70
|
+
<span class="report-type">CR</span>
|
|
71
|
+
<span class="score" style="color:${scoreColor(report.score)}">${report.score}/100</span>
|
|
72
|
+
<span class="report-time">${report.timestamp}</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="report-body">
|
|
75
|
+
<h4>审查摘要</h4>
|
|
76
|
+
<p>${escapeHtml(report.summary)}</p>`;
|
|
77
|
+
|
|
78
|
+
if (report.issues.length > 0) {
|
|
79
|
+
html += `<h4>问题列表 (${report.issues.length})</h4><div class="issues">`;
|
|
80
|
+
for (const issue of report.issues) {
|
|
81
|
+
const loc = issue.file ? ` <code>${escapeHtml(issue.file)}${issue.line ? `:${issue.line}` : ""}</code>` : "";
|
|
82
|
+
html += `
|
|
83
|
+
<div class="issue">
|
|
84
|
+
<div class="issue-header">${severityBadge(issue.severity)} ${escapeHtml(issue.title)}${loc}</div>
|
|
85
|
+
<p>${escapeHtml(issue.description)}</p>
|
|
86
|
+
${issue.suggestion ? `<p class="suggestion">💡 ${escapeHtml(issue.suggestion)}</p>` : ""}
|
|
87
|
+
</div>`;
|
|
88
|
+
}
|
|
89
|
+
html += `</div>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
html += `
|
|
93
|
+
</div>
|
|
94
|
+
<div class="report-footer">
|
|
95
|
+
<span class="report-id">ID: ${report.id}</span>
|
|
96
|
+
<a href="/api/cr/${report.id}" class="download-link">下载 JSON</a>
|
|
97
|
+
</div>
|
|
98
|
+
</div>`;
|
|
99
|
+
return html;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function escapeHtml(text: string): string {
|
|
103
|
+
return text
|
|
104
|
+
.replace(/&/g, "&")
|
|
105
|
+
.replace(/</g, "<")
|
|
106
|
+
.replace(/>/g, ">")
|
|
107
|
+
.replace(/"/g, """)
|
|
108
|
+
.replace(/'/g, "'");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function renderDashboard(raReports: RAReport[], crReports: CRReport[]): string {
|
|
112
|
+
const raCards = raReports.map(formatRAReport).join("\n");
|
|
113
|
+
const crCards = crReports.map(formatCRReport).join("\n");
|
|
114
|
+
|
|
115
|
+
return `<!DOCTYPE html>
|
|
116
|
+
<html lang="zh-CN">
|
|
117
|
+
<head>
|
|
118
|
+
<meta charset="UTF-8">
|
|
119
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
120
|
+
<title>Q Agent Dashboard</title>
|
|
121
|
+
<style>
|
|
122
|
+
:root {
|
|
123
|
+
--bg: #0f172a;
|
|
124
|
+
--card-bg: #1e293b;
|
|
125
|
+
--text: #e2e8f0;
|
|
126
|
+
--text-muted: #94a3b8;
|
|
127
|
+
--border: #334155;
|
|
128
|
+
--accent: #3b82f6;
|
|
129
|
+
}
|
|
130
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
131
|
+
body {
|
|
132
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
133
|
+
background: var(--bg);
|
|
134
|
+
color: var(--text);
|
|
135
|
+
line-height: 1.6;
|
|
136
|
+
}
|
|
137
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
138
|
+
header {
|
|
139
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
140
|
+
padding: 20px 0; border-bottom: 1px solid var(--border); margin-bottom: 30px;
|
|
141
|
+
}
|
|
142
|
+
h1 { font-size: 24px; font-weight: 700; }
|
|
143
|
+
h1 span { color: var(--accent); }
|
|
144
|
+
.stats { display: flex; gap: 20px; }
|
|
145
|
+
.stat { text-align: center; }
|
|
146
|
+
.stat-value { font-size: 28px; font-weight: 700; color: var(--accent); }
|
|
147
|
+
.stat-label { font-size: 12px; color: var(--text-muted); text-transform: uppercase; }
|
|
148
|
+
.tabs { display: flex; gap: 8px; margin-bottom: 24px; }
|
|
149
|
+
.tab {
|
|
150
|
+
padding: 8px 20px; border-radius: 8px; cursor: pointer;
|
|
151
|
+
background: var(--card-bg); border: 1px solid var(--border);
|
|
152
|
+
color: var(--text-muted); font-size: 14px; font-weight: 500;
|
|
153
|
+
transition: all 0.2s;
|
|
154
|
+
}
|
|
155
|
+
.tab:hover { border-color: var(--accent); color: var(--text); }
|
|
156
|
+
.tab.active { background: var(--accent); color: #fff; border-color: var(--accent); }
|
|
157
|
+
.tab-content { display: none; }
|
|
158
|
+
.tab-content.active { display: block; }
|
|
159
|
+
.report-card {
|
|
160
|
+
background: var(--card-bg); border: 1px solid var(--border);
|
|
161
|
+
border-radius: 12px; padding: 20px; margin-bottom: 16px;
|
|
162
|
+
}
|
|
163
|
+
.report-header {
|
|
164
|
+
display: flex; align-items: center; gap: 12px;
|
|
165
|
+
margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border);
|
|
166
|
+
}
|
|
167
|
+
.report-type {
|
|
168
|
+
display: inline-block; padding: 2px 10px; border-radius: 6px;
|
|
169
|
+
font-size: 12px; font-weight: 700; color: #fff;
|
|
170
|
+
}
|
|
171
|
+
.ra-card .report-type { background: #8b5cf6; }
|
|
172
|
+
.cr-card .report-type { background: #06b6d4; }
|
|
173
|
+
.report-time { color: var(--text-muted); font-size: 13px; margin-left: auto; }
|
|
174
|
+
.score { font-size: 20px; font-weight: 700; }
|
|
175
|
+
.report-body h4 { color: var(--text); margin: 12px 0 8px; font-size: 14px; }
|
|
176
|
+
.report-input {
|
|
177
|
+
background: var(--bg); padding: 12px; border-radius: 8px;
|
|
178
|
+
font-size: 13px; overflow-x: auto; white-space: pre-wrap; max-height: 200px; overflow-y: auto;
|
|
179
|
+
}
|
|
180
|
+
.categories, .issues { display: flex; flex-direction: column; gap: 12px; }
|
|
181
|
+
.category, .issue {
|
|
182
|
+
background: var(--bg); padding: 12px; border-radius: 8px;
|
|
183
|
+
}
|
|
184
|
+
.category-header, .issue-header { font-weight: 600; margin-bottom: 4px; }
|
|
185
|
+
.category ul { padding-left: 20px; margin-top: 8px; }
|
|
186
|
+
.category li { font-size: 13px; color: var(--text-muted); }
|
|
187
|
+
.suggestion { color: #22c55e; font-size: 13px; }
|
|
188
|
+
.report-footer {
|
|
189
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
190
|
+
margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);
|
|
191
|
+
}
|
|
192
|
+
.report-id { color: var(--text-muted); font-size: 12px; font-family: monospace; }
|
|
193
|
+
.download-link { color: var(--accent); font-size: 13px; text-decoration: none; }
|
|
194
|
+
.download-link:hover { text-decoration: underline; }
|
|
195
|
+
.empty { text-align: center; color: var(--text-muted); padding: 40px; }
|
|
196
|
+
code { background: var(--bg); padding: 1px 6px; border-radius: 4px; font-size: 13px; }
|
|
197
|
+
</style>
|
|
198
|
+
</head>
|
|
199
|
+
<body>
|
|
200
|
+
<div class="container">
|
|
201
|
+
<header>
|
|
202
|
+
<h1><span>Q</span> Agent Dashboard</h1>
|
|
203
|
+
<div class="stats">
|
|
204
|
+
<div class="stat">
|
|
205
|
+
<div class="stat-value">${raReports.length}</div>
|
|
206
|
+
<div class="stat-label">RA Reports</div>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="stat">
|
|
209
|
+
<div class="stat-value">${crReports.length}</div>
|
|
210
|
+
<div class="stat-label">CR Reports</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
</header>
|
|
214
|
+
<div class="tabs">
|
|
215
|
+
<div class="tab active" data-tab="all">All</div>
|
|
216
|
+
<div class="tab" data-tab="ra">Risk Assessment</div>
|
|
217
|
+
<div class="tab" data-tab="cr">Code Review</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="tab-content active" id="tab-all">
|
|
220
|
+
${raCards || crCards ? raCards + crCards : '<div class="empty">暂无分析结果</div>'}
|
|
221
|
+
</div>
|
|
222
|
+
<div class="tab-content" id="tab-ra">
|
|
223
|
+
${raCards || '<div class="empty">暂无风险评估报告</div>'}
|
|
224
|
+
</div>
|
|
225
|
+
<div class="tab-content" id="tab-cr">
|
|
226
|
+
${crCards || '<div class="empty">暂无代码审查报告</div>'}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
<script>
|
|
230
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
231
|
+
tab.addEventListener('click', () => {
|
|
232
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
233
|
+
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
234
|
+
tab.classList.add('active');
|
|
235
|
+
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
</script>
|
|
239
|
+
</body>
|
|
240
|
+
</html>`;
|
|
241
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Plugin, PluginInput, Hooks } from "@opencode-ai/plugin";
|
|
2
|
+
import { define, type PluginContext } from "@opencode-ai/plugin/v2/promise";
|
|
3
|
+
import { qRaTool } from "./commands/ra.js";
|
|
4
|
+
import { qCrTool } from "./commands/cr.js";
|
|
5
|
+
import { qDashboardTool } from "./commands/dashboard.js";
|
|
6
|
+
import { RA_COMMAND_TEMPLATE, CR_COMMAND_TEMPLATE } from "./core/prompts.js";
|
|
7
|
+
|
|
8
|
+
const QAgentPlugin: Plugin = async (_input: PluginInput): Promise<Hooks> => {
|
|
9
|
+
return {
|
|
10
|
+
tool: {
|
|
11
|
+
q_ra: qRaTool,
|
|
12
|
+
q_cr: qCrTool,
|
|
13
|
+
q_dashboard: qDashboardTool,
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
17
|
+
output.system.push(
|
|
18
|
+
"你已加载 Q Agent 插件,可以使用以下工具:",
|
|
19
|
+
"- q_ra: 需求风险评估,分析需求的技术可行性、安全、兼容性等风险",
|
|
20
|
+
"- q_cr: 代码审查,对代码进行质量、安全、性能等多维度审查",
|
|
21
|
+
"- q_dashboard: 启动/停止 Dashboard 可视化服务器",
|
|
22
|
+
);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const QAgentV2Plugin = define({
|
|
28
|
+
id: "q-agent",
|
|
29
|
+
async setup(context: PluginContext) {
|
|
30
|
+
await context.command.transform((draft) => {
|
|
31
|
+
if (!draft.get("q-ra")) {
|
|
32
|
+
draft.update("q-ra", (cmd) => {
|
|
33
|
+
cmd.name = "q-ra";
|
|
34
|
+
cmd.template = RA_COMMAND_TEMPLATE;
|
|
35
|
+
cmd.description = "需求风险评估 - 分析需求的技术可行性、安全、兼容性等风险";
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!draft.get("q-cr")) {
|
|
40
|
+
draft.update("q-cr", (cmd) => {
|
|
41
|
+
cmd.name = "q-cr";
|
|
42
|
+
cmd.template = CR_COMMAND_TEMPLATE;
|
|
43
|
+
cmd.description = "代码审查 - 对代码进行质量、安全、性能等多维度审查";
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!draft.get("q-dashboard")) {
|
|
48
|
+
draft.update("q-dashboard", (cmd) => {
|
|
49
|
+
cmd.name = "q-dashboard";
|
|
50
|
+
cmd.template = "请使用 q_dashboard 工具启动 Dashboard 可视化服务器,展示历史 RA 和 CR 分析结果。";
|
|
51
|
+
cmd.description = "启动 Q Dashboard 可视化面板,展示历史分析结果";
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
export default QAgentPlugin;
|
|
59
|
+
export { QAgentPlugin };
|
package/src/test.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type AnalysisType = "ra" | "cr";
|
|
2
|
+
|
|
3
|
+
export interface AnalysisResult {
|
|
4
|
+
id: string;
|
|
5
|
+
type: AnalysisType;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
input: string;
|
|
8
|
+
output: string;
|
|
9
|
+
metadata: {
|
|
10
|
+
model?: string;
|
|
11
|
+
duration?: number;
|
|
12
|
+
tokenUsage?: {
|
|
13
|
+
input: number;
|
|
14
|
+
output: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface RAReport extends AnalysisResult {
|
|
20
|
+
type: "ra";
|
|
21
|
+
riskLevel: "low" | "medium" | "high" | "critical";
|
|
22
|
+
categories: RACategory[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RACategory {
|
|
26
|
+
name: string;
|
|
27
|
+
riskLevel: "low" | "medium" | "high" | "critical";
|
|
28
|
+
description: string;
|
|
29
|
+
suggestions: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface CRReport extends AnalysisResult {
|
|
33
|
+
type: "cr";
|
|
34
|
+
summary: string;
|
|
35
|
+
issues: CRIssue[];
|
|
36
|
+
score: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface CRIssue {
|
|
40
|
+
severity: "info" | "warning" | "error";
|
|
41
|
+
file?: string;
|
|
42
|
+
line?: number;
|
|
43
|
+
title: string;
|
|
44
|
+
description: string;
|
|
45
|
+
suggestion?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DashboardConfig {
|
|
49
|
+
port: number;
|
|
50
|
+
host: string;
|
|
51
|
+
}
|