From 318e6d368802c5852c372fcae3077bde52f98a34 Mon Sep 17 00:00:00 2001 From: jeffusion Date: Tue, 3 Mar 2026 17:03:23 +0800 Subject: [PATCH] build: replace tslint with Biome for code quality - Add @biomejs/biome as dev dependency - Remove deprecated tslint dependency - Add biome.json with project-specific rules - Update lint script to use Biome - Apply Biome auto-fixes across codebase --- biome.json | 50 ++++++ bun.lock | 96 +++--------- package.json | 4 +- src/config/__tests__/config-manager.test.ts | 59 +++++--- src/config/config-manager.ts | 12 +- src/config/config-schema.ts | 3 +- src/controllers/admin.ts | 13 +- src/controllers/config.ts | 114 +++++++++----- src/controllers/feedback.ts | 63 ++++---- src/controllers/review.ts | 143 +++++++++--------- src/index.ts | 22 ++- .../__tests__/file-review-store.test.ts | 64 ++++++-- src/review/__tests__/integration.test.ts | 19 +-- src/review/__tests__/judge-agent.test.ts | 18 +-- src/review/__tests__/publish-policy.test.ts | 20 +-- src/review/__tests__/sandbox-exec.test.ts | 4 +- .../__tests__/specialist-agent-react.test.ts | 101 ++++++++++--- src/review/agents/correctness-agent.ts | 21 ++- src/review/agents/critic-agent.ts | 2 +- src/review/agents/debate-orchestrator.ts | 30 ++-- src/review/agents/judge-agent.ts | 2 +- src/review/agents/maintainability-agent.ts | 21 ++- src/review/agents/reflexion-agent.ts | 27 ++-- src/review/agents/reliability-agent.ts | 21 ++- src/review/agents/security-agent.ts | 21 ++- src/review/agents/specialist-agent.ts | 32 ++-- src/review/context/diff-extractor.ts | 106 ++++++++----- src/review/context/local-repo-manager.ts | 85 +++++++---- src/review/context/sandbox-exec.ts | 6 +- src/review/engine.ts | 14 +- src/review/learning/learning-system.ts | 22 +-- src/review/memory/vector-store.ts | 17 ++- src/review/orchestrator.ts | 94 +++++++----- src/review/store/file-review-store.ts | 55 ++++--- src/review/tools/code-search-tool.ts | 10 +- src/review/tools/file-read-tool.ts | 4 +- .../tools/function-reference-search-tool.ts | 35 ++--- src/review/tools/registry.ts | 2 +- src/review/types.ts | 13 +- src/services/ai-review.ts | 50 +++--- src/services/feishu.ts | 40 +++-- src/services/gitea.ts | 87 ++++++++--- src/utils/logger.ts | 2 +- 43 files changed, 1005 insertions(+), 619 deletions(-) create mode 100644 biome.json diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..e39ead2 --- /dev/null +++ b/biome.json @@ -0,0 +1,50 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "correctness": { + "noUnusedImports": "warn", + "noUnusedVariables": "warn" + }, + "style": { + "noNonNullAssertion": "off", + "useImportType": "off" + }, + "suspicious": { + "noExplicitAny": "off", + "noExportsInTest": "off", + "noImplicitAnyLet": "off" + }, + "complexity": { + "noForEach": "off" + } + } + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "always", + "trailingCommas": "es5" + } + }, + "files": { + "ignore": [ + "node_modules", + "dist", + "frontend", + "*.json", + "*.md" + ] + } +} diff --git a/bun.lock b/bun.lock index ac3ba71..4d2dc3d 100644 --- a/bun.lock +++ b/bun.lock @@ -15,18 +15,32 @@ "zod-to-json-schema": "^3.25.1", }, "devDependencies": { + "@biomejs/biome": "^1.9.4", "@types/lodash-es": "^4.17.12", "@types/node": "^22.13.10", "concurrently": "^9.2.1", - "tslint": "^6.1.3", "typescript": "^5.8.2", }, }, }, "packages": { - "@babel/code-frame": ["@babel/code-frame@7.26.2", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ=="], + "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], "@hono/zod-validator": ["@hono/zod-validator@0.4.3", "", { "peerDependencies": { "hono": ">=3.9.0", "zod": "^3.19.1" } }, "sha512-xIgMYXDyJ4Hj6ekm9T9Y27s080Nl9NXHcJkOvkXPhubOLj8hZkOL8pDnnXfvCf5xEE8Q4oMFenQUZZREUY2gqQ=="], @@ -50,18 +64,10 @@ "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "axios": ["axios@1.8.3", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - - "builtin-modules": ["builtin-modules@1.1.1", "", {}, "sha512-wxXCdllwGhI2kCC0MnvTGYTMvnVZTvqgypkiTI8Pa5tcz2i6VqsqwYGgqwXji+4RgCzms6EajE4IxiUH6HH8nQ=="], - "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -74,16 +80,10 @@ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - "diff": ["diff@4.0.2", "", {}, "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="], - "dotenv": ["dotenv@16.4.7", "", {}, "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -100,10 +100,6 @@ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], - - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], "follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="], @@ -114,8 +110,6 @@ "formdata-node": ["formdata-node@4.4.1", "", { "dependencies": { "node-domexception": "1.0.0", "web-streams-polyfill": "4.0.0-beta.3" } }, "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ=="], - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -124,8 +118,6 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], @@ -140,18 +132,8 @@ "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -160,59 +142,33 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "openai": ["openai@4.87.3", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" }, "peerDependencies": { "ws": "^8.18.0", "zod": "^3.23.8" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-d2D54fzMuBYTxMW8wcNmhT1rYKcTfMJ8t+4KjH2KtvYenygITiGBgHoIrzHwnDQWW+C5oCA+ikIR2jgPCFqcKQ=="], - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], - "tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - - "tslint": ["tslint@6.1.3", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", "commander": "^2.12.1", "diff": "^4.0.1", "glob": "^7.1.1", "js-yaml": "^3.13.1", "minimatch": "^3.0.4", "mkdirp": "^0.5.3", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.13.0", "tsutils": "^2.29.0" }, "peerDependencies": { "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" }, "bin": { "tslint": "bin/tslint" } }, "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg=="], - - "tsutils": ["tsutils@2.29.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" } }, "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], @@ -228,8 +184,6 @@ "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -246,22 +200,8 @@ "openai/@types/node": ["@types/node@18.19.80", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-kEWeMwMeIvxYkeg1gTc01awpwLbfMRZXdIhwRcakd/KlK53jmRC26LqcbIt7fnAQTu5GzlnWmzA3H6+l1u6xxQ=="], - "rxjs/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "tslint/chalk": ["chalk@2.4.2", "", { "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" } }, "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ=="], - "@types/node-fetch/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], "openai/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], - - "tslint/chalk/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], - - "tslint/chalk/supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="], - - "tslint/chalk/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], - - "tslint/chalk/supports-color/has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="], - - "tslint/chalk/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], } } diff --git a/package.json b/package.json index da09489..db5d531 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,10 @@ "zod-to-json-schema": "^3.25.1" }, "devDependencies": { + "@biomejs/biome": "^1.9.4", "@types/lodash-es": "^4.17.12", "@types/node": "^22.13.10", "concurrently": "^9.2.1", - "tslint": "^6.1.3", "typescript": "^5.8.2" }, "files": [ @@ -35,7 +35,7 @@ "build": "rm -rf dist && tsc", "start": "bun run src/index.ts", "start:prod": "bun run dist/index.js", - "lint": "tslint -c tslint.json src/**/*.ts", + "lint": "biome check src/", "test": "bun test" }, "keywords": [ diff --git a/src/config/__tests__/config-manager.test.ts b/src/config/__tests__/config-manager.test.ts index ff2df25..5a8ae97 100644 --- a/src/config/__tests__/config-manager.test.ts +++ b/src/config/__tests__/config-manager.test.ts @@ -11,27 +11,48 @@ declare module 'bun:test' { } // @ts-expect-error bun:test is provided by Bun at runtime -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { unlink, readFile } from 'node:fs/promises'; +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { randomUUID } from 'node:crypto'; +import { readFile, unlink } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import type { AppConfig } from '../config-manager'; // ── All env keys in the Zod schema ────────────────────────────────────────── const SCHEMA_KEYS = [ - 'GITEA_API_URL', 'GITEA_ACCESS_TOKEN', 'GITEA_ADMIN_TOKEN', - 'OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_MODEL', - 'CUSTOM_SUMMARY_PROMPT', 'CUSTOM_LINE_COMMENT_PROMPT', - 'FEISHU_WEBHOOK_URL', 'FEISHU_WEBHOOK_SECRET', - 'PORT', 'WEBHOOK_SECRET', 'ADMIN_PASSWORD', 'JWT_SECRET', - 'REVIEW_ENGINE', 'REVIEW_WORKDIR', 'REVIEW_MODEL_PLANNER', - 'REVIEW_MODEL_SPECIALIST', 'REVIEW_MODEL_JUDGE', - 'REVIEW_MAX_PARALLEL_RUNS', 'REVIEW_MAX_FILES_PER_RUN', - 'REVIEW_MAX_FILE_CONTENT_CHARS', 'REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE', - 'REVIEW_ENABLE_HUMAN_GATE', 'REVIEW_ALLOWED_COMMANDS', 'REVIEW_COMMAND_TIMEOUT_MS', - 'QDRANT_URL', 'ENABLE_MEMORY', 'FEW_SHOT_EXAMPLES_COUNT', - 'ENABLE_REFLECTION', 'MAX_REFLECTION_ROUNDS', 'ENABLE_DEBATE', 'DEBATE_THRESHOLD', + 'GITEA_API_URL', + 'GITEA_ACCESS_TOKEN', + 'GITEA_ADMIN_TOKEN', + 'OPENAI_BASE_URL', + 'OPENAI_API_KEY', + 'OPENAI_MODEL', + 'CUSTOM_SUMMARY_PROMPT', + 'CUSTOM_LINE_COMMENT_PROMPT', + 'FEISHU_WEBHOOK_URL', + 'FEISHU_WEBHOOK_SECRET', + 'PORT', + 'WEBHOOK_SECRET', + 'ADMIN_PASSWORD', + 'JWT_SECRET', + 'REVIEW_ENGINE', + 'REVIEW_WORKDIR', + 'REVIEW_MODEL_PLANNER', + 'REVIEW_MODEL_SPECIALIST', + 'REVIEW_MODEL_JUDGE', + 'REVIEW_MAX_PARALLEL_RUNS', + 'REVIEW_MAX_FILES_PER_RUN', + 'REVIEW_MAX_FILE_CONTENT_CHARS', + 'REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE', + 'REVIEW_ENABLE_HUMAN_GATE', + 'REVIEW_ALLOWED_COMMANDS', + 'REVIEW_COMMAND_TIMEOUT_MS', + 'QDRANT_URL', + 'ENABLE_MEMORY', + 'FEW_SHOT_EXAMPLES_COUNT', + 'ENABLE_REFLECTION', + 'MAX_REFLECTION_ROUNDS', + 'ENABLE_DEBATE', + 'DEBATE_THRESHOLD', ] as const; const CONTROL_KEYS = ['CONFIG_OVERRIDES_PATH', 'NODE_ENV'] as const; @@ -80,7 +101,11 @@ describe('ConfigManager', () => { process.env[key] = savedEnv[key]!; } } - try { await unlink(tmpPath); } catch { /* ok if missing */ } + try { + await unlink(tmpPath); + } catch { + /* ok if missing */ + } }); // ─── 1. Layering: defaults < env < override ───────────────────────── diff --git a/src/config/config-manager.ts b/src/config/config-manager.ts index cd03e04..ea5c89e 100644 --- a/src/config/config-manager.ts +++ b/src/config/config-manager.ts @@ -8,12 +8,12 @@ * Bun-friendly IO: reads via readFile, writes atomically via temp+rename. */ -import { z } from 'zod'; -import { dirname, resolve } from 'node:path'; -import { rename, mkdir, writeFile, readFile } from 'node:fs/promises'; -import { readFileSync } from 'node:fs'; import { randomUUID } from 'node:crypto'; +import { readFileSync } from 'node:fs'; +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; import { config as dotenvConfig } from 'dotenv'; +import { z } from 'zod'; // Load .env before any process.env access (must precede singleton construction) dotenvConfig(); @@ -79,7 +79,7 @@ const envSchema = z.object({ // Memory & learning QDRANT_URL: z.preprocess( (val) => (typeof val === 'string' && val.trim() === '' ? undefined : val), - z.string().url().optional(), + z.string().url().optional() ), ENABLE_MEMORY: z .enum(['true', 'false']) @@ -204,7 +204,6 @@ const DEV_FALLBACK_CONFIG: AppConfig = { }, }; - // --------------------------------------------------------------------------- // ConfigManager // --------------------------------------------------------------------------- @@ -394,7 +393,6 @@ class ConfigManager { } return 'default'; } - } // --------------------------------------------------------------------------- diff --git a/src/config/config-schema.ts b/src/config/config-schema.ts index ba71d8a..4eeee0a 100644 --- a/src/config/config-schema.ts +++ b/src/config/config-schema.ts @@ -193,7 +193,8 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [ envKey: 'WEBHOOK_SECRET', group: 'app', label: 'Webhook 密钥', - description: '用于验证 Gitea Webhook 请求来源的 HMAC 密钥,修改需通过 .env 配置并同步更新 Gitea', + description: + '用于验证 Gitea Webhook 请求来源的 HMAC 密钥,修改需通过 .env 配置并同步更新 Gitea', type: 'string', sensitive: true, readonly: true, diff --git a/src/controllers/admin.ts b/src/controllers/admin.ts index 73433f5..d7c71b1 100644 --- a/src/controllers/admin.ts +++ b/src/controllers/admin.ts @@ -1,9 +1,9 @@ import { Hono } from 'hono'; import { sign } from 'hono/jwt'; import config from '../config'; +import { reviewEngine } from '../review/engine'; import { giteaService } from '../services/gitea'; import { logger } from '../utils/logger'; -import { reviewEngine } from '../review/engine'; const publicRoutes = new Hono(); const protectedRoutes = new Hono(); @@ -26,13 +26,12 @@ publicRoutes.post('/login', async (c) => { return c.json({ message: 'Invalid credentials' }, 401); }); - // --- Protected Routes --- // 获取仓库列表及 Webhook 状态 protectedRoutes.get('/repositories', async (c) => { try { - const page = parseInt(c.req.query('page') || '1', 10); + const page = Number.parseInt(c.req.query('page') || '1', 10); const query = c.req.query('q'); const limit = 30; // 每页数量固定,或也可从查询参数获取 @@ -43,7 +42,7 @@ protectedRoutes.get('/repositories', async (c) => { repos.map(async (repo) => { const [owner, repoName] = repo.full_name.split('/'); const hooks = await giteaService.listWebhooks(owner, repoName); - const webhook = hooks.find(h => h.config.url === webhookUrl); + const webhook = hooks.find((h) => h.config.url === webhookUrl); return { name: repo.full_name, webhook_status: webhook ? 'active' : 'inactive', @@ -67,7 +66,7 @@ protectedRoutes.get('/repositories', async (c) => { // 创建 Webhook protectedRoutes.post('/repositories/:owner/:repo/webhook', async (c) => { const { owner, repo } = c.req.param(); - const webhookUrl = new URL(c.req.url).origin + '/webhook/gitea'; + const webhookUrl = `${new URL(c.req.url).origin}/webhook/gitea`; try { await giteaService.createWebhook(owner, repo, webhookUrl); @@ -83,7 +82,7 @@ protectedRoutes.delete('/repositories/:owner/:repo/webhook/:hookId', async (c) = const { owner, repo, hookId } = c.req.param(); try { - await giteaService.deleteWebhook(owner, repo, parseInt(hookId, 10)); + await giteaService.deleteWebhook(owner, repo, Number.parseInt(hookId, 10)); return c.json({ success: true }); } catch (error: any) { logger.error(`删除 ${owner}/${repo} 的 Webhook 失败:`, error); @@ -94,7 +93,7 @@ protectedRoutes.delete('/repositories/:owner/:repo/webhook/:hookId', async (c) = // 查询审查任务 protectedRoutes.get('/review/runs', async (c) => { try { - const limit = parseInt(c.req.query('limit') || '50', 10); + const limit = Number.parseInt(c.req.query('limit') || '50', 10); const runs = await reviewEngine.listRuns(limit); return c.json({ data: runs }); } catch (error: any) { diff --git a/src/controllers/config.ts b/src/controllers/config.ts index 4e76cfd..556e62c 100644 --- a/src/controllers/config.ts +++ b/src/controllers/config.ts @@ -1,5 +1,5 @@ import { Hono } from 'hono'; -import { configManager, type AppConfig } from '../config/config-manager'; +import { type AppConfig, configManager } from '../config/config-manager'; import { CONFIG_FIELDS, CONFIG_GROUPS, type ConfigFieldMeta } from '../config/config-schema'; import { logger } from '../utils/logger'; @@ -19,9 +19,7 @@ const INTEGER_FIELDS = new Set([ ]); /** Fast lookup from envKey → field metadata. */ -const FIELDS_MAP = new Map( - CONFIG_FIELDS.map((f) => [f.envKey, f]), -); +const FIELDS_MAP = new Map(CONFIG_FIELDS.map((f) => [f.envKey, f])); // ── Helpers ────────────────────────────────────────────────────────────────── @@ -31,50 +29,84 @@ const FIELDS_MAP = new Map( */ function getEffectiveValue( envKey: string, - current: AppConfig, + current: AppConfig ): string | number | boolean | undefined { switch (envKey) { // Gitea - case 'GITEA_API_URL': return current.gitea.apiUrl; - case 'GITEA_ACCESS_TOKEN': return current.gitea.accessToken; - case 'GITEA_ADMIN_TOKEN': return current.admin.giteaAdminToken; + case 'GITEA_API_URL': + return current.gitea.apiUrl; + case 'GITEA_ACCESS_TOKEN': + return current.gitea.accessToken; + case 'GITEA_ADMIN_TOKEN': + return current.admin.giteaAdminToken; // OpenAI - case 'OPENAI_BASE_URL': return current.openai.baseUrl; - case 'OPENAI_API_KEY': return current.openai.apiKey; - case 'OPENAI_MODEL': return current.openai.model; - case 'CUSTOM_SUMMARY_PROMPT': return current.openai.customSummaryPrompt; - case 'CUSTOM_LINE_COMMENT_PROMPT': return current.openai.customLineCommentPrompt; + case 'OPENAI_BASE_URL': + return current.openai.baseUrl; + case 'OPENAI_API_KEY': + return current.openai.apiKey; + case 'OPENAI_MODEL': + return current.openai.model; + case 'CUSTOM_SUMMARY_PROMPT': + return current.openai.customSummaryPrompt; + case 'CUSTOM_LINE_COMMENT_PROMPT': + return current.openai.customLineCommentPrompt; // Feishu - case 'FEISHU_WEBHOOK_URL': return current.feishu.webhookUrl; - case 'FEISHU_WEBHOOK_SECRET': return current.feishu.webhookSecret; + case 'FEISHU_WEBHOOK_URL': + return current.feishu.webhookUrl; + case 'FEISHU_WEBHOOK_SECRET': + return current.feishu.webhookSecret; // App - case 'PORT': return current.app.port; - case 'WEBHOOK_SECRET': return current.app.webhookSecret; + case 'PORT': + return current.app.port; + case 'WEBHOOK_SECRET': + return current.app.webhookSecret; // Admin - case 'ADMIN_PASSWORD': return current.admin.password; - case 'JWT_SECRET': return current.admin.jwtSecret; + case 'ADMIN_PASSWORD': + return current.admin.password; + case 'JWT_SECRET': + return current.admin.jwtSecret; // Review - case 'REVIEW_ENGINE': return current.review.engine; - case 'REVIEW_WORKDIR': return current.review.workdir; - case 'REVIEW_MODEL_PLANNER': return current.review.modelPlanner; - case 'REVIEW_MODEL_SPECIALIST': return current.review.modelSpecialist; - case 'REVIEW_MODEL_JUDGE': return current.review.modelJudge; - case 'REVIEW_MAX_PARALLEL_RUNS': return current.review.maxParallelRuns; - case 'REVIEW_MAX_FILES_PER_RUN': return current.review.maxFilesPerRun; - case 'REVIEW_MAX_FILE_CONTENT_CHARS': return current.review.maxFileContentChars; - case 'REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE': return current.review.autoPublishMinConfidence; - case 'REVIEW_ENABLE_HUMAN_GATE': return current.review.enableHumanGate; - case 'REVIEW_ALLOWED_COMMANDS': return current.review.allowedCommands.join(','); - case 'REVIEW_COMMAND_TIMEOUT_MS': return current.review.commandTimeoutMs; + case 'REVIEW_ENGINE': + return current.review.engine; + case 'REVIEW_WORKDIR': + return current.review.workdir; + case 'REVIEW_MODEL_PLANNER': + return current.review.modelPlanner; + case 'REVIEW_MODEL_SPECIALIST': + return current.review.modelSpecialist; + case 'REVIEW_MODEL_JUDGE': + return current.review.modelJudge; + case 'REVIEW_MAX_PARALLEL_RUNS': + return current.review.maxParallelRuns; + case 'REVIEW_MAX_FILES_PER_RUN': + return current.review.maxFilesPerRun; + case 'REVIEW_MAX_FILE_CONTENT_CHARS': + return current.review.maxFileContentChars; + case 'REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE': + return current.review.autoPublishMinConfidence; + case 'REVIEW_ENABLE_HUMAN_GATE': + return current.review.enableHumanGate; + case 'REVIEW_ALLOWED_COMMANDS': + return current.review.allowedCommands.join(','); + case 'REVIEW_COMMAND_TIMEOUT_MS': + return current.review.commandTimeoutMs; // Memory - case 'QDRANT_URL': return current.review.qdrantUrl; - case 'ENABLE_MEMORY': return current.review.enableMemory; - case 'FEW_SHOT_EXAMPLES_COUNT': return current.review.fewShotExamplesCount; - case 'ENABLE_REFLECTION': return current.review.enableReflection; - case 'MAX_REFLECTION_ROUNDS': return current.review.maxReflectionRounds; - case 'ENABLE_DEBATE': return current.review.enableDebate; - case 'DEBATE_THRESHOLD': return current.review.debateThreshold; - default: return undefined; + case 'QDRANT_URL': + return current.review.qdrantUrl; + case 'ENABLE_MEMORY': + return current.review.enableMemory; + case 'FEW_SHOT_EXAMPLES_COUNT': + return current.review.fewShotExamplesCount; + case 'ENABLE_REFLECTION': + return current.review.enableReflection; + case 'MAX_REFLECTION_ROUNDS': + return current.review.maxReflectionRounds; + case 'ENABLE_DEBATE': + return current.review.enableDebate; + case 'DEBATE_THRESHOLD': + return current.review.debateThreshold; + default: + return undefined; } } @@ -106,7 +138,7 @@ function validateField(field: ConfigFieldMeta, key: string, value: string): stri } case 'number': { const num = Number(value); - if (isNaN(num)) { + if (Number.isNaN(num)) { return `${field.label}(${key})必须是有效的数字`; } if (INTEGER_FIELDS.has(key) && !Number.isInteger(num)) { @@ -255,7 +287,7 @@ configRouter.post('/reset', async (c) => { if (unknownKeys.length > 0) { return c.json( { message: '保存配置失败', error: `未知配置项: ${unknownKeys.join(', ')}` }, - 400, + 400 ); } diff --git a/src/controllers/feedback.ts b/src/controllers/feedback.ts index f63dda3..8061d3a 100644 --- a/src/controllers/feedback.ts +++ b/src/controllers/feedback.ts @@ -1,12 +1,12 @@ -import { Hono } from 'hono'; import { zValidator } from '@hono/zod-validator'; -import { z } from 'zod'; -import { FileReviewStore } from '../review/store/file-review-store'; -import { VectorMemoryStore } from '../review/memory/vector-store'; -import { LearningSystem } from '../review/learning/learning-system'; -import { giteaService } from '../services/gitea'; -import config from '../config'; +import { Hono } from 'hono'; import OpenAI from 'openai'; +import { z } from 'zod'; +import config from '../config'; +import { LearningSystem } from '../review/learning/learning-system'; +import { VectorMemoryStore } from '../review/memory/vector-store'; +import { FileReviewStore } from '../review/store/file-review-store'; +import { giteaService } from '../services/gitea'; const feedbackRouter = new Hono(); @@ -68,7 +68,10 @@ feedbackRouter.post( // 原子幂等性保护:先标记finding为published(原子check-and-set) // 只有第一个请求会得到true,后续并发/重试请求会得到false // 这解决了read-check-write竞态:两个并发请求不会都发布评论 - const wasUnpublished = await reviewStore.markFindingPublished(finding.runId, finding.fingerprint); + const wasUnpublished = await reviewStore.markFindingPublished( + finding.runId, + finding.fingerprint + ); if (!wasUnpublished) { // finding已被标记为published,但需验证是否真的发布成功 @@ -76,7 +79,7 @@ feedbackRouter.post( // 检查是否存在已发布的comment记录来确认真实状态 // 关键:必须通过fingerprint匹配,而非仅path+line,以区分同一位置的不同findings const publishedComment = runDetails.comments.find( - c => c.status === 'published' && c.fingerprint === finding.fingerprint + (c) => c.status === 'published' && c.fingerprint === finding.fingerprint ); if (publishedComment) { @@ -88,15 +91,17 @@ feedbackRouter.post( learningApplied: false, published: true, }); - } else { - // published标记存在但无published comment记录 - // 可能原因:1) 并发请求正在发布中 2) 之前发布失败并回滚 - // 不能声称成功,返回错误让用户稍后重试 - return c.json({ + } + // published标记存在但无published comment记录 + // 可能原因:1) 并发请求正在发布中 2) 之前发布失败并回滚 + // 不能声称成功,返回错误让用户稍后重试 + return c.json( + { error: 'Finding approval in progress or previously failed. Please retry in a moment.', inProgress: true, - }, 409); // 409 Conflict - } + }, + 409 + ); // 409 Conflict } // 以下代码只会被第一个请求执行(wasUnpublished=true) @@ -111,7 +116,12 @@ feedbackRouter.post( if (approved) { await learningSystem.learnFromApproval(finding, owner, repo); } else { - await learningSystem.learnFromFalsePositive(finding, reason || '人工标记为误报', owner, repo); + await learningSystem.learnFromFalsePositive( + finding, + reason || '人工标记为误报', + owner, + repo + ); } learningApplied = true; @@ -146,19 +156,9 @@ _此问题已通过人工审批确认_`; // 2. 再写本地record,失败不回滚(因为Gitea已成功,重试不应重复发布) try { if (runDetails.run.eventType === 'pull_request' && runDetails.run.prNumber) { - await giteaService.addPullRequestComment( - owner, - repo, - runDetails.run.prNumber, - comment - ); + await giteaService.addPullRequestComment(owner, repo, runDetails.run.prNumber, comment); } else if (runDetails.run.commitSha) { - await giteaService.addCommitComment( - owner, - repo, - runDetails.run.commitSha, - comment - ); + await giteaService.addCommitComment(owner, repo, runDetails.run.commitSha, comment); } } catch (giteaError) { // Gitea API失败:回滚published状态,允许用户重试发布 @@ -181,7 +181,10 @@ _此问题已通过人工审批确认_`; } catch (storeError) { // 本地store失败:回滚published标记,允许用户重试 // 如果用户立即重试,可能导致重复Gitea评论(可接受的权衡以避免永久卡死) - console.error('Failed to persist comment record after successful Gitea publish, rolling back:', storeError); + console.error( + 'Failed to persist comment record after successful Gitea publish, rolling back:', + storeError + ); await reviewStore.unmarkFindingPublished(finding.runId, finding.fingerprint); throw new Error( 'Comment published to Gitea but failed to save locally. State rolled back, you may retry. Note: immediate retry may create duplicate comments.' diff --git a/src/controllers/review.ts b/src/controllers/review.ts index ac72c4f..8a1bb34 100644 --- a/src/controllers/review.ts +++ b/src/controllers/review.ts @@ -1,11 +1,11 @@ +import * as crypto from 'node:crypto'; import { Context } from 'hono'; import { map } from 'lodash-es'; -import { giteaService, PullRequestFile, PullRequestDetails } from '../services/gitea'; -import { aiReviewService } from '../services/ai-review'; -import { feishuService } from '../services/feishu'; import config from '../config'; import { reviewEngine } from '../review/engine'; -import * as crypto from 'crypto'; +import { aiReviewService } from '../services/ai-review'; +import { feishuService } from '../services/feishu'; +import { PullRequestDetails, PullRequestFile, giteaService } from '../services/gitea'; import { logger } from '../utils/logger'; // 判断是否为开发环境 @@ -16,7 +16,7 @@ enum GiteaEventType { PullRequest = 'pull_request', Status = 'status', Issue = 'issues', - Unknown = 'unknown' + Unknown = 'unknown', } /** @@ -48,10 +48,7 @@ function verifyWebhookSignature(body: string, signature: string): boolean { // Gitea的签名没有前缀,直接比较 try { // 使用timingSafeEqual进行常量时间比较,防止时序攻击 - return crypto.timingSafeEqual( - Buffer.from(calculatedSignature), - Buffer.from(signature) - ); + return crypto.timingSafeEqual(Buffer.from(calculatedSignature), Buffer.from(signature)); } catch (error) { logger.error('签名验证失败', error); return false; @@ -108,10 +105,7 @@ async function handlePullRequestEvent(c: Context, body: any): Promise } // 从事件中提取必要信息 - const { - pull_request: pullRequest, - repository: repo - } = body; + const { pull_request: pullRequest, repository: repo } = body; if (!pullRequest || !repo) { return c.json({ error: '无效的Webhook数据' }, 400); @@ -123,18 +117,21 @@ async function handlePullRequestEvent(c: Context, body: any): Promise const prTitle = pullRequest.title; const prUrl = pullRequest.html_url; - logger.info(`收到PR事件`, { owner, repo: repoName, prNumber, action: body.action }); + logger.info('收到PR事件', { owner, repo: repoName, prNumber, action: body.action }); // 处理PR审阅者通知 try { // 获取PR的审阅者列表 - const reviewerUsernames = map(pullRequest.requested_reviewers, reviewer => reviewer.full_name || reviewer.login); + const reviewerUsernames = map( + pullRequest.requested_reviewers, + (reviewer) => reviewer.full_name || reviewer.login + ); // 记录审阅者信息 if (reviewerUsernames.length > 0) { - logger.info(`PR有指定审阅者`, { + logger.info('PR有指定审阅者', { prNumber, - reviewers: reviewerUsernames.join(',') + reviewers: reviewerUsernames.join(','), }); } @@ -145,13 +142,16 @@ async function handlePullRequestEvent(c: Context, body: any): Promise // 处理审阅者指派事件 if (body.action === 'review_requested' && body.requested_reviewer) { - const newReviewerUsername = body.requested_reviewer.full_name || body.requested_reviewer.login; + const newReviewerUsername = + body.requested_reviewer.full_name || body.requested_reviewer.login; if (newReviewerUsername) { - await feishuService.sendPrReviewerAssignedNotification(prTitle, prUrl, [newReviewerUsername]); + await feishuService.sendPrReviewerAssignedNotification(prTitle, prUrl, [ + newReviewerUsername, + ]); } } } catch (error) { - logger.error(`处理PR审阅者通知失败:`, error); + logger.error('处理PR审阅者通知失败:', error); // 继续执行代码审查流程,不因通知失败而中断 } @@ -165,7 +165,9 @@ async function handlePullRequestEvent(c: Context, body: any): Promise } // 检测fork PR:head.repo存在且与base repo不同 - const headCloneUrl = pullRequest.head?.repo ? resolveCloneUrl(pullRequest.head.repo) : undefined; + const headCloneUrl = pullRequest.head?.repo + ? resolveCloneUrl(pullRequest.head.repo) + : undefined; const isForkPR = headCloneUrl && headCloneUrl !== baseCloneUrl; // 包含baseSha以支持retarget场景:相同headSha但baseSha变化时需要重新审查 @@ -193,7 +195,7 @@ async function handlePullRequestEvent(c: Context, body: any): Promise } // Legacy模式:开始异步审查流程 - reviewPullRequest(owner, repoName, prNumber).catch(error => { + reviewPullRequest(owner, repoName, prNumber).catch((error) => { logger.error(`审查PR ${owner}/${repoName}#${prNumber} 失败:`, error); }); @@ -211,7 +213,7 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise sha: body.sha, commit_id: body.commit?.id, context: body.context, - repo: body.repository?.full_name + repo: body.repository?.full_name, }); // 验证请求体中是否包含必要信息 @@ -250,10 +252,10 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise message: body.commit.message || '', added: body.commit.added || [], removed: body.commit.removed || [], - modified: body.commit.modified || [] + modified: body.commit.modified || [], }; - logger.info(`收到提交状态更新事件`, { + logger.info('收到提交状态更新事件', { owner, repo: repoName, commitSha, @@ -261,7 +263,7 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise relatedPR: relatedPR?.number || 'unknown', added: commitInfo.added.length, modified: commitInfo.modified.length, - removed: commitInfo.removed.length + removed: commitInfo.removed.length, }); // Agent模式优先处理:从本地仓库派生diff,不依赖webhook文件列表 @@ -294,13 +296,17 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise } // Legacy模式:需要webhook文件列表 - if (commitInfo.added.length === 0 && commitInfo.modified.length === 0 && commitInfo.removed.length === 0) { + if ( + commitInfo.added.length === 0 && + commitInfo.modified.length === 0 && + commitInfo.removed.length === 0 + ) { logger.warn('提交没有文件变更信息,忽略审查', { commitSha }); return c.json({ status: 'ignored', message: '提交没有文件变更信息' }, 200); } // 开始异步审查流程,传入关联的PR信息 - reviewCommit(owner, repoName, commitSha, commitInfo, relatedPR).catch(error => { + reviewCommit(owner, repoName, commitSha, commitInfo, relatedPR).catch((error) => { logger.error(`审查提交 ${owner}/${repoName}@${commitSha} 失败:`, error); }); @@ -321,14 +327,17 @@ async function handleIssueEvent(c: Context, body: any): Promise { const issueTitle = issue.title; const issueUrl = issue.html_url; const creatorUsername = issue.user.full_name || issue.user.login; - const assigneeUsernames = map(issue.assignees, assignee => assignee.full_name || assignee.login); + const assigneeUsernames = map( + issue.assignees, + (assignee) => assignee.full_name || assignee.login + ); - logger.info(`收到工单事件`, { + logger.info('收到工单事件', { action, issueTitle, issueUrl, creatorUsername, - assigneeUsernames: assigneeUsernames.join(',') + assigneeUsernames: assigneeUsernames.join(','), }); try { @@ -371,16 +380,16 @@ async function reviewPullRequest(owner: string, repo: string, prNumber: number): number: prNumber, title: '测试PR', head: { - sha: 'abcd1234abcd1234abcd1234abcd1234abcd1234' + sha: 'abcd1234abcd1234abcd1234abcd1234abcd1234', }, base: { repo: { owner: { - login: owner + login: owner, }, - name: repo - } - } + name: repo, + }, + }, }; // 测试用diff内容 @@ -404,7 +413,7 @@ index 1234567..abcdefg 100644 // 生产环境中从Gitea获取真实数据 [prDetails, diffContent] = await Promise.all([ giteaService.getPullRequestDetails(owner, repo, prNumber), - giteaService.getPullRequestDiff(owner, repo, prNumber) + giteaService.getPullRequestDiff(owner, repo, prNumber), ]); } @@ -421,21 +430,21 @@ index 1234567..abcdefg 100644 ); logger.info('代码审查结果', { - summary: reviewResult.summary.substring(0, 100) + '...', - commentCount: reviewResult.lineComments.length + summary: `${reviewResult.summary.substring(0, 100)}...`, + commentCount: reviewResult.lineComments.length, }); // 添加总结评论 if (isDev) { logger.info('开发环境: 模拟添加PR评论', { - comment: reviewResult.summary + comment: reviewResult.summary, }); } else { logger.info('生产环境: 添加PR评论', { owner, repo, prNumber, - comment: reviewResult.summary + comment: reviewResult.summary, }); await giteaService.addPullRequestComment( owner, @@ -450,7 +459,7 @@ index 1234567..abcdefg 100644 if (isDev) { logger.info('开发环境: 模拟添加行评论', { commentCount: reviewResult.lineComments.length, - comments: reviewResult.lineComments + comments: reviewResult.lineComments, }); } else { await giteaService.addLineComments( @@ -465,7 +474,7 @@ index 1234567..abcdefg 100644 logger.info(`完成PR ${owner}/${repo}#${prNumber} 的代码审查`); } catch (error) { - logger.error(`审查PR失败:`, error); + logger.error('审查PR失败:', error); throw error; } } @@ -478,21 +487,22 @@ async function reviewCommit( repo: string, commitSha: string, commitInfo: { - sha: string, - message: string, - added: string[], - modified: string[], - removed: string[] + sha: string; + message: string; + added: string[]; + modified: string[]; + removed: string[]; }, relatedPR?: PullRequestDetails | null ): Promise { try { logger.info(`开始审查提交 ${owner}/${repo}@${commitSha}`); logger.info('提交信息', { - message: commitInfo.message.substring(0, 100) + (commitInfo.message.length > 100 ? '...' : ''), + message: + commitInfo.message.substring(0, 100) + (commitInfo.message.length > 100 ? '...' : ''), added: commitInfo.added.length, modified: commitInfo.modified.length, - removed: commitInfo.removed.length + removed: commitInfo.removed.length, }); // 如果是开发环境,打印更多信息但不执行实际审查 @@ -503,47 +513,42 @@ async function reviewCommit( commitSha, added: commitInfo.added, modified: commitInfo.modified, - removed: commitInfo.removed + removed: commitInfo.removed, }); return; } // 创建自定义文件列表,因为Gitea API不直接提供 const webhookFiles: PullRequestFile[] = [ - ...commitInfo.added.map(filename => ({ + ...commitInfo.added.map((filename) => ({ filename, status: 'added', additions: 0, // 不知道具体行数 deletions: 0, - changes: 0 + changes: 0, })), - ...commitInfo.modified.map(filename => ({ + ...commitInfo.modified.map((filename) => ({ filename, status: 'modified', additions: 0, deletions: 0, - changes: 0 + changes: 0, })), - ...commitInfo.removed.map(filename => ({ + ...commitInfo.removed.map((filename) => ({ filename, status: 'removed', additions: 0, deletions: 0, - changes: 0 - })) + changes: 0, + })), ]; // 使用AI审查服务分析提交,并传入webhook提供的文件列表 - const reviewResult = await aiReviewService.reviewCommit( - owner, - repo, - commitSha, - webhookFiles - ); + const reviewResult = await aiReviewService.reviewCommit(owner, repo, commitSha, webhookFiles); logger.info('提交代码审查结果', { - summary: reviewResult.summary.substring(0, 100) + '...', - commentCount: reviewResult.lineComments.length + summary: `${reviewResult.summary.substring(0, 100)}...`, + commentCount: reviewResult.lineComments.length, }); // 添加总结评论到提交 @@ -562,7 +567,7 @@ async function reviewCommit( // 尝试使用传入的PR信息,或者查找相关的PR try { // 如果已经有关联PR,直接使用 - if (relatedPR && relatedPR.number) { + if (relatedPR?.number) { logger.info(`使用已知关联的PR #${relatedPR.number}`); // 添加行级评论 @@ -579,7 +584,7 @@ async function reviewCommit( // 否则尝试查找 logger.info('尝试查找与提交关联的PR'); const response = await giteaService.getRelatedPullRequest(owner, repo, commitSha); - if (response && response.number) { + if (response?.number) { logger.info(`找到与提交关联的PR #${response.number}`); // 添加行级评论 @@ -602,7 +607,7 @@ async function reviewCommit( logger.info(`完成提交 ${owner}/${repo}@${commitSha} 的代码审查`); } catch (error) { - logger.error(`审查提交失败:`, error); + logger.error('审查提交失败:', error); throw error; } } diff --git a/src/index.ts b/src/index.ts index ef0f90a..ccded6b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,13 @@ import { Hono } from 'hono'; -import { jwt } from 'hono/jwt'; import { serveStatic } from 'hono/bun'; -import { handleGiteaWebhook } from './controllers/review'; -import { adminController } from './controllers/admin'; -import { feedbackRouter, initializeFeedbackSystem } from './controllers/feedback'; -import { configRouter } from './controllers/config'; -import config from './config'; -import { reviewEngine } from './review/engine'; +import { jwt } from 'hono/jwt'; import OpenAI from 'openai'; +import config from './config'; +import { adminController } from './controllers/admin'; +import { configRouter } from './controllers/config'; +import { feedbackRouter, initializeFeedbackSystem } from './controllers/feedback'; +import { handleGiteaWebhook } from './controllers/review'; +import { reviewEngine } from './review/engine'; // 创建Hono应用实例 const app = new Hono(); @@ -25,12 +25,12 @@ app.get('/', (c) => { webhookSecurityEnabled: webhookSecretConfigured, configuration: { webhookEndpoints: { - unified: '/webhook/gitea (支持Pull Request和Commit Status事件)' + unified: '/webhook/gitea (支持Pull Request和Commit Status事件)', }, signature: webhookSecretConfigured ? '签名验证已启用 (使用X-Gitea-Signature头)' - : '警告: 签名验证未配置,建议设置WEBHOOK_SECRET环境变量' - } + : '警告: 签名验证未配置,建议设置WEBHOOK_SECRET环境变量', + }, }); }); @@ -49,7 +49,6 @@ adminProtected.route('/feedback', feedbackRouter); adminProtected.route('/config', configRouter); app.route('/admin/api', adminProtected); - // --- 前端静态文件服务 --- // 优先服务于 public 目录下的静态文件 @@ -58,7 +57,6 @@ app.use('/*', serveStatic({ root: './public' })); // 对于所有未匹配到的GET请求,返回 index.html,以支持SPA路由 app.get('*', serveStatic({ path: './public/index.html' })); - // 启动服务器 const port = config.app.port; console.log(`⚡️ 服务启动在 http://localhost:${port}`); diff --git a/src/review/__tests__/file-review-store.test.ts b/src/review/__tests__/file-review-store.test.ts index 8b936fd..46e6af4 100644 --- a/src/review/__tests__/file-review-store.test.ts +++ b/src/review/__tests__/file-review-store.test.ts @@ -1,13 +1,15 @@ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; -import { FileReviewStore } from '../store/file-review-store'; -import { mkdtemp, rm, readFile } from 'node:fs/promises'; +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; +import { mkdtemp, readFile, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; +import { FileReviewStore } from '../store/file-review-store'; import type { PullRequestReviewPayload } from '../types'; -function makePRPayload(overrides: Partial = {}): PullRequestReviewPayload { +function makePRPayload( + overrides: Partial = {} +): PullRequestReviewPayload { return { - idempotencyKey: 'idem-' + Math.random().toString(36).slice(2, 8), + idempotencyKey: `idem-${Math.random().toString(36).slice(2, 8)}`, eventType: 'pull_request', owner: 'test-owner', repo: 'test-repo', @@ -305,17 +307,37 @@ describe('FileReviewStore', () => { await store.addFindings(run.id, [ { - id: 'f1', runId: run.id, fingerprint: 'fp1', category: 'correctness', - severity: 'high', confidence: 0.9, path: 'a.ts', line: 1, - title: 'Old', detail: 'd', evidence: 'e', suggestion: 's', published: false, + id: 'f1', + runId: run.id, + fingerprint: 'fp1', + category: 'correctness', + severity: 'high', + confidence: 0.9, + path: 'a.ts', + line: 1, + title: 'Old', + detail: 'd', + evidence: 'e', + suggestion: 's', + published: false, }, ]); await store.addFindings(run.id, [ { - id: 'f2', runId: run.id, fingerprint: 'fp2', category: 'security', - severity: 'medium', confidence: 0.8, path: 'b.ts', line: 2, - title: 'New', detail: 'd', evidence: 'e', suggestion: 's', published: false, + id: 'f2', + runId: run.id, + fingerprint: 'fp2', + category: 'security', + severity: 'medium', + confidence: 0.8, + path: 'b.ts', + line: 2, + title: 'New', + detail: 'd', + evidence: 'e', + suggestion: 's', + published: false, }, ]); @@ -329,9 +351,19 @@ describe('FileReviewStore', () => { const { run } = await store.createOrReuseRun(payload); await store.addFindings(run.id, [ { - id: 'f1', runId: run.id, fingerprint: 'fp1', category: 'correctness', - severity: 'high', confidence: 0.9, path: 'a.ts', line: 1, - title: 'Bug', detail: 'd', evidence: 'e', suggestion: 's', published: false, + id: 'f1', + runId: run.id, + fingerprint: 'fp1', + category: 'correctness', + severity: 'high', + confidence: 0.9, + path: 'a.ts', + line: 1, + title: 'Bug', + detail: 'd', + evidence: 'e', + suggestion: 's', + published: false, }, ]); @@ -352,9 +384,9 @@ describe('FileReviewStore', () => { await store.createOrReuseRun(p1); // Ensure distinct timestamps for sorting - await new Promise(r => setTimeout(r, 5)); + await new Promise((r) => setTimeout(r, 5)); await store.createOrReuseRun(p2); - await new Promise(r => setTimeout(r, 5)); + await new Promise((r) => setTimeout(r, 5)); await store.createOrReuseRun(p3); const runs = await store.listRuns(); diff --git a/src/review/__tests__/integration.test.ts b/src/review/__tests__/integration.test.ts index 01bcf67..31680de 100644 --- a/src/review/__tests__/integration.test.ts +++ b/src/review/__tests__/integration.test.ts @@ -1,19 +1,17 @@ -import { describe, test, expect, beforeEach, afterEach } from 'bun:test'; +import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { FileReviewStore } from '../store/file-review-store'; import { JudgeAgent } from '../agents/judge-agent'; import { applyPublishPolicy } from '../policy/publish-policy'; -import type { - PullRequestReviewPayload, - Finding, - ReviewRun, -} from '../types'; +import { FileReviewStore } from '../store/file-review-store'; +import type { Finding, PullRequestReviewPayload } from '../types'; type PartialFinding = Omit; -function makePRPayload(overrides: Partial = {}): PullRequestReviewPayload { +function makePRPayload( + overrides: Partial = {} +): PullRequestReviewPayload { return { idempotencyKey: 'test/repo#1:aaa...bbb', eventType: 'pull_request', @@ -27,7 +25,10 @@ function makePRPayload(overrides: Partial = {}): PullR }; } -function makeAgentFindings(count: number, severity: 'high' | 'medium' | 'low' = 'high'): PartialFinding[] { +function makeAgentFindings( + count: number, + severity: 'high' | 'medium' | 'low' = 'high' +): PartialFinding[] { return Array.from({ length: count }, (_, i) => ({ fingerprint: `fp-${severity}-${i}`, category: 'correctness' as const, diff --git a/src/review/__tests__/judge-agent.test.ts b/src/review/__tests__/judge-agent.test.ts index faaebc4..d25be25 100644 --- a/src/review/__tests__/judge-agent.test.ts +++ b/src/review/__tests__/judge-agent.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; import { JudgeAgent } from '../agents/judge-agent'; import type { Finding } from '../types'; @@ -6,7 +6,7 @@ type TestFinding = Omit; function makeFinding(overrides: Partial = {}): TestFinding { return { - fingerprint: 'fp-' + Math.random().toString(36).slice(2, 8), + fingerprint: `fp-${Math.random().toString(36).slice(2, 8)}`, category: 'correctness', severity: 'medium', confidence: 0.8, @@ -34,8 +34,8 @@ describe('JudgeAgent', () => { test('duplicate fingerprints → keeps highest weighted', () => { const fp = 'same-fingerprint'; const findings: TestFinding[] = [ - makeFinding({ fingerprint: fp, severity: 'low', confidence: 0.9 }), // weight: 1 * 0.9 = 0.9 - makeFinding({ fingerprint: fp, severity: 'high', confidence: 0.5 }), // weight: 3 * 0.5 = 1.5 ← winner + makeFinding({ fingerprint: fp, severity: 'low', confidence: 0.9 }), // weight: 1 * 0.9 = 0.9 + makeFinding({ fingerprint: fp, severity: 'high', confidence: 0.5 }), // weight: 3 * 0.5 = 1.5 ← winner makeFinding({ fingerprint: fp, severity: 'medium', confidence: 0.6 }), // weight: 2 * 0.6 = 1.2 ]; const result = judge.judge(findings); @@ -59,8 +59,8 @@ describe('JudgeAgent', () => { // ─── Sorting by severity × confidence ─── test('findings sorted by weight descending', () => { const findings: TestFinding[] = [ - makeFinding({ fingerprint: 'a', severity: 'low', confidence: 0.9 }), // 1 * 0.9 = 0.9 - makeFinding({ fingerprint: 'b', severity: 'high', confidence: 0.8 }), // 3 * 0.8 = 2.4 + makeFinding({ fingerprint: 'a', severity: 'low', confidence: 0.9 }), // 1 * 0.9 = 0.9 + makeFinding({ fingerprint: 'b', severity: 'high', confidence: 0.8 }), // 3 * 0.8 = 2.4 makeFinding({ fingerprint: 'c', severity: 'medium', confidence: 0.7 }), // 2 * 0.7 = 1.4 ]; const result = judge.judge(findings); @@ -99,10 +99,10 @@ describe('JudgeAgent', () => { // ─── Dedup + sort combined ─── test('dedup then sort: complex scenario', () => { const findings: TestFinding[] = [ - makeFinding({ fingerprint: 'x', severity: 'low', confidence: 0.3 }), // weight 0.3 — will be overridden - makeFinding({ fingerprint: 'y', severity: 'high', confidence: 0.9 }), // weight 2.7 — unique + makeFinding({ fingerprint: 'x', severity: 'low', confidence: 0.3 }), // weight 0.3 — will be overridden + makeFinding({ fingerprint: 'y', severity: 'high', confidence: 0.9 }), // weight 2.7 — unique makeFinding({ fingerprint: 'x', severity: 'medium', confidence: 0.8 }), // weight 1.6 — overrides x - makeFinding({ fingerprint: 'z', severity: 'high', confidence: 0.5 }), // weight 1.5 — unique + makeFinding({ fingerprint: 'z', severity: 'high', confidence: 0.5 }), // weight 1.5 — unique ]; const result = judge.judge(findings); expect(result.findings).toHaveLength(3); // x, y, z (deduped) diff --git a/src/review/__tests__/publish-policy.test.ts b/src/review/__tests__/publish-policy.test.ts index 7c7cddb..896d972 100644 --- a/src/review/__tests__/publish-policy.test.ts +++ b/src/review/__tests__/publish-policy.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; import { applyPublishPolicy } from '../policy/publish-policy'; import type { Finding } from '../types'; @@ -6,7 +6,7 @@ type TestFinding = Omit; function makeFinding(overrides: Partial = {}): TestFinding { return { - fingerprint: 'fp-' + Math.random().toString(36).slice(2, 8), + fingerprint: `fp-${Math.random().toString(36).slice(2, 8)}`, category: 'correctness', severity: 'medium', confidence: 0.9, @@ -113,11 +113,11 @@ describe('applyPublishPolicy', () => { // ─── Mixed findings ─── test('mixed findings split correctly', () => { const findings: TestFinding[] = [ - makeFinding({ severity: 'high', confidence: 0.95 }), // → publishable + makeFinding({ severity: 'high', confidence: 0.95 }), // → publishable makeFinding({ severity: 'medium', confidence: 0.85 }), // → publishable - makeFinding({ severity: 'low', confidence: 0.9 }), // → dropped (low severity, humanGate off) - makeFinding({ severity: 'high', confidence: 0.5 }), // → dropped (low confidence) - makeFinding({ severity: 'medium', confidence: 0.6 }), // → dropped (low confidence) + makeFinding({ severity: 'low', confidence: 0.9 }), // → dropped (low severity, humanGate off) + makeFinding({ severity: 'high', confidence: 0.5 }), // → dropped (low confidence) + makeFinding({ severity: 'medium', confidence: 0.6 }), // → dropped (low confidence) ]; const result = applyPublishPolicy(findings, MIN_CONFIDENCE, false); expect(result.publishable).toHaveLength(2); @@ -127,9 +127,9 @@ describe('applyPublishPolicy', () => { test('mixed findings with humanGate on', () => { const findings: TestFinding[] = [ - makeFinding({ severity: 'high', confidence: 0.95 }), // → publishable - makeFinding({ severity: 'low', confidence: 0.9 }), // → gated - makeFinding({ severity: 'high', confidence: 0.5 }), // → gated + makeFinding({ severity: 'high', confidence: 0.95 }), // → publishable + makeFinding({ severity: 'low', confidence: 0.9 }), // → gated + makeFinding({ severity: 'high', confidence: 0.5 }), // → gated ]; const result = applyPublishPolicy(findings, MIN_CONFIDENCE, true); expect(result.publishable).toHaveLength(1); @@ -161,7 +161,7 @@ describe('applyPublishPolicy', () => { const result = applyPublishPolicy(findings, MIN_CONFIDENCE, false); // Policy doesn't care about fingerprint - each finding evaluated independently expect(result.publishable).toHaveLength(2); // high+medium - expect(result.dropped).toHaveLength(1); // low severity + expect(result.dropped).toHaveLength(1); // low severity }); // ─── Different minConfidence thresholds ─── diff --git a/src/review/__tests__/sandbox-exec.test.ts b/src/review/__tests__/sandbox-exec.test.ts index 0140509..7952e04 100644 --- a/src/review/__tests__/sandbox-exec.test.ts +++ b/src/review/__tests__/sandbox-exec.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; import { SandboxExec } from '../context/sandbox-exec'; describe('SandboxExec', () => { @@ -157,6 +157,6 @@ describe('SandboxExec', () => { }); expect(result.stdout).not.toContain('SUPER_SECRET_TOKEN'); expect(result.stdout).not.toContain('should-not-leak'); - delete process.env.SUPER_SECRET_TOKEN; + process.env.SUPER_SECRET_TOKEN = undefined; }); }); diff --git a/src/review/__tests__/specialist-agent-react.test.ts b/src/review/__tests__/specialist-agent-react.test.ts index bfad2c4..ec79d19 100644 --- a/src/review/__tests__/specialist-agent-react.test.ts +++ b/src/review/__tests__/specialist-agent-react.test.ts @@ -1,9 +1,9 @@ -import { describe, test, expect, mock } from 'bun:test'; +import { describe, expect, mock, test } from 'bun:test'; +import { z } from 'zod'; import { SpecialistAgent } from '../agents/specialist-agent'; import { ToolRegistry } from '../tools/registry'; -import { z } from 'zod'; -import type { ReviewRun, ReviewContext, FindingCategory } from '../types'; import type { Tool } from '../tools/types'; +import type { FindingCategory, ReviewContext, ReviewRun } from '../types'; function makeRun(overrides: Partial = {}): ReviewRun { return { @@ -139,9 +139,7 @@ describe('SpecialistAgent ReAct loop', () => { suggestion: 'Use undefined', }; - const { client, getCalls } = createMockOpenAI([ - () => jsonResponse({ findings: [finding] }), - ]); + const { client, getCalls } = createMockOpenAI([() => jsonResponse({ findings: [finding] })]); const agent = new SpecialistAgent(client as any, 'gpt-4', category, 'TestAgent', 'bugs'); const result = await agent.review(makeRun(), makeContext()); @@ -178,7 +176,12 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); const result = await agent.review(makeRun(), makeContext()); @@ -203,7 +206,12 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); await agent.review(makeRun(), makeContext()); @@ -222,16 +230,21 @@ describe('SpecialistAgent ReAct loop', () => { const registry = new ToolRegistry(); registry.register(makeDummyTool()); - let callCount = 0; + const _callCount = 0; const { client, getCalls } = createMockOpenAI([ () => jsonResponse({ findings: [], need_more_investigation: true }), () => jsonResponse({ findings: [], need_more_investigation: false }), ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); - const result = await agent.review(makeRun(), makeContext()); + const _result = await agent.review(makeRun(), makeContext()); const calls = getCalls(); expect(calls.length).toBeGreaterThanOrEqual(2); @@ -271,7 +284,12 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); const result = await agent.review(makeRun(), makeContext()); @@ -314,7 +332,12 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); const result = await agent.review(makeRun(), makeContext()); @@ -329,11 +352,18 @@ describe('SpecialistAgent ReAct loop', () => { registry.register(makeDummyTool()); const { client } = createMockOpenAI([ - () => { throw new Error('API rate limited'); }, + () => { + throw new Error('API rate limited'); + }, ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); const result = await agent.review(makeRun(), makeContext()); @@ -351,9 +381,14 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); - const result = await agent.review(makeRun(), makeContext()); + const _result = await agent.review(makeRun(), makeContext()); const calls = getCalls(); expect(calls).toHaveLength(2); @@ -369,7 +404,9 @@ describe('SpecialistAgent ReAct loop', () => { const registry = new ToolRegistry(); registry.register({ ...makeDummyTool(), - execute: async () => { throw new Error('Sandbox timeout'); }, + execute: async () => { + throw new Error('Sandbox timeout'); + }, }); const { client, getCalls } = createMockOpenAI([ @@ -378,7 +415,12 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); await agent.review(makeRun(), makeContext()); @@ -397,7 +439,12 @@ describe('SpecialistAgent ReAct loop', () => { const { client } = createMockOpenAI([() => emptyResponse()]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); const result = await agent.review(makeRun(), makeContext()); @@ -413,7 +460,12 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); const result = await agent.review(makeRun(), makeContext()); @@ -440,7 +492,12 @@ describe('SpecialistAgent ReAct loop', () => { ]); const agent = new SpecialistAgent( - client as any, 'gpt-4', category, 'TestAgent', 'bugs', registry + client as any, + 'gpt-4', + category, + 'TestAgent', + 'bugs', + registry ); const result = await agent.review(makeRun(), makeContext()); diff --git a/src/review/agents/correctness-agent.ts b/src/review/agents/correctness-agent.ts index ee95f2a..ab4dc75 100644 --- a/src/review/agents/correctness-agent.ts +++ b/src/review/agents/correctness-agent.ts @@ -1,10 +1,23 @@ import OpenAI from 'openai'; -import { SpecialistAgent } from './specialist-agent'; -import { ToolRegistry } from '../tools/registry'; import type { LearningSystem } from '../learning/learning-system'; +import { ToolRegistry } from '../tools/registry'; +import { SpecialistAgent } from './specialist-agent'; export class CorrectnessAgent extends SpecialistAgent { - constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) { - super(openai, model, 'correctness', 'Correctness Agent', '业务逻辑正确性、边界条件、空值处理和明显bug', toolRegistry, learningSystem); + constructor( + openai: OpenAI, + model: string, + toolRegistry?: ToolRegistry, + learningSystem?: LearningSystem + ) { + super( + openai, + model, + 'correctness', + 'Correctness Agent', + '业务逻辑正确性、边界条件、空值处理和明显bug', + toolRegistry, + learningSystem + ); } } diff --git a/src/review/agents/critic-agent.ts b/src/review/agents/critic-agent.ts index f3dc747..eeedd2a 100644 --- a/src/review/agents/critic-agent.ts +++ b/src/review/agents/critic-agent.ts @@ -1,6 +1,6 @@ import OpenAI from 'openai'; -import { Finding, ReviewContext } from '../types'; import { logger } from '../../utils/logger'; +import { Finding, ReviewContext } from '../types'; export interface CritiqueResult { qualityScore: number; // 0-1 diff --git a/src/review/agents/debate-orchestrator.ts b/src/review/agents/debate-orchestrator.ts index 4d3c74c..54f25b9 100644 --- a/src/review/agents/debate-orchestrator.ts +++ b/src/review/agents/debate-orchestrator.ts @@ -1,7 +1,7 @@ import OpenAI from 'openai'; -import { SpecialistAgent } from './specialist-agent'; -import { Finding, FindingSeverity } from '../types'; import { logger } from '../../utils/logger'; +import { Finding, FindingSeverity } from '../types'; +import { SpecialistAgent } from './specialist-agent'; interface AgentOpinion { agentName: string; @@ -23,7 +23,7 @@ export class DebateOrchestrator { async conductDebate( finding: Omit, agents: SpecialistAgent[], - maxRounds: number = 2 + maxRounds = 2 ): Promise> { if (agents.length < 2) { logger.debug('Debate需要至少2个agents,跳过'); @@ -213,13 +213,15 @@ ${otherOpinions // 返回当前意见(从opinions Map中获取) const currentOpinion = opinions.get(agentName); - return currentOpinion || { - agentName, - confidence: 0.5, - severity: 'medium', - reasoning: '修订失败', - isValid: true, - }; + return ( + currentOpinion || { + agentName, + confidence: 0.5, + severity: 'medium', + reasoning: '修订失败', + isValid: true, + } + ); } } @@ -297,11 +299,15 @@ ${otherOpinions severityVotes[vote.severity] += vote.confidence; }); - const agreedSeverity = (Object.entries(severityVotes).sort((a, b) => b[1] - a[1])[0][0] as FindingSeverity) || finding.severity; + const agreedSeverity = + (Object.entries(severityVotes).sort((a, b) => b[1] - a[1])[0][0] as FindingSeverity) || + finding.severity; // 综合推理 const synthesizedDetail = `${finding.detail}\n\n**专家Debate意见汇总:**\n${validVotes - .map((v) => `- ${v.agentName} (${v.severity}, 置信度${v.confidence.toFixed(2)}): ${v.reasoning}`) + .map( + (v) => `- ${v.agentName} (${v.severity}, 置信度${v.confidence.toFixed(2)}): ${v.reasoning}` + ) .join('\n')}`; logger.info('Debate达成共识', { diff --git a/src/review/agents/judge-agent.ts b/src/review/agents/judge-agent.ts index d615a11..e7aa87b 100644 --- a/src/review/agents/judge-agent.ts +++ b/src/review/agents/judge-agent.ts @@ -1,4 +1,4 @@ -import { ReviewDecision, Finding } from '../types'; +import { Finding, ReviewDecision } from '../types'; const severityWeight: Record = { high: 3, diff --git a/src/review/agents/maintainability-agent.ts b/src/review/agents/maintainability-agent.ts index 0205d6e..d5990f2 100644 --- a/src/review/agents/maintainability-agent.ts +++ b/src/review/agents/maintainability-agent.ts @@ -1,10 +1,23 @@ import OpenAI from 'openai'; -import { SpecialistAgent } from './specialist-agent'; -import { ToolRegistry } from '../tools/registry'; import type { LearningSystem } from '../learning/learning-system'; +import { ToolRegistry } from '../tools/registry'; +import { SpecialistAgent } from './specialist-agent'; export class MaintainabilityAgent extends SpecialistAgent { - constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) { - super(openai, model, 'maintainability', 'Maintainability Agent', '可维护性、复杂度、接口破坏风险和可测试性不足', toolRegistry, learningSystem); + constructor( + openai: OpenAI, + model: string, + toolRegistry?: ToolRegistry, + learningSystem?: LearningSystem + ) { + super( + openai, + model, + 'maintainability', + 'Maintainability Agent', + '可维护性、复杂度、接口破坏风险和可测试性不足', + toolRegistry, + learningSystem + ); } } diff --git a/src/review/agents/reflexion-agent.ts b/src/review/agents/reflexion-agent.ts index 6412c35..c4f8a06 100644 --- a/src/review/agents/reflexion-agent.ts +++ b/src/review/agents/reflexion-agent.ts @@ -1,15 +1,18 @@ -import OpenAI from 'openai'; -import { SpecialistAgent } from './specialist-agent'; -import { CriticAgent, CritiqueResult } from './critic-agent'; -import { AgentResult, FindingCategory, ReviewContext, ReviewRun, Finding } from '../types'; -import { ToolRegistry } from '../tools/registry'; -import { LearningSystem } from '../learning/learning-system'; -import { logger } from '../../utils/logger'; -import { findingResponseSchema } from '../schema/finding-schema'; import { createHash } from 'node:crypto'; +import OpenAI from 'openai'; +import { logger } from '../../utils/logger'; +import { LearningSystem } from '../learning/learning-system'; +import { findingResponseSchema } from '../schema/finding-schema'; +import { ToolRegistry } from '../tools/registry'; +import { AgentResult, Finding, FindingCategory, ReviewContext, ReviewRun } from '../types'; +import { CriticAgent, CritiqueResult } from './critic-agent'; +import { SpecialistAgent } from './specialist-agent'; function buildFingerprint(category: string, path: string, line: number, title: string): string { - return createHash('sha256').update(`${category}:${path}:${line}:${title}`).digest('hex').slice(0, 24); + return createHash('sha256') + .update(`${category}:${path}:${line}:${title}`) + .digest('hex') + .slice(0, 24); } export class ReflexionAgent extends SpecialistAgent { @@ -31,7 +34,7 @@ export class ReflexionAgent extends SpecialistAgent { async reviewWithReflection( run: ReviewRun, context: ReviewContext, - maxReflectionRounds: number = 2 + maxReflectionRounds = 2 ): Promise { let bestFindings: Omit[] = []; let bestQualityScore = 0; @@ -165,7 +168,9 @@ ${context.diff.slice(0, 3000)} return validated.findings.map((finding) => ({ ...finding, category: this.category, - fingerprint: finding.fingerprint || buildFingerprint(this.category, finding.path, finding.line, finding.title), + fingerprint: + finding.fingerprint || + buildFingerprint(this.category, finding.path, finding.line, finding.title), })); } catch (error) { logger.error(`${this.agentName} Refine失败`, { diff --git a/src/review/agents/reliability-agent.ts b/src/review/agents/reliability-agent.ts index cf53464..cb3f715 100644 --- a/src/review/agents/reliability-agent.ts +++ b/src/review/agents/reliability-agent.ts @@ -1,10 +1,23 @@ import OpenAI from 'openai'; -import { SpecialistAgent } from './specialist-agent'; -import { ToolRegistry } from '../tools/registry'; import type { LearningSystem } from '../learning/learning-system'; +import { ToolRegistry } from '../tools/registry'; +import { SpecialistAgent } from './specialist-agent'; export class ReliabilityAgent extends SpecialistAgent { - constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) { - super(openai, model, 'reliability', 'Reliability Agent', '错误处理、重试策略、幂等性、并发一致性和资源释放', toolRegistry, learningSystem); + constructor( + openai: OpenAI, + model: string, + toolRegistry?: ToolRegistry, + learningSystem?: LearningSystem + ) { + super( + openai, + model, + 'reliability', + 'Reliability Agent', + '错误处理、重试策略、幂等性、并发一致性和资源释放', + toolRegistry, + learningSystem + ); } } diff --git a/src/review/agents/security-agent.ts b/src/review/agents/security-agent.ts index d533f4c..fe4ceca 100644 --- a/src/review/agents/security-agent.ts +++ b/src/review/agents/security-agent.ts @@ -1,10 +1,23 @@ import OpenAI from 'openai'; -import { SpecialistAgent } from './specialist-agent'; -import { ToolRegistry } from '../tools/registry'; import type { LearningSystem } from '../learning/learning-system'; +import { ToolRegistry } from '../tools/registry'; +import { SpecialistAgent } from './specialist-agent'; export class SecurityAgent extends SpecialistAgent { - constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) { - super(openai, model, 'security', 'Security Agent', '注入漏洞、权限绕过、敏感信息泄露、反序列化和输入校验缺失', toolRegistry, learningSystem); + constructor( + openai: OpenAI, + model: string, + toolRegistry?: ToolRegistry, + learningSystem?: LearningSystem + ) { + super( + openai, + model, + 'security', + 'Security Agent', + '注入漏洞、权限绕过、敏感信息泄露、反序列化和输入校验缺失', + toolRegistry, + learningSystem + ); } } diff --git a/src/review/agents/specialist-agent.ts b/src/review/agents/specialist-agent.ts index 2da8272..fbe9142 100644 --- a/src/review/agents/specialist-agent.ts +++ b/src/review/agents/specialist-agent.ts @@ -1,14 +1,17 @@ -import OpenAI from 'openai'; import { createHash } from 'node:crypto'; +import OpenAI from 'openai'; import { logger } from '../../utils/logger'; -import { findingResponseSchema } from '../schema/finding-schema'; -import { AgentResult, Finding, FindingCategory, ReviewContext, ReviewRun } from '../types'; -import { ToolRegistry } from '../tools/registry'; -import type { ToolResult, ToolExecutionContext } from '../tools/types'; import type { LearningSystem } from '../learning/learning-system'; +import { findingResponseSchema } from '../schema/finding-schema'; +import { ToolRegistry } from '../tools/registry'; +import type { ToolExecutionContext, ToolResult } from '../tools/types'; +import { AgentResult, Finding, FindingCategory, ReviewContext, ReviewRun } from '../types'; function buildFingerprint(category: string, path: string, line: number, title: string): string { - return createHash('sha256').update(`${category}:${path}:${line}:${title}`).digest('hex').slice(0, 24); + return createHash('sha256') + .update(`${category}:${path}:${line}:${title}`) + .digest('hex') + .slice(0, 24); } function toCompactContext(context: ReviewContext): string { @@ -58,7 +61,10 @@ function toCompactContext(context: ReviewContext): string { let result = tryBuild(maxChangesPerFile, maxFileContentsEntries); // 如果超过限制,逐步缩减 - while (result.length > MAX_CONTEXT_CHARS && (maxChangesPerFile > 20 || maxFileContentsEntries > 0)) { + while ( + result.length > MAX_CONTEXT_CHARS && + (maxChangesPerFile > 20 || maxFileContentsEntries > 0) + ) { if (maxChangesPerFile > 20) { maxChangesPerFile = Math.max(20, Math.floor(maxChangesPerFile * 0.7)); } else if (maxFileContentsEntries > 0) { @@ -74,7 +80,7 @@ function toCompactContext(context: ReviewContext): string { originalSize: result.length, limit: MAX_CONTEXT_CHARS, }); - result = result.slice(0, MAX_CONTEXT_CHARS) + '\n... [truncated]'; + result = `${result.slice(0, MAX_CONTEXT_CHARS)}\n... [truncated]`; } return result; @@ -137,7 +143,8 @@ ${toCompactContext(context)}`; const findings = parsed.findings.map((item) => ({ ...item, category: this.category, - fingerprint: item.fingerprint || buildFingerprint(this.category, item.path, item.line, item.title), + fingerprint: + item.fingerprint || buildFingerprint(this.category, item.path, item.line, item.title), })); return { @@ -259,7 +266,9 @@ confidence取值范围0到1。line必须是正整数且引用新增行。`, // 使用schema验证findings,防止畸形数据流入发布系统 const validated = findingResponseSchema.parse({ findings: parsed.findings }); for (const item of validated.findings) { - const fp = item.fingerprint || buildFingerprint(this.category, item.path, item.line, item.title); + const fp = + item.fingerprint || + buildFingerprint(this.category, item.path, item.line, item.title); // 基于 fingerprint 去重:后续迭代产生的同一 finding 覆盖前一次 findingsMap.set(fp, { ...item, @@ -278,7 +287,8 @@ confidence取值范围0到1。line必须是正整数且引用新增行。`, messages.push(choice.message as OpenAI.Chat.ChatCompletionMessageParam); messages.push({ role: 'user', - content: '请使用工具进行更深入的调查。如果你已经获得了足够的信息,请将 need_more_investigation 设为 false 并输出最终结果。', + content: + '请使用工具进行更深入的调查。如果你已经获得了足够的信息,请将 need_more_investigation 设为 false 并输出最终结果。', }); } catch (parseError) { logger.error(`${this.agentName} 解析响应失败`, { diff --git a/src/review/context/diff-extractor.ts b/src/review/context/diff-extractor.ts index 293c5a0..7a723ac 100644 --- a/src/review/context/diff-extractor.ts +++ b/src/review/context/diff-extractor.ts @@ -1,8 +1,8 @@ -import { readFile, lstat } from 'node:fs/promises'; +import { lstat, readFile } from 'node:fs/promises'; import path from 'node:path'; -import { DiffFile, ReviewContext, ReviewRun, ChangedFile } from '../types'; -import { SandboxExec } from './sandbox-exec'; +import { ChangedFile, DiffFile, ReviewContext, ReviewRun } from '../types'; import { LocalRepoManager } from './local-repo-manager'; +import { SandboxExec } from './sandbox-exec'; function toStatus(status: string): ChangedFile['status'] { const value = status.trim().charAt(0).toUpperCase(); @@ -33,7 +33,11 @@ export class DiffExtractor { return this.sandboxExec; } - async buildContext(run: ReviewRun, mirrorPath: string, workspacePath: string): Promise { + async buildContext( + run: ReviewRun, + mirrorPath: string, + workspacePath: string + ): Promise { const targetSha = run.headSha || run.commitSha; if (!targetSha) { throw new Error('缺少 target sha,无法构建审查上下文'); @@ -41,7 +45,8 @@ export class DiffExtractor { let baseSha = run.baseSha; if (!baseSha) { - baseSha = await this.localRepoManager.resolveCommitParent(workspacePath, targetSha) || undefined; + baseSha = + (await this.localRepoManager.resolveCommitParent(workspacePath, targetSha)) || undefined; } // Root commit场景:没有parent,使用git show获取完整diff @@ -55,7 +60,7 @@ export class DiffExtractor { : await this.getChangedFiles(workspacePath, baseSha!, targetSha); // 构建允许的文件路径集合,确保parsedDiff也受REVIEW_MAX_FILES_PER_RUN限制 - const allowedPaths = new Set(changedFiles.map(f => f.path)); + const allowedPaths = new Set(changedFiles.map((f) => f.path)); const parsedDiff = this.parseDiff(diff, allowedPaths); const fileContents = await this.readChangedFileContents(workspacePath, changedFiles); @@ -86,32 +91,51 @@ export class DiffExtractor { targetSha: string ): Promise { if (eventType === 'pull_request') { - const response = await this.sandboxExec.run('git', ['diff', '--unified=3', `${baseSha}...${targetSha}`], { - cwd: workspacePath, - timeoutMs: this.commandTimeoutMs, - }); + const response = await this.sandboxExec.run( + 'git', + ['diff', '--unified=3', `${baseSha}...${targetSha}`], + { + cwd: workspacePath, + timeoutMs: this.commandTimeoutMs, + } + ); return response.stdout; } - const response = await this.sandboxExec.run('git', ['show', '--format=', '--unified=3', targetSha], { - cwd: workspacePath, - timeoutMs: this.commandTimeoutMs, - }); + const response = await this.sandboxExec.run( + 'git', + ['show', '--format=', '--unified=3', targetSha], + { + cwd: workspacePath, + timeoutMs: this.commandTimeoutMs, + } + ); return response.stdout; } - private async getRootCommitChangedFiles(workspacePath: string, sha: string): Promise { + private async getRootCommitChangedFiles( + workspacePath: string, + sha: string + ): Promise { // Root commit:所有文件都是新增的(A状态) // --root flag是必需的,否则diff-tree对root commit返回空输出 - const statusResult = await this.sandboxExec.run('git', ['diff-tree', '--root', '--no-commit-id', '--name-status', '-r', sha], { - cwd: workspacePath, - timeoutMs: this.commandTimeoutMs, - }); + const statusResult = await this.sandboxExec.run( + 'git', + ['diff-tree', '--root', '--no-commit-id', '--name-status', '-r', sha], + { + cwd: workspacePath, + timeoutMs: this.commandTimeoutMs, + } + ); - const numStatResult = await this.sandboxExec.run('git', ['diff-tree', '--root', '--no-commit-id', '--numstat', '-r', sha], { - cwd: workspacePath, - timeoutMs: this.commandTimeoutMs, - }); + const numStatResult = await this.sandboxExec.run( + 'git', + ['diff-tree', '--root', '--no-commit-id', '--numstat', '-r', sha], + { + cwd: workspacePath, + timeoutMs: this.commandTimeoutMs, + } + ); const numMap = new Map(); for (const line of numStatResult.stdout.split('\n')) { @@ -155,16 +179,28 @@ export class DiffExtractor { return changedFiles; } - private async getChangedFiles(workspacePath: string, baseSha: string, targetSha: string): Promise { - const statusResult = await this.sandboxExec.run('git', ['diff', '--name-status', `${baseSha}...${targetSha}`], { - cwd: workspacePath, - timeoutMs: this.commandTimeoutMs, - }); + private async getChangedFiles( + workspacePath: string, + baseSha: string, + targetSha: string + ): Promise { + const statusResult = await this.sandboxExec.run( + 'git', + ['diff', '--name-status', `${baseSha}...${targetSha}`], + { + cwd: workspacePath, + timeoutMs: this.commandTimeoutMs, + } + ); - const numStatResult = await this.sandboxExec.run('git', ['diff', '--numstat', `${baseSha}...${targetSha}`], { - cwd: workspacePath, - timeoutMs: this.commandTimeoutMs, - }); + const numStatResult = await this.sandboxExec.run( + 'git', + ['diff', '--numstat', `${baseSha}...${targetSha}`], + { + cwd: workspacePath, + timeoutMs: this.commandTimeoutMs, + } + ); const numMap = new Map(); for (const line of numStatResult.stdout.split('\n')) { @@ -229,9 +265,7 @@ export class DiffExtractor { const content = await readFile(filePath, 'utf-8'); result[file.path] = content.slice(0, this.maxFileContentChars); - } catch { - continue; - } + } catch {} } return result; @@ -277,7 +311,7 @@ export class DiffExtractor { if (line.startsWith('@@')) { const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/); - if (match && match[1]) { + if (match?.[1]) { lineNumber = Number.parseInt(match[1], 10) - 1; inHunk = true; } diff --git a/src/review/context/local-repo-manager.ts b/src/review/context/local-repo-manager.ts index f4fc818..912725d 100644 --- a/src/review/context/local-repo-manager.ts +++ b/src/review/context/local-repo-manager.ts @@ -1,8 +1,8 @@ +import { createHash } from 'node:crypto'; import { access, mkdir, rm } from 'node:fs/promises'; import path from 'node:path'; -import { createHash } from 'node:crypto'; -import { SandboxExec } from './sandbox-exec'; import { logger } from '../../utils/logger'; +import { SandboxExec } from './sandbox-exec'; export interface LocalRepoPaths { mirrorPath: string; @@ -91,16 +91,24 @@ export class LocalRepoManager { if (!mirrorExists) { logger.info('创建本地 mirror 仓库', { owner, repo, mirrorPath }); - await this.sandboxExec.run('git', [...authArgs, 'clone', '--mirror', cloneUrl, mirrorPath], { - cwd: this.workDir, - timeoutMs: this.commandTimeoutMs, - }); + await this.sandboxExec.run( + 'git', + [...authArgs, 'clone', '--mirror', cloneUrl, mirrorPath], + { + cwd: this.workDir, + timeoutMs: this.commandTimeoutMs, + } + ); } else { // 更新remote URL(不含认证信息) - await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote', 'set-url', 'origin', cloneUrl], { - cwd: this.workDir, - timeoutMs: this.commandTimeoutMs, - }); + await this.sandboxExec.run( + 'git', + ['--git-dir', mirrorPath, 'remote', 'set-url', 'origin', cloneUrl], + { + cwd: this.workDir, + timeoutMs: this.commandTimeoutMs, + } + ); // fetch使用认证参数 await this.sandboxExec.run( 'git', @@ -117,28 +125,47 @@ export class LocalRepoManager { logger.info('Fork PR检测,添加head remote', { owner, repo, headCloneUrl }); // 检查head remote是否已存在,存在则更新URL - const remoteListResult = await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote'], { - cwd: this.workDir, - timeoutMs: this.commandTimeoutMs, - }); + const remoteListResult = await this.sandboxExec.run( + 'git', + ['--git-dir', mirrorPath, 'remote'], + { + cwd: this.workDir, + timeoutMs: this.commandTimeoutMs, + } + ); const hasHeadRemote = remoteListResult.stdout.includes('head'); if (hasHeadRemote) { - await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote', 'set-url', 'head', headCloneUrl], { - cwd: this.workDir, - timeoutMs: this.commandTimeoutMs, - }); + await this.sandboxExec.run( + 'git', + ['--git-dir', mirrorPath, 'remote', 'set-url', 'head', headCloneUrl], + { + cwd: this.workDir, + timeoutMs: this.commandTimeoutMs, + } + ); } else { - await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote', 'add', 'head', headCloneUrl], { - cwd: this.workDir, - timeoutMs: this.commandTimeoutMs, - }); + await this.sandboxExec.run( + 'git', + ['--git-dir', mirrorPath, 'remote', 'add', 'head', headCloneUrl], + { + cwd: this.workDir, + timeoutMs: this.commandTimeoutMs, + } + ); } // Fetch head remote await this.sandboxExec.run( 'git', - [...authArgs, '--git-dir', mirrorPath, 'fetch', 'head', '+refs/heads/*:refs/remotes/head/*'], + [ + ...authArgs, + '--git-dir', + mirrorPath, + 'fetch', + 'head', + '+refs/heads/*:refs/remotes/head/*', + ], { cwd: this.workDir, timeoutMs: this.commandTimeoutMs, @@ -156,10 +183,14 @@ export class LocalRepoManager { timeoutMs: this.commandTimeoutMs, }); - await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'worktree', 'add', '--detach', workspacePath, targetSha], { - cwd: this.workDir, - timeoutMs: this.commandTimeoutMs, - }); + await this.sandboxExec.run( + 'git', + ['--git-dir', mirrorPath, 'worktree', 'add', '--detach', workspacePath, targetSha], + { + cwd: this.workDir, + timeoutMs: this.commandTimeoutMs, + } + ); } finally { // 确保锁总是被释放,在所有mirror-mutating操作(fetch/prune/add)完成后释放 unlock(); diff --git a/src/review/context/sandbox-exec.ts b/src/review/context/sandbox-exec.ts index e9e1002..6d4970c 100644 --- a/src/review/context/sandbox-exec.ts +++ b/src/review/context/sandbox-exec.ts @@ -44,7 +44,11 @@ export class SandboxExec { }); } - async run(command: string, args: string[], options: SandboxRunOptions): Promise { + async run( + command: string, + args: string[], + options: SandboxRunOptions + ): Promise { if (!this.allowedCommands.has(command)) { throw new Error(`命令未在白名单中: ${command}`); } diff --git a/src/review/engine.ts b/src/review/engine.ts index 3fbb2c5..a16a54b 100644 --- a/src/review/engine.ts +++ b/src/review/engine.ts @@ -23,7 +23,11 @@ class ReviewEngine { config.review.maxFilesPerRun, config.review.maxFileContentChars ); - private readonly orchestrator = new ReviewOrchestrator(this.store, this.localRepoManager, this.diffExtractor); + private readonly orchestrator = new ReviewOrchestrator( + this.store, + this.localRepoManager, + this.diffExtractor + ); private started = false; private activeRunsCount = 0; @@ -61,7 +65,9 @@ class ReviewEngine { this.started = false; } - async enqueuePullRequest(payload: PullRequestReviewPayload): Promise<{ run: ReviewRun; reused: boolean }> { + async enqueuePullRequest( + payload: PullRequestReviewPayload + ): Promise<{ run: ReviewRun; reused: boolean }> { await this.store.init(); return this.store.createOrReuseRun(payload); } @@ -75,7 +81,9 @@ class ReviewEngine { return this.store.listRuns(limit); } - async getRunDetails(runId: string): Promise>> { + async getRunDetails( + runId: string + ): Promise>> { return this.store.getRunDetails(runId); } diff --git a/src/review/learning/learning-system.ts b/src/review/learning/learning-system.ts index 3df065b..5be7120 100644 --- a/src/review/learning/learning-system.ts +++ b/src/review/learning/learning-system.ts @@ -1,9 +1,9 @@ +import OpenAI from 'openai'; +import config from '../../config'; +import { logger } from '../../utils/logger'; import { VectorMemoryStore } from '../memory/vector-store'; import { FileReviewStore } from '../store/file-review-store'; import { Finding, FindingCategory } from '../types'; -import { logger } from '../../utils/logger'; -import OpenAI from 'openai'; -import config from '../../config'; export class LearningSystem { constructor( @@ -100,7 +100,9 @@ export class LearningSystem { { key: 'approved', match: { value: true } }, ], }); - approved.push(...globalApproved.filter((a) => !approved.find((e) => e.entry.id === a.entry.id))); + approved.push( + ...globalApproved.filter((a) => !approved.find((e) => e.entry.id === a.entry.id)) + ); } const examples: OpenAI.Chat.ChatCompletionMessageParam[] = []; @@ -160,11 +162,7 @@ export class LearningSystem { return []; } - async learnFromApproval( - finding: Finding, - _owner: string, - _repo: string - ): Promise { + async learnFromApproval(finding: Finding, _owner: string, _repo: string): Promise { // 将已批准的finding存储为正样本 await this.memoryStore.storeFinding(finding, true, _owner, _repo); @@ -199,9 +197,11 @@ export class LearningSystem { if (maxSimilarity > 0.9) { return -0.3; // 高度相似的误报,大幅降低置信度 - } else if (maxSimilarity > 0.8) { + } + if (maxSimilarity > 0.8) { return -0.15; // 中度相似,适度降低 - } else if (maxSimilarity > 0.7) { + } + if (maxSimilarity > 0.7) { return -0.05; // 低度相似,略微降低 } diff --git a/src/review/memory/vector-store.ts b/src/review/memory/vector-store.ts index 1a9d41f..d44ef10 100644 --- a/src/review/memory/vector-store.ts +++ b/src/review/memory/vector-store.ts @@ -1,8 +1,8 @@ import { QdrantClient } from '@qdrant/js-client-rest'; import OpenAI from 'openai'; -import { MemoryEntry, MemorySearchResult } from './types'; -import { Finding } from '../types'; import { logger } from '../../utils/logger'; +import { Finding } from '../types'; +import { MemoryEntry, MemorySearchResult } from './types'; export class VectorMemoryStore { private client: QdrantClient; @@ -70,11 +70,7 @@ export class VectorMemoryStore { }); } - async searchSimilar( - query: string, - limit: number = 5, - filter?: any - ): Promise { + async searchSimilar(query: string, limit = 5, filter?: any): Promise { await this.initialize(); const queryEmbedding = await this.getEmbedding(query); @@ -121,7 +117,12 @@ export class VectorMemoryStore { } } - async storeFinding(finding: Finding, approved: boolean, owner: string, repo: string): Promise { + async storeFinding( + finding: Finding, + approved: boolean, + owner: string, + repo: string + ): Promise { const content = `${finding.title}\n${finding.detail}\nEvidence: ${finding.evidence}`; // 使用repo-scoped ID防止不同仓库的findings相互覆盖 diff --git a/src/review/orchestrator.ts b/src/review/orchestrator.ts index e4d6705..fddfd53 100644 --- a/src/review/orchestrator.ts +++ b/src/review/orchestrator.ts @@ -3,20 +3,20 @@ import OpenAI from 'openai'; import config from '../config'; import { giteaService } from '../services/gitea'; import { logger } from '../utils/logger'; +import { DebateOrchestrator } from './agents/debate-orchestrator'; import { JudgeAgent } from './agents/judge-agent'; import { ReflexionAgent } from './agents/reflexion-agent'; -import { DebateOrchestrator } from './agents/debate-orchestrator'; import { DiffExtractor } from './context/diff-extractor'; import { LocalRepoManager, LocalRepoPaths } from './context/local-repo-manager'; +import { LearningSystem } from './learning/learning-system'; +import { VectorMemoryStore } from './memory/vector-store'; import { applyPublishPolicy } from './policy/publish-policy'; import { FileReviewStore } from './store/file-review-store'; -import { Finding, ReviewRun } from './types'; -import { ToolRegistry } from './tools/registry'; import { createCodeSearchTool } from './tools/code-search-tool'; -import { createFunctionReferenceSearchTool } from './tools/function-reference-search-tool'; import { createFileReadTool } from './tools/file-read-tool'; -import { VectorMemoryStore } from './memory/vector-store'; -import { LearningSystem } from './learning/learning-system'; +import { createFunctionReferenceSearchTool } from './tools/function-reference-search-tool'; +import { ToolRegistry } from './tools/registry'; +import { Finding, ReviewRun } from './types'; interface LineCommentInput { path: string; @@ -24,7 +24,9 @@ interface LineCommentInput { comment: string; } -function findingToLineComment(finding: Omit): LineCommentInput { +function findingToLineComment( + finding: Omit +): LineCommentInput { return { path: finding.path, line: finding.line, @@ -172,7 +174,11 @@ export class ReviewOrchestrator { startedAt: new Date(contextStart).toISOString(), }); - const context = await this.diffExtractor.buildContext(run, repoPaths.mirrorPath, repoPaths.workspacePath); + const context = await this.diffExtractor.buildContext( + run, + repoPaths.mirrorPath, + repoPaths.workspacePath + ); await this.store.addStep({ runId: run.id, @@ -291,17 +297,17 @@ export class ReviewOrchestrator { // summary comment特征:status='published' 且 path字段为空 // line comment特征:status='published' 且 path字段存在 const runDetails = await this.store.getRunDetails(run.id); - const summaryPublished = runDetails?.comments.some( - (comment) => comment.status === 'published' && !comment.path - ) || false; - const lineCommentsPublished = runDetails?.comments.some( - (comment) => comment.status === 'published' && comment.path - ) || false; + const summaryPublished = + runDetails?.comments.some((comment) => comment.status === 'published' && !comment.path) || + false; + const lineCommentsPublished = + runDetails?.comments.some((comment) => comment.status === 'published' && comment.path) || + false; if (lineCommentsPublished) { logger.info('检测到重试且line comments已发布,跳过line comments和findings标记', { runId: run.id, - existingLineComments: runDetails?.comments.filter(c => c.path).length, + existingLineComments: runDetails?.comments.filter((c) => c.path).length, }); // 重试场景:line comments已发布,跳过line comments发布步骤 // 注意:不能return,需要继续执行summary和pending gate记录(即使summary已存在) @@ -362,7 +368,8 @@ export class ReviewOrchestrator { // 关键:即使summary已存在,仍需添加gated findings到pending队列 // 防止crash发生在publishSummary之后、addCommentRecord之前时丢失待审批findings // 使用幂等性检查防止retry时重复添加 - const existingPendingComments = runDetails?.comments.filter(c => c.status === 'pending') || []; + const existingPendingComments = + runDetails?.comments.filter((c) => c.status === 'pending') || []; // 跟踪本次循环中已添加的location,防止同一run中多个findings在同一位置导致重复pending记录 const addedLocations = new Set(); @@ -373,7 +380,7 @@ export class ReviewOrchestrator { // 检查是否已存在相同的pending记录(通过runId + path + line去重) // 需要同时检查:1) 之前run的记录 2) 本次循环已添加的记录 const alreadyPending = - existingPendingComments.some(c => c.path === finding.path && c.line === finding.line) || + existingPendingComments.some((c) => c.path === finding.path && c.line === finding.line) || addedLocations.has(locationKey); if (!alreadyPending) { @@ -398,10 +405,17 @@ export class ReviewOrchestrator { // 将已发布的findings存储到向量记忆(自动标记为已批准) if (this.memoryStore && policyResult.publishable.length > 0) { for (const finding of policyResult.publishable) { - const persistedFinding = persistedFindings.find((f) => f.fingerprint === finding.fingerprint); + const persistedFinding = persistedFindings.find( + (f) => f.fingerprint === finding.fingerprint + ); if (persistedFinding) { try { - await this.memoryStore.storeFinding(persistedFinding as Finding, true, run.owner, run.repo); + await this.memoryStore.storeFinding( + persistedFinding as Finding, + true, + run.owner, + run.repo + ); } catch (error) { logger.warn('存储finding到向量记忆失败', { findingId: persistedFinding.id, @@ -456,10 +470,13 @@ export class ReviewOrchestrator { body, }); } catch (storeError) { - logger.error('Failed to persist summary comment record (non-fatal, may cause duplicate on retry)', { - runId: run.id, - error: storeError instanceof Error ? storeError.message : String(storeError), - }); + logger.error( + 'Failed to persist summary comment record (non-fatal, may cause duplicate on retry)', + { + runId: run.id, + error: storeError instanceof Error ? storeError.message : String(storeError), + } + ); // 不抛出,允许审查流程继续 } return; @@ -475,16 +492,22 @@ export class ReviewOrchestrator { body, }); } catch (storeError) { - logger.error('Failed to persist summary comment record (non-fatal, may cause duplicate on retry)', { - runId: run.id, - error: storeError instanceof Error ? storeError.message : String(storeError), - }); + logger.error( + 'Failed to persist summary comment record (non-fatal, may cause duplicate on retry)', + { + runId: run.id, + error: storeError instanceof Error ? storeError.message : String(storeError), + } + ); // 不抛出,允许审查流程继续 } } } - private async publishLineComments(run: ReviewRun, comments: LineCommentInput[]): Promise { + private async publishLineComments( + run: ReviewRun, + comments: LineCommentInput[] + ): Promise { if (comments.length === 0) { return false; } @@ -518,12 +541,15 @@ export class ReviewOrchestrator { body: comment.comment, }); } catch (storeError) { - logger.error('Failed to persist line comment record (non-fatal, may cause duplicate on retry)', { - runId: run.id, - path: comment.path, - line: comment.line, - error: storeError instanceof Error ? storeError.message : String(storeError), - }); + logger.error( + 'Failed to persist line comment record (non-fatal, may cause duplicate on retry)', + { + runId: run.id, + path: comment.path, + line: comment.line, + error: storeError instanceof Error ? storeError.message : String(storeError), + } + ); // 不抛出,继续处理下一条comment } } diff --git a/src/review/store/file-review-store.ts b/src/review/store/file-review-store.ts index 1d3375e..3ce5afe 100644 --- a/src/review/store/file-review-store.ts +++ b/src/review/store/file-review-store.ts @@ -1,6 +1,6 @@ -import { mkdir, readFile, writeFile, rename } from 'node:fs/promises'; -import path from 'node:path'; import { randomUUID } from 'node:crypto'; +import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; +import path from 'node:path'; import { CommitReviewPayload, Finding, @@ -76,9 +76,7 @@ export class FileReviewStore { this.data = createEmptyData(); await this.persist(); } else { - throw new Error( - `Store初始化失败 - 拒绝擦除数据: ${error.message || String(error)}` - ); + throw new Error(`Store初始化失败 - 拒绝擦除数据: ${error.message || String(error)}`); } } @@ -188,7 +186,10 @@ export class FileReviewStore { await this.markRunFinished(runId, 'ignored', reason); } - async markRunFailed(runId: string, error: string): Promise<{ requeued: boolean; run: ReviewRun | null }> { + async markRunFailed( + runId: string, + error: string + ): Promise<{ requeued: boolean; run: ReviewRun | null }> { await this.ensureInitialized(); const run = this.data.runs.find((item) => item.id === runId); @@ -287,7 +288,12 @@ export class FileReviewStore { return runs.slice(0, limit); } - async getRunDetails(runId: string): Promise<{ run: ReviewRun; steps: ReviewStep[]; findings: Finding[]; comments: ReviewCommentRecord[] } | null> { + async getRunDetails(runId: string): Promise<{ + run: ReviewRun; + steps: ReviewStep[]; + findings: Finding[]; + comments: ReviewCommentRecord[]; + } | null> { await this.ensureInitialized(); const run = this.data.runs.find((item) => item.id === runId); @@ -356,7 +362,11 @@ export class FileReviewStore { }; } - private async markRunFinished(runId: string, status: ReviewRunStatus, error?: string): Promise { + private async markRunFinished( + runId: string, + status: ReviewRunStatus, + error?: string + ): Promise { await this.ensureInitialized(); const run = this.data.runs.find((item) => item.id === runId); @@ -382,21 +392,20 @@ export class FileReviewStore { // 追踪当前write操作是否成功,失败时立即抛出给调用者(防止静默数据丢失) let currentWriteError: Error | null = null; - this.writeChain = this.writeChain - .then(async () => { - try { - // 原子写入:先写临时文件,再 rename 覆盖目标文件 - // POSIX rename 是原子操作,即使进程在 rename 中间崩溃,文件也不会损坏 - const tempPath = `${this.statePath}.tmp`; - await writeFile(tempPath, JSON.stringify(this.data, null, 2), 'utf-8'); - await rename(tempPath, this.statePath); - currentWriteError = null; // 写入成功 - } catch (error) { - // 捕获错误但不重新throw,保持chain为resolved状态(允许后续persist()重试) - currentWriteError = error instanceof Error ? error : new Error(String(error)); - console.error('Store persist failed:', currentWriteError); - } - }); + this.writeChain = this.writeChain.then(async () => { + try { + // 原子写入:先写临时文件,再 rename 覆盖目标文件 + // POSIX rename 是原子操作,即使进程在 rename 中间崩溃,文件也不会损坏 + const tempPath = `${this.statePath}.tmp`; + await writeFile(tempPath, JSON.stringify(this.data, null, 2), 'utf-8'); + await rename(tempPath, this.statePath); + currentWriteError = null; // 写入成功 + } catch (error) { + // 捕获错误但不重新throw,保持chain为resolved状态(允许后续persist()重试) + currentWriteError = error instanceof Error ? error : new Error(String(error)); + console.error('Store persist failed:', currentWriteError); + } + }); await this.writeChain; diff --git a/src/review/tools/code-search-tool.ts b/src/review/tools/code-search-tool.ts index db325db..7da6161 100644 --- a/src/review/tools/code-search-tool.ts +++ b/src/review/tools/code-search-tool.ts @@ -1,18 +1,14 @@ import { z } from 'zod'; -import { Tool } from './types'; import { SandboxExec } from '../context/sandbox-exec'; +import { Tool } from './types'; export function createCodeSearchTool(sandbox: SandboxExec): Tool { return { name: 'search_code', - description: - '在代码库中搜索匹配给定模式的代码,支持正则表达式。用于发现相似问题或影响范围。', + description: '在代码库中搜索匹配给定模式的代码,支持正则表达式。用于发现相似问题或影响范围。', parameters: z.object({ pattern: z.string().describe('要搜索的正则表达式模式'), - file_types: z - .array(z.string()) - .optional() - .describe('限制搜索的文件类型,如["ts", "js"]'), + file_types: z.array(z.string()).optional().describe('限制搜索的文件类型,如["ts", "js"]'), max_results: z.number().default(20).describe('最大返回结果数'), }), execute: async (params, context) => { diff --git a/src/review/tools/file-read-tool.ts b/src/review/tools/file-read-tool.ts index d29a144..8c2b208 100644 --- a/src/review/tools/file-read-tool.ts +++ b/src/review/tools/file-read-tool.ts @@ -1,7 +1,7 @@ -import { z } from 'zod'; -import { Tool } from './types'; import { readFile, realpath } from 'node:fs/promises'; import path from 'node:path'; +import { z } from 'zod'; +import { Tool } from './types'; export function createFileReadTool(): Tool { return { diff --git a/src/review/tools/function-reference-search-tool.ts b/src/review/tools/function-reference-search-tool.ts index 7a5bda9..4671e56 100644 --- a/src/review/tools/function-reference-search-tool.ts +++ b/src/review/tools/function-reference-search-tool.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { Tool } from './types'; import { SandboxExec } from '../context/sandbox-exec'; +import { Tool } from './types'; // 转义正则元字符,将identifier中的特殊字符转义为字面量 function escapeRegex(str: string): string { @@ -10,7 +10,8 @@ function escapeRegex(str: string): string { export function createFunctionReferenceSearchTool(sandbox: SandboxExec): Tool { return { name: 'search_function_references', - description: '搜索指定函数、方法或类的所有引用和定义(支持所有编程语言)。用于理解代码影响范围和调用关系。', + description: + '搜索指定函数、方法或类的所有引用和定义(支持所有编程语言)。用于理解代码影响范围和调用关系。', parameters: z.object({ identifier: z.string().describe('要搜索的标识符(函数名、类名、方法名等)'), file_types: z @@ -31,25 +32,25 @@ export function createFunctionReferenceSearchTool(sandbox: SandboxExec): Tool { // 定义调用模式(适配多种语言) const callPatterns: string[] = [ - `${escapedId}\\s*\\(`, // 直接调用: functionName( - `\\.${escapedId}\\s*\\(`, // 方法调用: obj.methodName( - `::${escapedId}\\s*\\(`, // C++/Rust静态调用: Class::method( - `${escapedId}\\s*<[^>]+>\\s*\\(`, // 泛型调用: functionName( (修复:限制<>内容) + `${escapedId}\\s*\\(`, // 直接调用: functionName( + `\\.${escapedId}\\s*\\(`, // 方法调用: obj.methodName( + `::${escapedId}\\s*\\(`, // C++/Rust静态调用: Class::method( + `${escapedId}\\s*<[^>]+>\\s*\\(`, // 泛型调用: functionName( (修复:限制<>内容) ]; // 定义声明模式(多语言) const definitionPatterns: string[] = [ - `func\\s+${escapedId}\\s*\\(`, // Go: func functionName( - `fn\\s+${escapedId}\\s*\\(`, // Rust: fn functionName( - `def\\s+${escapedId}\\s*\\(`, // Python: def functionName( - `function\\s+${escapedId}\\s*\\(`, // JavaScript: function functionName( - `${escapedId}\\s*:\\s*function`, // JS对象方法: methodName: function + `func\\s+${escapedId}\\s*\\(`, // Go: func functionName( + `fn\\s+${escapedId}\\s*\\(`, // Rust: fn functionName( + `def\\s+${escapedId}\\s*\\(`, // Python: def functionName( + `function\\s+${escapedId}\\s*\\(`, // JavaScript: function functionName( + `${escapedId}\\s*:\\s*function`, // JS对象方法: methodName: function `${escapedId}\\s*=\\s*\\([^)]*\\)\\s*=>`, // Arrow function: const fn = () => (修复:限制参数) - `class\\s+${escapedId}\\s*[{<]`, // 类定义: class ClassName { - `interface\\s+${escapedId}\\s*[{<]`, // 接口: interface InterfaceName { - `type\\s+${escapedId}\\s*=`, // 类型别名: type TypeName = - `struct\\s+${escapedId}\\s*[{]`, // Go/Rust struct: struct StructName { - `public\\s+[^(]*\\s+${escapedId}\\s*\\(`, // Java方法: public void methodName( + `class\\s+${escapedId}\\s*[{<]`, // 类定义: class ClassName { + `interface\\s+${escapedId}\\s*[{<]`, // 接口: interface InterfaceName { + `type\\s+${escapedId}\\s*=`, // 类型别名: type TypeName = + `struct\\s+${escapedId}\\s*[{]`, // Go/Rust struct: struct StructName { + `public\\s+[^(]*\\s+${escapedId}\\s*\\(`, // Java方法: public void methodName( `private\\s+[^(]*\\s+${escapedId}\\s*\\(`, // Java私有方法 ]; @@ -133,7 +134,7 @@ export function createFunctionReferenceSearchTool(sandbox: SandboxExec): Tool { } // 去重(同一位置可能同时匹配调用和定义模式) - const uniqueRefs = new Map(); + const uniqueRefs = new Map(); for (const ref of allReferences) { const key = `${ref.path}:${ref.line}`; if (!uniqueRefs.has(key)) { diff --git a/src/review/tools/registry.ts b/src/review/tools/registry.ts index a129659..066baca 100644 --- a/src/review/tools/registry.ts +++ b/src/review/tools/registry.ts @@ -1,6 +1,6 @@ +import { z } from 'zod'; import zodToJsonSchema from 'zod-to-json-schema'; import type { JsonSchema7Type } from 'zod-to-json-schema'; -import { z } from 'zod'; import { Tool } from './types'; export class ToolRegistry { diff --git a/src/review/types.ts b/src/review/types.ts index 16308bc..83ea55c 100644 --- a/src/review/types.ts +++ b/src/review/types.ts @@ -2,20 +2,11 @@ export type ReviewEngineMode = 'legacy' | 'agent'; export type ReviewEventType = 'pull_request' | 'commit_status'; -export type ReviewRunStatus = - | 'queued' - | 'in_progress' - | 'succeeded' - | 'failed' - | 'ignored'; +export type ReviewRunStatus = 'queued' | 'in_progress' | 'succeeded' | 'failed' | 'ignored'; export type FindingSeverity = 'high' | 'medium' | 'low'; -export type FindingCategory = - | 'correctness' - | 'security' - | 'reliability' - | 'maintainability'; +export type FindingCategory = 'correctness' | 'security' | 'reliability' | 'maintainability'; export interface ReviewRun { id: string; diff --git a/src/services/ai-review.ts b/src/services/ai-review.ts index d1d5931..5ff2109 100644 --- a/src/services/ai-review.ts +++ b/src/services/ai-review.ts @@ -1,7 +1,7 @@ import OpenAI from 'openai'; import config from '../config'; import { logger } from '../utils/logger'; -import { giteaService, PullRequestFile } from './gitea'; +import { PullRequestFile, giteaService } from './gitea'; // 创建OpenAI客户端 const openai = new OpenAI({ @@ -92,7 +92,7 @@ export const aiReviewService = { logger.warn('提交差异为空,无法进行代码审查'); return { summary: '提交差异为空,无法进行代码审查', - lineComments: [] + lineComments: [], }; } @@ -112,7 +112,7 @@ export const aiReviewService = { const context: ReviewContext = { changedFiles: files, fileContents, - diffContent + diffContent, }; // 使用上下文进行总体评价 @@ -153,7 +153,7 @@ export const aiReviewService = { return { changedFiles, fileContents, - diffContent + diffContent, }; } catch (error: any) { logger.error('获取审查上下文失败:', error); @@ -161,7 +161,7 @@ export const aiReviewService = { return { changedFiles: [], fileContents: {}, - diffContent + diffContent, }; } }, @@ -174,13 +174,13 @@ export const aiReviewService = { async generateSummary(context: ReviewContext): Promise { try { // 准备上下文信息 - const fileInfo = context.changedFiles.map(file => { + const fileInfo = context.changedFiles.map((file) => { return { path: file.filename, status: file.status, additions: file.additions, deletions: file.deletions, - content: context.fileContents[file.filename] || '无法获取文件内容' + content: context.fileContents[file.filename] || '无法获取文件内容', }; }); @@ -205,9 +205,10 @@ export const aiReviewService = { messages: [ { role: 'system', - content: '你是一个专业的代码审查助手,擅长识别代码中的严重问题和bug。你会查看代码的完整上下文,而不是为了评论而评论。如无明显问题,应给予简短肯定。' + content: + '你是一个专业的代码审查助手,擅长识别代码中的严重问题和bug。你会查看代码的完整上下文,而不是为了评论而评论。如无明显问题,应给予简短肯定。', }, - { role: 'user', content: summaryPrompt } + { role: 'user', content: summaryPrompt }, ], temperature: 0.1, }); @@ -234,7 +235,7 @@ export const aiReviewService = { // 对每个文件的变更行进行审查 for (const file of diffFiles) { // 只对添加的行进行评论 - const addedLines = file.changes.filter(change => change.type === 'add'); + const addedLines = file.changes.filter((change) => change.type === 'add'); if (addedLines.length === 0) continue; // 获取文件的完整内容作为上下文 @@ -257,7 +258,7 @@ export const aiReviewService = { ${fileContent} 变更部分上下文: - ${file.changes.map(c => `${c.lineNumber}: ${c.content} (${c.type === 'add' ? '新增' : '上下文'})`).join('\n')} + ${file.changes.map((c) => `${c.lineNumber}: ${c.content} (${c.type === 'add' ? '新增' : '上下文'})`).join('\n')} 请以JSON格式返回评论,格式如下: [ @@ -276,9 +277,10 @@ export const aiReviewService = { messages: [ { role: 'system', - content: '你是一个谨慎的代码审查助手,只对有明显bug或严重问题的代码行提供评论。大多数情况下,如果代码没有严重问题,你应该返回空数组。请以JSON格式返回结果。' + content: + '你是一个谨慎的代码审查助手,只对有明显bug或严重问题的代码行提供评论。大多数情况下,如果代码没有严重问题,你应该返回空数组。请以JSON格式返回结果。', }, - { role: 'user', content: filePrompt } + { role: 'user', content: filePrompt }, ], temperature: 0.1, response_format: { type: 'json_object' }, @@ -290,7 +292,9 @@ export const aiReviewService = { try { // 解析JSON响应 const responseObject = JSON.parse(content); - const comments = Array.isArray(responseObject) ? responseObject : (responseObject.comments || []); + const comments = Array.isArray(responseObject) + ? responseObject + : responseObject.comments || []; // 添加到结果中 for (const comment of comments) { @@ -298,7 +302,7 @@ export const aiReviewService = { lineComments.push({ path: file.path, line: comment.line, - comment: comment.comment + comment: comment.comment, }); } } @@ -321,17 +325,17 @@ export const aiReviewService = { */ parseDiff(diffContent: string): Array<{ path: string; - changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }> + changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }>; }> { const files: Array<{ path: string; - changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }> + changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }>; }> = []; const diffLines = diffContent.split('\n'); let currentFile: { path: string; - changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }> + changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }>; } | null = null; let lineNumber = 0; @@ -355,8 +359,8 @@ export const aiReviewService = { // Hunk头,记录起始行号 else if (line.startsWith('@@')) { const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/); - if (match && match[1]) { - lineNumber = parseInt(match[1], 10) - 1; // 因为下面会+1 + if (match?.[1]) { + lineNumber = Number.parseInt(match[1], 10) - 1; // 因为下面会+1 inHunk = true; } } @@ -368,7 +372,7 @@ export const aiReviewService = { currentFile.changes.push({ lineNumber, content: line.substring(1), - type: 'add' + type: 'add', }); } else if (line.startsWith(' ')) { // 上下文行 @@ -376,7 +380,7 @@ export const aiReviewService = { currentFile.changes.push({ lineNumber, content: line.substring(1), - type: 'context' + type: 'context', }); } else if (line.startsWith('-')) { // 删除的行,不增加行号 @@ -394,5 +398,5 @@ export const aiReviewService = { } return files; - } + }, }; diff --git a/src/services/feishu.ts b/src/services/feishu.ts index 4ba9a63..e5a2958 100644 --- a/src/services/feishu.ts +++ b/src/services/feishu.ts @@ -1,6 +1,6 @@ -import { logger } from '../utils/logger'; +import * as crypto from 'node:crypto'; import config from '../config'; -import * as crypto from 'crypto'; +import { logger } from '../utils/logger'; export class FeishuService { private webhookUrl: string; @@ -42,14 +42,14 @@ export class FeishuService { const message: any = { msg_type: 'text', content: { - text: content - } + text: content, + }, }; // 如果需要@用户,添加at信息 if (usernames.length > 0) { message.content.text += '\n'; - usernames.forEach(username => { + usernames.forEach((username) => { message.content.text += `@${username} `; }); } @@ -85,7 +85,11 @@ export class FeishuService { * @param issueUrl 工单链接 * @param assigneeUsernames 被指派人用户名列表 */ - async sendIssueCreatedNotification(issueTitle: string, issueUrl: string, assigneeUsernames: string[]): Promise { + async sendIssueCreatedNotification( + issueTitle: string, + issueUrl: string, + assigneeUsernames: string[] + ): Promise { const content = `📝 新工单已创建\n标题: ${issueTitle}\n链接: ${issueUrl}`; await this.sendMessage(content, assigneeUsernames); } @@ -96,7 +100,11 @@ export class FeishuService { * @param issueUrl 工单链接 * @param creatorUsername 创建者用户名 */ - async sendIssueClosedNotification(issueTitle: string, issueUrl: string, creatorUsername: string): Promise { + async sendIssueClosedNotification( + issueTitle: string, + issueUrl: string, + creatorUsername: string + ): Promise { const content = `✅ 工单已关闭\n标题: ${issueTitle}\n链接: ${issueUrl}`; await this.sendMessage(content, [creatorUsername]); } @@ -107,7 +115,11 @@ export class FeishuService { * @param issueUrl 工单链接 * @param assigneeUsernames 被指派人用户名列表 */ - async sendIssueAssignedNotification(issueTitle: string, issueUrl: string, assigneeUsernames: string[]): Promise { + async sendIssueAssignedNotification( + issueTitle: string, + issueUrl: string, + assigneeUsernames: string[] + ): Promise { const content = `👤 工单已指派给你\n标题: ${issueTitle}\n链接: ${issueUrl}`; await this.sendMessage(content, assigneeUsernames); } @@ -118,7 +130,11 @@ export class FeishuService { * @param prUrl PR链接 * @param reviewerUsernames 审阅者用户名列表 */ - async sendPrCreatedNotification(prTitle: string, prUrl: string, reviewerUsernames: string[]): Promise { + async sendPrCreatedNotification( + prTitle: string, + prUrl: string, + reviewerUsernames: string[] + ): Promise { const content = `🔄 新PR等待你审阅\n标题: ${prTitle}\n链接: ${prUrl}`; await this.sendMessage(content, reviewerUsernames); } @@ -129,7 +145,11 @@ export class FeishuService { * @param prUrl PR链接 * @param reviewerUsernames 审阅者用户名列表 */ - async sendPrReviewerAssignedNotification(prTitle: string, prUrl: string, reviewerUsernames: string[]): Promise { + async sendPrReviewerAssignedNotification( + prTitle: string, + prUrl: string, + reviewerUsernames: string[] + ): Promise { const content = `👀 你被指定为PR审阅者\n标题: ${prTitle}\n链接: ${prUrl}`; await this.sendMessage(content, reviewerUsernames); } diff --git a/src/services/gitea.ts b/src/services/gitea.ts index bab222c..28dea13 100644 --- a/src/services/gitea.ts +++ b/src/services/gitea.ts @@ -12,7 +12,7 @@ export interface LineComment { const giteaClient = axios.create({ baseURL: config.gitea.apiUrl, headers: { - 'Authorization': `token ${config.gitea.accessToken}`, + Authorization: `token ${config.gitea.accessToken}`, 'Content-Type': 'application/json', }, }); @@ -21,7 +21,7 @@ const giteaClient = axios.create({ const giteaAdminClient = axios.create({ baseURL: config.gitea.apiUrl, headers: { - 'Authorization': `token ${config.admin.giteaAdminToken || config.gitea.accessToken}`, + Authorization: `token ${config.admin.giteaAdminToken || config.gitea.accessToken}`, 'Content-Type': 'application/json', 'User-Agent': 'curl/7.81.0', // 伪装成 curl }, @@ -46,13 +46,22 @@ export interface GiteaService { getCommitFiles(owner: string, repo: string, commitSha: string): Promise; // 获取与提交关联的Pull Request - getRelatedPullRequest(owner: string, repo: string, commitSha: string): Promise; + getRelatedPullRequest( + owner: string, + repo: string, + commitSha: string + ): Promise; // 获取文件内容 getFileContent(owner: string, repo: string, path: string, ref?: string): Promise; // 获取引用的相关文件 - getRelatedFiles(owner: string, repo: string, files: PullRequestFile[], commitSha: string): Promise>; + getRelatedFiles( + owner: string, + repo: string, + files: PullRequestFile[], + commitSha: string + ): Promise>; // 添加PR评论 addPullRequestComment(owner: string, repo: string, prNumber: number, body: string): Promise; @@ -70,7 +79,11 @@ export interface GiteaService { addCommitComment(owner: string, repo: string, commitSha: string, body: string): Promise; // 管理后台方法 - listAllRepositories(page: number, limit: number, query?: string): Promise<{ repos: any[], totalCount: number }>; + listAllRepositories( + page: number, + limit: number, + query?: string + ): Promise<{ repos: any[]; totalCount: number }>; listWebhooks(owner: string, repo: string): Promise; createWebhook(owner: string, repo: string, webhookUrl: string): Promise; deleteWebhook(owner: string, repo: string, hookId: number): Promise; @@ -118,7 +131,11 @@ export const giteaService: GiteaService = { }, // 获取PR详情 - async getPullRequestDetails(owner: string, repo: string, prNumber: number): Promise { + async getPullRequestDetails( + owner: string, + repo: string, + prNumber: number + ): Promise { try { const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls/${prNumber}`); return response.data; @@ -129,7 +146,11 @@ export const giteaService: GiteaService = { }, // 获取PR变更的文件列表 - async getPullRequestFiles(owner: string, repo: string, prNumber: number): Promise { + async getPullRequestFiles( + owner: string, + repo: string, + prNumber: number + ): Promise { try { const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls/${prNumber}/files`); return response.data || []; @@ -153,7 +174,9 @@ export const giteaService: GiteaService = { } // 使用官方API获取差异,使用diff格式 - const diffResponse = await giteaClient.get(`/repos/${owner}/${repo}/git/commits/${commitSha}.diff`); + const diffResponse = await giteaClient.get( + `/repos/${owner}/${repo}/git/commits/${commitSha}.diff` + ); return diffResponse.data || ''; } catch (error: any) { logger.error('获取提交差异失败:', error); @@ -175,10 +198,9 @@ export const giteaService: GiteaService = { if (response.data.files) { // 如果API返回了文件列表,则使用它 return response.data.files; - } else { - // 否则返回空数组,依赖控制器中webhook提供的文件列表 - return []; } + // 否则返回空数组,依赖控制器中webhook提供的文件列表 + return []; } catch (error: any) { logger.error('获取提交文件列表失败:', error); throw new Error(`获取提交文件列表失败: ${error.message}`); @@ -186,7 +208,11 @@ export const giteaService: GiteaService = { }, // 获取与提交关联的Pull Request - async getRelatedPullRequest(owner: string, repo: string, commitSha: string): Promise { + async getRelatedPullRequest( + owner: string, + repo: string, + commitSha: string + ): Promise { try { // 获取仓库中所有开放的PR const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls?state=open`); @@ -198,7 +224,9 @@ export const giteaService: GiteaService = { const prDetails = await giteaService.getPullRequestDetails(owner, repo, pr.number); // 检查PR的提交列表 - const commitsResponse = await giteaClient.get(`/repos/${owner}/${repo}/pulls/${pr.number}/commits`); + const commitsResponse = await giteaClient.get( + `/repos/${owner}/${repo}/pulls/${pr.number}/commits` + ); const commits = commitsResponse.data || []; // 检查提交是否在PR中 @@ -239,7 +267,12 @@ export const giteaService: GiteaService = { }, // 获取引用的相关文件 - async getRelatedFiles(owner: string, repo: string, files: PullRequestFile[], commitSha: string): Promise> { + async getRelatedFiles( + owner: string, + repo: string, + files: PullRequestFile[], + commitSha: string + ): Promise> { const result: Record = {}; // 对每个修改过的文件,获取其完整内容 @@ -261,7 +294,12 @@ export const giteaService: GiteaService = { }, // 添加PR评论 - async addPullRequestComment(owner: string, repo: string, prNumber: number, body: string): Promise { + async addPullRequestComment( + owner: string, + repo: string, + prNumber: number, + body: string + ): Promise { try { await giteaClient.post(`/repos/${owner}/${repo}/issues/${prNumber}/comments`, { body }); } catch (error: any) { @@ -288,7 +326,7 @@ export const giteaService: GiteaService = { await giteaClient.post(`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, { event: 'COMMENT', commit_id: commitId, - comments: comments.map(comment => ({ + comments: comments.map((comment) => ({ path: comment.path, body: comment.comment, new_position: comment.line, @@ -308,7 +346,7 @@ export const giteaService: GiteaService = { commit_id: commitId, path: comment.path, line: comment.line, - position: comment.line, // Gitea使用position参数表示行号 + position: comment.line, // Gitea使用position参数表示行号 }); } logger.info(`成功逐条添加 ${comments.length} 条评论`); @@ -320,7 +358,12 @@ export const giteaService: GiteaService = { }, // 添加提交评论 - async addCommitComment(owner: string, repo: string, commitSha: string, body: string): Promise { + async addCommitComment( + owner: string, + repo: string, + commitSha: string, + body: string + ): Promise { try { await giteaClient.post(`/repos/${owner}/${repo}/git/commits/${commitSha}/comments`, { body }); } catch (error: any) { @@ -330,7 +373,11 @@ export const giteaService: GiteaService = { }, // 获取所有仓库 - async listAllRepositories(page: number = 1, limit: number = 30, query?: string): Promise<{ repos: any[], totalCount: number }> { + async listAllRepositories( + page = 1, + limit = 30, + query?: string + ): Promise<{ repos: any[]; totalCount: number }> { try { const response = await giteaAdminClient.get('/repos/search', { params: { @@ -339,7 +386,7 @@ export const giteaService: GiteaService = { q: query, }, }); - const totalCount = parseInt(response.headers['x-total-count'] || '0', 10); + const totalCount = Number.parseInt(response.headers['x-total-count'] || '0', 10); return { repos: response.data.data, totalCount }; } catch (error: any) { logger.error('获取所有仓库列表失败:', error); diff --git a/src/utils/logger.ts b/src/utils/logger.ts index 1d39edc..21c4b45 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -28,7 +28,7 @@ function formatMessage(level: LogLevel, message: string, meta?: any): string { if (meta) { try { formattedMessage += ` - ${JSON.stringify(meta)}`; - } catch (error) { + } catch (_error) { formattedMessage += ` - ${meta}`; } }