feat: 提交详情能看 diff,也能丢给 AI 审这一次改动。文件检查可单独排除路径,TODO 只认大写,避免把 todo 表和模块当成待办标记。

热力图按容器宽度铺满一年,不再横向滚动。托盘右键换成可换皮肤的弹层;更新下载显示进度,退出后再由脚本拉起安装器,避免还占着 exe。
This commit is contained in:
李琦
2026-08-19 15:46:03 +08:00
parent 1694397245
commit 7c4109f687
38 changed files with 2618 additions and 129 deletions

View File

@@ -21,7 +21,7 @@ const (
// 它不会重新遍历整个仓库,只读取已记录的小型源码文件,用于发现 TODO、长文件等轻量风险。
type InsightService struct{}
func (InsightService) Analyze(ctx context.Context, project model.Project, structure model.StructureStats, git model.GitStats) (model.ProjectInsights, error) {
func (InsightService) Analyze(ctx context.Context, project model.Project, structure model.StructureStats, git model.GitStats, excludes []model.ExclusionRule) (model.ProjectInsights, error) {
desc, e := platform.ResolvePath(project.Path)
if e != nil {
return model.ProjectInsights{}, e
@@ -40,6 +40,7 @@ func (InsightService) Analyze(ctx context.Context, project model.Project, struct
}
checkLanguageHealth(project.Languages, add)
structure = filterStructureForInsights(structure, excludes)
out.Summary.LargeFiles = len(structure.LargeFiles)
checkStructureHealth(structure, add)
checkGitHealth(git, add)
@@ -162,7 +163,7 @@ func inspectTextFile(path string) (int, []model.InsightIssue) {
kind, severity = "FIXME", "medium"
case strings.Contains(upper, "HACK"):
kind, severity = "HACK", "medium"
case strings.Contains(upper, "TODO"):
case hasUpperTODO(text):
kind = "TODO"
}
if kind != "" {
@@ -172,6 +173,70 @@ func inspectTextFile(path string) (int, []model.InsightIssue) {
return line, issues
}
func filterStructureForInsights(s model.StructureStats, rules []model.ExclusionRule) model.StructureStats {
if len(rules) == 0 {
return s
}
out := s
out.Files = out.Files[:0:0]
out.LargeFiles = out.LargeFiles[:0:0]
out.Folders = out.Folders[:0:0]
for _, f := range s.Files {
if pathExcluded(f.Path, rules) {
continue
}
out.Files = append(out.Files, f)
}
for _, f := range s.LargeFiles {
if pathExcluded(f.Path, rules) {
continue
}
out.LargeFiles = append(out.LargeFiles, f)
}
for _, f := range s.Folders {
if pathExcluded(f.Name, rules) {
continue
}
out.Folders = append(out.Folders, f)
}
return out
}
func pathExcluded(path string, rules []model.ExclusionRule) bool {
path = strings.TrimSpace(path)
if path == "" || len(rules) == 0 {
return false
}
slash := filepath.ToSlash(path)
for _, r := range rules {
if MatchExcludedPattern(r.Pattern, slash) {
return true
}
}
return false
}
// hasUpperTODO 只认大写 TODO 标记,避免命中 todo 表名、模块名或变量。
func hasUpperTODO(text string) bool {
for i := 0; i <= len(text)-4; i++ {
if text[i:i+4] != "TODO" {
continue
}
if i > 0 && isIdentByte(text[i-1]) {
continue
}
if i+4 < len(text) && isIdentByte(text[i+4]) {
continue
}
return true
}
return false
}
func isIdentByte(b byte) bool {
return b == '_' || (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z') || (b >= '0' && b <= '9')
}
func isInsightTextFile(ext string) bool {
switch strings.ToLower(ext) {
case ".go", ".php", ".js", ".ts", ".vue", ".jsx", ".tsx", ".css", ".scss", ".sass", ".less", ".html", ".md", ".json", ".yaml", ".yml", ".xml", ".sql", ".py", ".java", ".cs", ".rb", ".rs", ".c", ".cpp", ".h":