mirror of
https://github.com/jeffusion/gitea-ai-assistant.git
synced 2026-03-27 10:05:50 +00:00
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
This commit is contained in:
50
biome.json
Normal file
50
biome.json
Normal file
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
96
bun.lock
96
bun.lock
@@ -15,18 +15,32 @@
|
|||||||
"zod-to-json-schema": "^3.25.1",
|
"zod-to-json-schema": "^3.25.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"tslint": "^6.1.3",
|
|
||||||
"typescript": "^5.8.2",
|
"typescript": "^5.8.2",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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=="],
|
"@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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||||
|
|
||||||
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
"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=="],
|
"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=="],
|
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||||
|
|
||||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="],
|
||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"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=="],
|
"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=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-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=="],
|
"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=="],
|
"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=="],
|
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"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=="],
|
|
||||||
|
|
||||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"@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=="],
|
"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=="],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
"zod-to-json-schema": "^3.25.1"
|
"zod-to-json-schema": "^3.25.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^22.13.10",
|
||||||
"concurrently": "^9.2.1",
|
"concurrently": "^9.2.1",
|
||||||
"tslint": "^6.1.3",
|
|
||||||
"typescript": "^5.8.2"
|
"typescript": "^5.8.2"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"build": "rm -rf dist && tsc",
|
"build": "rm -rf dist && tsc",
|
||||||
"start": "bun run src/index.ts",
|
"start": "bun run src/index.ts",
|
||||||
"start:prod": "bun run dist/index.js",
|
"start:prod": "bun run dist/index.js",
|
||||||
"lint": "tslint -c tslint.json src/**/*.ts",
|
"lint": "biome check src/",
|
||||||
"test": "bun test"
|
"test": "bun test"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -11,27 +11,48 @@ declare module 'bun:test' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @ts-expect-error bun:test is provided by Bun at runtime
|
// @ts-expect-error bun:test is provided by Bun at runtime
|
||||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||||
import { join } from 'node:path';
|
|
||||||
import { tmpdir } from 'node:os';
|
|
||||||
import { unlink, readFile } from 'node:fs/promises';
|
|
||||||
import { randomUUID } from 'node:crypto';
|
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';
|
import type { AppConfig } from '../config-manager';
|
||||||
|
|
||||||
// ── All env keys in the Zod schema ──────────────────────────────────────────
|
// ── All env keys in the Zod schema ──────────────────────────────────────────
|
||||||
const SCHEMA_KEYS = [
|
const SCHEMA_KEYS = [
|
||||||
'GITEA_API_URL', 'GITEA_ACCESS_TOKEN', 'GITEA_ADMIN_TOKEN',
|
'GITEA_API_URL',
|
||||||
'OPENAI_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_MODEL',
|
'GITEA_ACCESS_TOKEN',
|
||||||
'CUSTOM_SUMMARY_PROMPT', 'CUSTOM_LINE_COMMENT_PROMPT',
|
'GITEA_ADMIN_TOKEN',
|
||||||
'FEISHU_WEBHOOK_URL', 'FEISHU_WEBHOOK_SECRET',
|
'OPENAI_BASE_URL',
|
||||||
'PORT', 'WEBHOOK_SECRET', 'ADMIN_PASSWORD', 'JWT_SECRET',
|
'OPENAI_API_KEY',
|
||||||
'REVIEW_ENGINE', 'REVIEW_WORKDIR', 'REVIEW_MODEL_PLANNER',
|
'OPENAI_MODEL',
|
||||||
'REVIEW_MODEL_SPECIALIST', 'REVIEW_MODEL_JUDGE',
|
'CUSTOM_SUMMARY_PROMPT',
|
||||||
'REVIEW_MAX_PARALLEL_RUNS', 'REVIEW_MAX_FILES_PER_RUN',
|
'CUSTOM_LINE_COMMENT_PROMPT',
|
||||||
'REVIEW_MAX_FILE_CONTENT_CHARS', 'REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE',
|
'FEISHU_WEBHOOK_URL',
|
||||||
'REVIEW_ENABLE_HUMAN_GATE', 'REVIEW_ALLOWED_COMMANDS', 'REVIEW_COMMAND_TIMEOUT_MS',
|
'FEISHU_WEBHOOK_SECRET',
|
||||||
'QDRANT_URL', 'ENABLE_MEMORY', 'FEW_SHOT_EXAMPLES_COUNT',
|
'PORT',
|
||||||
'ENABLE_REFLECTION', 'MAX_REFLECTION_ROUNDS', 'ENABLE_DEBATE', 'DEBATE_THRESHOLD',
|
'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;
|
] as const;
|
||||||
|
|
||||||
const CONTROL_KEYS = ['CONFIG_OVERRIDES_PATH', 'NODE_ENV'] as const;
|
const CONTROL_KEYS = ['CONFIG_OVERRIDES_PATH', 'NODE_ENV'] as const;
|
||||||
@@ -80,7 +101,11 @@ describe('ConfigManager', () => {
|
|||||||
process.env[key] = savedEnv[key]!;
|
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 ─────────────────────────
|
// ─── 1. Layering: defaults < env < override ─────────────────────────
|
||||||
|
|||||||
@@ -8,12 +8,12 @@
|
|||||||
* Bun-friendly IO: reads via readFile, writes atomically via temp+rename.
|
* 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 { 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 { config as dotenvConfig } from 'dotenv';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
// Load .env before any process.env access (must precede singleton construction)
|
// Load .env before any process.env access (must precede singleton construction)
|
||||||
dotenvConfig();
|
dotenvConfig();
|
||||||
@@ -79,7 +79,7 @@ const envSchema = z.object({
|
|||||||
// Memory & learning
|
// Memory & learning
|
||||||
QDRANT_URL: z.preprocess(
|
QDRANT_URL: z.preprocess(
|
||||||
(val) => (typeof val === 'string' && val.trim() === '' ? undefined : val),
|
(val) => (typeof val === 'string' && val.trim() === '' ? undefined : val),
|
||||||
z.string().url().optional(),
|
z.string().url().optional()
|
||||||
),
|
),
|
||||||
ENABLE_MEMORY: z
|
ENABLE_MEMORY: z
|
||||||
.enum(['true', 'false'])
|
.enum(['true', 'false'])
|
||||||
@@ -204,7 +204,6 @@ const DEV_FALLBACK_CONFIG: AppConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ConfigManager
|
// ConfigManager
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -394,7 +393,6 @@ class ConfigManager {
|
|||||||
}
|
}
|
||||||
return 'default';
|
return 'default';
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -193,7 +193,8 @@ export const CONFIG_FIELDS: ConfigFieldMeta[] = [
|
|||||||
envKey: 'WEBHOOK_SECRET',
|
envKey: 'WEBHOOK_SECRET',
|
||||||
group: 'app',
|
group: 'app',
|
||||||
label: 'Webhook 密钥',
|
label: 'Webhook 密钥',
|
||||||
description: '用于验证 Gitea Webhook 请求来源的 HMAC 密钥,修改需通过 .env 配置并同步更新 Gitea',
|
description:
|
||||||
|
'用于验证 Gitea Webhook 请求来源的 HMAC 密钥,修改需通过 .env 配置并同步更新 Gitea',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
sensitive: true,
|
sensitive: true,
|
||||||
readonly: true,
|
readonly: true,
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { sign } from 'hono/jwt';
|
import { sign } from 'hono/jwt';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
import { reviewEngine } from '../review/engine';
|
||||||
import { giteaService } from '../services/gitea';
|
import { giteaService } from '../services/gitea';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { reviewEngine } from '../review/engine';
|
|
||||||
|
|
||||||
const publicRoutes = new Hono();
|
const publicRoutes = new Hono();
|
||||||
const protectedRoutes = new Hono();
|
const protectedRoutes = new Hono();
|
||||||
@@ -26,13 +26,12 @@ publicRoutes.post('/login', async (c) => {
|
|||||||
return c.json({ message: 'Invalid credentials' }, 401);
|
return c.json({ message: 'Invalid credentials' }, 401);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
// --- Protected Routes ---
|
// --- Protected Routes ---
|
||||||
|
|
||||||
// 获取仓库列表及 Webhook 状态
|
// 获取仓库列表及 Webhook 状态
|
||||||
protectedRoutes.get('/repositories', async (c) => {
|
protectedRoutes.get('/repositories', async (c) => {
|
||||||
try {
|
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 query = c.req.query('q');
|
||||||
const limit = 30; // 每页数量固定,或也可从查询参数获取
|
const limit = 30; // 每页数量固定,或也可从查询参数获取
|
||||||
|
|
||||||
@@ -43,7 +42,7 @@ protectedRoutes.get('/repositories', async (c) => {
|
|||||||
repos.map(async (repo) => {
|
repos.map(async (repo) => {
|
||||||
const [owner, repoName] = repo.full_name.split('/');
|
const [owner, repoName] = repo.full_name.split('/');
|
||||||
const hooks = await giteaService.listWebhooks(owner, repoName);
|
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 {
|
return {
|
||||||
name: repo.full_name,
|
name: repo.full_name,
|
||||||
webhook_status: webhook ? 'active' : 'inactive',
|
webhook_status: webhook ? 'active' : 'inactive',
|
||||||
@@ -67,7 +66,7 @@ protectedRoutes.get('/repositories', async (c) => {
|
|||||||
// 创建 Webhook
|
// 创建 Webhook
|
||||||
protectedRoutes.post('/repositories/:owner/:repo/webhook', async (c) => {
|
protectedRoutes.post('/repositories/:owner/:repo/webhook', async (c) => {
|
||||||
const { owner, repo } = c.req.param();
|
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 {
|
try {
|
||||||
await giteaService.createWebhook(owner, repo, webhookUrl);
|
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();
|
const { owner, repo, hookId } = c.req.param();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await giteaService.deleteWebhook(owner, repo, parseInt(hookId, 10));
|
await giteaService.deleteWebhook(owner, repo, Number.parseInt(hookId, 10));
|
||||||
return c.json({ success: true });
|
return c.json({ success: true });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(`删除 ${owner}/${repo} 的 Webhook 失败:`, error);
|
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) => {
|
protectedRoutes.get('/review/runs', async (c) => {
|
||||||
try {
|
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);
|
const runs = await reviewEngine.listRuns(limit);
|
||||||
return c.json({ data: runs });
|
return c.json({ data: runs });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Hono } from 'hono';
|
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 { CONFIG_FIELDS, CONFIG_GROUPS, type ConfigFieldMeta } from '../config/config-schema';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
@@ -19,9 +19,7 @@ const INTEGER_FIELDS = new Set([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
/** Fast lookup from envKey → field metadata. */
|
/** Fast lookup from envKey → field metadata. */
|
||||||
const FIELDS_MAP = new Map<string, ConfigFieldMeta>(
|
const FIELDS_MAP = new Map<string, ConfigFieldMeta>(CONFIG_FIELDS.map((f) => [f.envKey, f]));
|
||||||
CONFIG_FIELDS.map((f) => [f.envKey, f]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -31,50 +29,84 @@ const FIELDS_MAP = new Map<string, ConfigFieldMeta>(
|
|||||||
*/
|
*/
|
||||||
function getEffectiveValue(
|
function getEffectiveValue(
|
||||||
envKey: string,
|
envKey: string,
|
||||||
current: AppConfig,
|
current: AppConfig
|
||||||
): string | number | boolean | undefined {
|
): string | number | boolean | undefined {
|
||||||
switch (envKey) {
|
switch (envKey) {
|
||||||
// Gitea
|
// Gitea
|
||||||
case 'GITEA_API_URL': return current.gitea.apiUrl;
|
case 'GITEA_API_URL':
|
||||||
case 'GITEA_ACCESS_TOKEN': return current.gitea.accessToken;
|
return current.gitea.apiUrl;
|
||||||
case 'GITEA_ADMIN_TOKEN': return current.admin.giteaAdminToken;
|
case 'GITEA_ACCESS_TOKEN':
|
||||||
|
return current.gitea.accessToken;
|
||||||
|
case 'GITEA_ADMIN_TOKEN':
|
||||||
|
return current.admin.giteaAdminToken;
|
||||||
// OpenAI
|
// OpenAI
|
||||||
case 'OPENAI_BASE_URL': return current.openai.baseUrl;
|
case 'OPENAI_BASE_URL':
|
||||||
case 'OPENAI_API_KEY': return current.openai.apiKey;
|
return current.openai.baseUrl;
|
||||||
case 'OPENAI_MODEL': return current.openai.model;
|
case 'OPENAI_API_KEY':
|
||||||
case 'CUSTOM_SUMMARY_PROMPT': return current.openai.customSummaryPrompt;
|
return current.openai.apiKey;
|
||||||
case 'CUSTOM_LINE_COMMENT_PROMPT': return current.openai.customLineCommentPrompt;
|
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
|
// Feishu
|
||||||
case 'FEISHU_WEBHOOK_URL': return current.feishu.webhookUrl;
|
case 'FEISHU_WEBHOOK_URL':
|
||||||
case 'FEISHU_WEBHOOK_SECRET': return current.feishu.webhookSecret;
|
return current.feishu.webhookUrl;
|
||||||
|
case 'FEISHU_WEBHOOK_SECRET':
|
||||||
|
return current.feishu.webhookSecret;
|
||||||
// App
|
// App
|
||||||
case 'PORT': return current.app.port;
|
case 'PORT':
|
||||||
case 'WEBHOOK_SECRET': return current.app.webhookSecret;
|
return current.app.port;
|
||||||
|
case 'WEBHOOK_SECRET':
|
||||||
|
return current.app.webhookSecret;
|
||||||
// Admin
|
// Admin
|
||||||
case 'ADMIN_PASSWORD': return current.admin.password;
|
case 'ADMIN_PASSWORD':
|
||||||
case 'JWT_SECRET': return current.admin.jwtSecret;
|
return current.admin.password;
|
||||||
|
case 'JWT_SECRET':
|
||||||
|
return current.admin.jwtSecret;
|
||||||
// Review
|
// Review
|
||||||
case 'REVIEW_ENGINE': return current.review.engine;
|
case 'REVIEW_ENGINE':
|
||||||
case 'REVIEW_WORKDIR': return current.review.workdir;
|
return current.review.engine;
|
||||||
case 'REVIEW_MODEL_PLANNER': return current.review.modelPlanner;
|
case 'REVIEW_WORKDIR':
|
||||||
case 'REVIEW_MODEL_SPECIALIST': return current.review.modelSpecialist;
|
return current.review.workdir;
|
||||||
case 'REVIEW_MODEL_JUDGE': return current.review.modelJudge;
|
case 'REVIEW_MODEL_PLANNER':
|
||||||
case 'REVIEW_MAX_PARALLEL_RUNS': return current.review.maxParallelRuns;
|
return current.review.modelPlanner;
|
||||||
case 'REVIEW_MAX_FILES_PER_RUN': return current.review.maxFilesPerRun;
|
case 'REVIEW_MODEL_SPECIALIST':
|
||||||
case 'REVIEW_MAX_FILE_CONTENT_CHARS': return current.review.maxFileContentChars;
|
return current.review.modelSpecialist;
|
||||||
case 'REVIEW_AUTO_PUBLISH_MIN_CONFIDENCE': return current.review.autoPublishMinConfidence;
|
case 'REVIEW_MODEL_JUDGE':
|
||||||
case 'REVIEW_ENABLE_HUMAN_GATE': return current.review.enableHumanGate;
|
return current.review.modelJudge;
|
||||||
case 'REVIEW_ALLOWED_COMMANDS': return current.review.allowedCommands.join(',');
|
case 'REVIEW_MAX_PARALLEL_RUNS':
|
||||||
case 'REVIEW_COMMAND_TIMEOUT_MS': return current.review.commandTimeoutMs;
|
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
|
// Memory
|
||||||
case 'QDRANT_URL': return current.review.qdrantUrl;
|
case 'QDRANT_URL':
|
||||||
case 'ENABLE_MEMORY': return current.review.enableMemory;
|
return current.review.qdrantUrl;
|
||||||
case 'FEW_SHOT_EXAMPLES_COUNT': return current.review.fewShotExamplesCount;
|
case 'ENABLE_MEMORY':
|
||||||
case 'ENABLE_REFLECTION': return current.review.enableReflection;
|
return current.review.enableMemory;
|
||||||
case 'MAX_REFLECTION_ROUNDS': return current.review.maxReflectionRounds;
|
case 'FEW_SHOT_EXAMPLES_COUNT':
|
||||||
case 'ENABLE_DEBATE': return current.review.enableDebate;
|
return current.review.fewShotExamplesCount;
|
||||||
case 'DEBATE_THRESHOLD': return current.review.debateThreshold;
|
case 'ENABLE_REFLECTION':
|
||||||
default: return undefined;
|
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': {
|
case 'number': {
|
||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
if (isNaN(num)) {
|
if (Number.isNaN(num)) {
|
||||||
return `${field.label}(${key})必须是有效的数字`;
|
return `${field.label}(${key})必须是有效的数字`;
|
||||||
}
|
}
|
||||||
if (INTEGER_FIELDS.has(key) && !Number.isInteger(num)) {
|
if (INTEGER_FIELDS.has(key) && !Number.isInteger(num)) {
|
||||||
@@ -255,7 +287,7 @@ configRouter.post('/reset', async (c) => {
|
|||||||
if (unknownKeys.length > 0) {
|
if (unknownKeys.length > 0) {
|
||||||
return c.json(
|
return c.json(
|
||||||
{ message: '保存配置失败', error: `未知配置项: ${unknownKeys.join(', ')}` },
|
{ message: '保存配置失败', error: `未知配置项: ${unknownKeys.join(', ')}` },
|
||||||
400,
|
400
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Hono } from 'hono';
|
|
||||||
import { zValidator } from '@hono/zod-validator';
|
import { zValidator } from '@hono/zod-validator';
|
||||||
import { z } from 'zod';
|
import { Hono } from 'hono';
|
||||||
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 OpenAI from 'openai';
|
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();
|
const feedbackRouter = new Hono();
|
||||||
|
|
||||||
@@ -68,7 +68,10 @@ feedbackRouter.post(
|
|||||||
// 原子幂等性保护:先标记finding为published(原子check-and-set)
|
// 原子幂等性保护:先标记finding为published(原子check-and-set)
|
||||||
// 只有第一个请求会得到true,后续并发/重试请求会得到false
|
// 只有第一个请求会得到true,后续并发/重试请求会得到false
|
||||||
// 这解决了read-check-write竞态:两个并发请求不会都发布评论
|
// 这解决了read-check-write竞态:两个并发请求不会都发布评论
|
||||||
const wasUnpublished = await reviewStore.markFindingPublished(finding.runId, finding.fingerprint);
|
const wasUnpublished = await reviewStore.markFindingPublished(
|
||||||
|
finding.runId,
|
||||||
|
finding.fingerprint
|
||||||
|
);
|
||||||
|
|
||||||
if (!wasUnpublished) {
|
if (!wasUnpublished) {
|
||||||
// finding已被标记为published,但需验证是否真的发布成功
|
// finding已被标记为published,但需验证是否真的发布成功
|
||||||
@@ -76,7 +79,7 @@ feedbackRouter.post(
|
|||||||
// 检查是否存在已发布的comment记录来确认真实状态
|
// 检查是否存在已发布的comment记录来确认真实状态
|
||||||
// 关键:必须通过fingerprint匹配,而非仅path+line,以区分同一位置的不同findings
|
// 关键:必须通过fingerprint匹配,而非仅path+line,以区分同一位置的不同findings
|
||||||
const publishedComment = runDetails.comments.find(
|
const publishedComment = runDetails.comments.find(
|
||||||
c => c.status === 'published' && c.fingerprint === finding.fingerprint
|
(c) => c.status === 'published' && c.fingerprint === finding.fingerprint
|
||||||
);
|
);
|
||||||
|
|
||||||
if (publishedComment) {
|
if (publishedComment) {
|
||||||
@@ -88,15 +91,17 @@ feedbackRouter.post(
|
|||||||
learningApplied: false,
|
learningApplied: false,
|
||||||
published: true,
|
published: true,
|
||||||
});
|
});
|
||||||
} else {
|
}
|
||||||
// published标记存在但无published comment记录
|
// published标记存在但无published comment记录
|
||||||
// 可能原因:1) 并发请求正在发布中 2) 之前发布失败并回滚
|
// 可能原因:1) 并发请求正在发布中 2) 之前发布失败并回滚
|
||||||
// 不能声称成功,返回错误让用户稍后重试
|
// 不能声称成功,返回错误让用户稍后重试
|
||||||
return c.json({
|
return c.json(
|
||||||
|
{
|
||||||
error: 'Finding approval in progress or previously failed. Please retry in a moment.',
|
error: 'Finding approval in progress or previously failed. Please retry in a moment.',
|
||||||
inProgress: true,
|
inProgress: true,
|
||||||
}, 409); // 409 Conflict
|
},
|
||||||
}
|
409
|
||||||
|
); // 409 Conflict
|
||||||
}
|
}
|
||||||
|
|
||||||
// 以下代码只会被第一个请求执行(wasUnpublished=true)
|
// 以下代码只会被第一个请求执行(wasUnpublished=true)
|
||||||
@@ -111,7 +116,12 @@ feedbackRouter.post(
|
|||||||
if (approved) {
|
if (approved) {
|
||||||
await learningSystem.learnFromApproval(finding, owner, repo);
|
await learningSystem.learnFromApproval(finding, owner, repo);
|
||||||
} else {
|
} else {
|
||||||
await learningSystem.learnFromFalsePositive(finding, reason || '人工标记为误报', owner, repo);
|
await learningSystem.learnFromFalsePositive(
|
||||||
|
finding,
|
||||||
|
reason || '人工标记为误报',
|
||||||
|
owner,
|
||||||
|
repo
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
learningApplied = true;
|
learningApplied = true;
|
||||||
@@ -146,19 +156,9 @@ _此问题已通过人工审批确认_`;
|
|||||||
// 2. 再写本地record,失败不回滚(因为Gitea已成功,重试不应重复发布)
|
// 2. 再写本地record,失败不回滚(因为Gitea已成功,重试不应重复发布)
|
||||||
try {
|
try {
|
||||||
if (runDetails.run.eventType === 'pull_request' && runDetails.run.prNumber) {
|
if (runDetails.run.eventType === 'pull_request' && runDetails.run.prNumber) {
|
||||||
await giteaService.addPullRequestComment(
|
await giteaService.addPullRequestComment(owner, repo, runDetails.run.prNumber, comment);
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
runDetails.run.prNumber,
|
|
||||||
comment
|
|
||||||
);
|
|
||||||
} else if (runDetails.run.commitSha) {
|
} else if (runDetails.run.commitSha) {
|
||||||
await giteaService.addCommitComment(
|
await giteaService.addCommitComment(owner, repo, runDetails.run.commitSha, comment);
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
runDetails.run.commitSha,
|
|
||||||
comment
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (giteaError) {
|
} catch (giteaError) {
|
||||||
// Gitea API失败:回滚published状态,允许用户重试发布
|
// Gitea API失败:回滚published状态,允许用户重试发布
|
||||||
@@ -181,7 +181,10 @@ _此问题已通过人工审批确认_`;
|
|||||||
} catch (storeError) {
|
} catch (storeError) {
|
||||||
// 本地store失败:回滚published标记,允许用户重试
|
// 本地store失败:回滚published标记,允许用户重试
|
||||||
// 如果用户立即重试,可能导致重复Gitea评论(可接受的权衡以避免永久卡死)
|
// 如果用户立即重试,可能导致重复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);
|
await reviewStore.unmarkFindingPublished(finding.runId, finding.fingerprint);
|
||||||
throw new Error(
|
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.'
|
'Comment published to Gitea but failed to save locally. State rolled back, you may retry. Note: immediate retry may create duplicate comments.'
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
|
import * as crypto from 'node:crypto';
|
||||||
import { Context } from 'hono';
|
import { Context } from 'hono';
|
||||||
import { map } from 'lodash-es';
|
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 config from '../config';
|
||||||
import { reviewEngine } from '../review/engine';
|
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';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
// 判断是否为开发环境
|
// 判断是否为开发环境
|
||||||
@@ -16,7 +16,7 @@ enum GiteaEventType {
|
|||||||
PullRequest = 'pull_request',
|
PullRequest = 'pull_request',
|
||||||
Status = 'status',
|
Status = 'status',
|
||||||
Issue = 'issues',
|
Issue = 'issues',
|
||||||
Unknown = 'unknown'
|
Unknown = 'unknown',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,10 +48,7 @@ function verifyWebhookSignature(body: string, signature: string): boolean {
|
|||||||
// Gitea的签名没有前缀,直接比较
|
// Gitea的签名没有前缀,直接比较
|
||||||
try {
|
try {
|
||||||
// 使用timingSafeEqual进行常量时间比较,防止时序攻击
|
// 使用timingSafeEqual进行常量时间比较,防止时序攻击
|
||||||
return crypto.timingSafeEqual(
|
return crypto.timingSafeEqual(Buffer.from(calculatedSignature), Buffer.from(signature));
|
||||||
Buffer.from(calculatedSignature),
|
|
||||||
Buffer.from(signature)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('签名验证失败', error);
|
logger.error('签名验证失败', error);
|
||||||
return false;
|
return false;
|
||||||
@@ -108,10 +105,7 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从事件中提取必要信息
|
// 从事件中提取必要信息
|
||||||
const {
|
const { pull_request: pullRequest, repository: repo } = body;
|
||||||
pull_request: pullRequest,
|
|
||||||
repository: repo
|
|
||||||
} = body;
|
|
||||||
|
|
||||||
if (!pullRequest || !repo) {
|
if (!pullRequest || !repo) {
|
||||||
return c.json({ error: '无效的Webhook数据' }, 400);
|
return c.json({ error: '无效的Webhook数据' }, 400);
|
||||||
@@ -123,18 +117,21 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
const prTitle = pullRequest.title;
|
const prTitle = pullRequest.title;
|
||||||
const prUrl = pullRequest.html_url;
|
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审阅者通知
|
// 处理PR审阅者通知
|
||||||
try {
|
try {
|
||||||
// 获取PR的审阅者列表
|
// 获取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) {
|
if (reviewerUsernames.length > 0) {
|
||||||
logger.info(`PR有指定审阅者`, {
|
logger.info('PR有指定审阅者', {
|
||||||
prNumber,
|
prNumber,
|
||||||
reviewers: reviewerUsernames.join(',')
|
reviewers: reviewerUsernames.join(','),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +142,16 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
|
|
||||||
// 处理审阅者指派事件
|
// 处理审阅者指派事件
|
||||||
if (body.action === 'review_requested' && body.requested_reviewer) {
|
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) {
|
if (newReviewerUsername) {
|
||||||
await feishuService.sendPrReviewerAssignedNotification(prTitle, prUrl, [newReviewerUsername]);
|
await feishuService.sendPrReviewerAssignedNotification(prTitle, prUrl, [
|
||||||
|
newReviewerUsername,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`处理PR审阅者通知失败:`, error);
|
logger.error('处理PR审阅者通知失败:', error);
|
||||||
// 继续执行代码审查流程,不因通知失败而中断
|
// 继续执行代码审查流程,不因通知失败而中断
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +165,9 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检测fork PR:head.repo存在且与base repo不同
|
// 检测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;
|
const isForkPR = headCloneUrl && headCloneUrl !== baseCloneUrl;
|
||||||
|
|
||||||
// 包含baseSha以支持retarget场景:相同headSha但baseSha变化时需要重新审查
|
// 包含baseSha以支持retarget场景:相同headSha但baseSha变化时需要重新审查
|
||||||
@@ -193,7 +195,7 @@ async function handlePullRequestEvent(c: Context, body: any): Promise<Response>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Legacy模式:开始异步审查流程
|
// Legacy模式:开始异步审查流程
|
||||||
reviewPullRequest(owner, repoName, prNumber).catch(error => {
|
reviewPullRequest(owner, repoName, prNumber).catch((error) => {
|
||||||
logger.error(`审查PR ${owner}/${repoName}#${prNumber} 失败:`, error);
|
logger.error(`审查PR ${owner}/${repoName}#${prNumber} 失败:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -211,7 +213,7 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise<Response>
|
|||||||
sha: body.sha,
|
sha: body.sha,
|
||||||
commit_id: body.commit?.id,
|
commit_id: body.commit?.id,
|
||||||
context: body.context,
|
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<Response>
|
|||||||
message: body.commit.message || '',
|
message: body.commit.message || '',
|
||||||
added: body.commit.added || [],
|
added: body.commit.added || [],
|
||||||
removed: body.commit.removed || [],
|
removed: body.commit.removed || [],
|
||||||
modified: body.commit.modified || []
|
modified: body.commit.modified || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(`收到提交状态更新事件`, {
|
logger.info('收到提交状态更新事件', {
|
||||||
owner,
|
owner,
|
||||||
repo: repoName,
|
repo: repoName,
|
||||||
commitSha,
|
commitSha,
|
||||||
@@ -261,7 +263,7 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise<Response>
|
|||||||
relatedPR: relatedPR?.number || 'unknown',
|
relatedPR: relatedPR?.number || 'unknown',
|
||||||
added: commitInfo.added.length,
|
added: commitInfo.added.length,
|
||||||
modified: commitInfo.modified.length,
|
modified: commitInfo.modified.length,
|
||||||
removed: commitInfo.removed.length
|
removed: commitInfo.removed.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Agent模式优先处理:从本地仓库派生diff,不依赖webhook文件列表
|
// Agent模式优先处理:从本地仓库派生diff,不依赖webhook文件列表
|
||||||
@@ -294,13 +296,17 @@ async function handleCommitStatusEvent(c: Context, body: any): Promise<Response>
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Legacy模式:需要webhook文件列表
|
// 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 });
|
logger.warn('提交没有文件变更信息,忽略审查', { commitSha });
|
||||||
return c.json({ status: 'ignored', message: '提交没有文件变更信息' }, 200);
|
return c.json({ status: 'ignored', message: '提交没有文件变更信息' }, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始异步审查流程,传入关联的PR信息
|
// 开始异步审查流程,传入关联的PR信息
|
||||||
reviewCommit(owner, repoName, commitSha, commitInfo, relatedPR).catch(error => {
|
reviewCommit(owner, repoName, commitSha, commitInfo, relatedPR).catch((error) => {
|
||||||
logger.error(`审查提交 ${owner}/${repoName}@${commitSha} 失败:`, error);
|
logger.error(`审查提交 ${owner}/${repoName}@${commitSha} 失败:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,14 +327,17 @@ async function handleIssueEvent(c: Context, body: any): Promise<Response> {
|
|||||||
const issueTitle = issue.title;
|
const issueTitle = issue.title;
|
||||||
const issueUrl = issue.html_url;
|
const issueUrl = issue.html_url;
|
||||||
const creatorUsername = issue.user.full_name || issue.user.login;
|
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,
|
action,
|
||||||
issueTitle,
|
issueTitle,
|
||||||
issueUrl,
|
issueUrl,
|
||||||
creatorUsername,
|
creatorUsername,
|
||||||
assigneeUsernames: assigneeUsernames.join(',')
|
assigneeUsernames: assigneeUsernames.join(','),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -371,16 +380,16 @@ async function reviewPullRequest(owner: string, repo: string, prNumber: number):
|
|||||||
number: prNumber,
|
number: prNumber,
|
||||||
title: '测试PR',
|
title: '测试PR',
|
||||||
head: {
|
head: {
|
||||||
sha: 'abcd1234abcd1234abcd1234abcd1234abcd1234'
|
sha: 'abcd1234abcd1234abcd1234abcd1234abcd1234',
|
||||||
},
|
},
|
||||||
base: {
|
base: {
|
||||||
repo: {
|
repo: {
|
||||||
owner: {
|
owner: {
|
||||||
login: owner
|
login: owner,
|
||||||
},
|
},
|
||||||
name: repo
|
name: repo,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 测试用diff内容
|
// 测试用diff内容
|
||||||
@@ -404,7 +413,7 @@ index 1234567..abcdefg 100644
|
|||||||
// 生产环境中从Gitea获取真实数据
|
// 生产环境中从Gitea获取真实数据
|
||||||
[prDetails, diffContent] = await Promise.all([
|
[prDetails, diffContent] = await Promise.all([
|
||||||
giteaService.getPullRequestDetails(owner, repo, prNumber),
|
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('代码审查结果', {
|
logger.info('代码审查结果', {
|
||||||
summary: reviewResult.summary.substring(0, 100) + '...',
|
summary: `${reviewResult.summary.substring(0, 100)}...`,
|
||||||
commentCount: reviewResult.lineComments.length
|
commentCount: reviewResult.lineComments.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加总结评论
|
// 添加总结评论
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
logger.info('开发环境: 模拟添加PR评论', {
|
logger.info('开发环境: 模拟添加PR评论', {
|
||||||
comment: reviewResult.summary
|
comment: reviewResult.summary,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
logger.info('生产环境: 添加PR评论', {
|
logger.info('生产环境: 添加PR评论', {
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
prNumber,
|
prNumber,
|
||||||
comment: reviewResult.summary
|
comment: reviewResult.summary,
|
||||||
});
|
});
|
||||||
await giteaService.addPullRequestComment(
|
await giteaService.addPullRequestComment(
|
||||||
owner,
|
owner,
|
||||||
@@ -450,7 +459,7 @@ index 1234567..abcdefg 100644
|
|||||||
if (isDev) {
|
if (isDev) {
|
||||||
logger.info('开发环境: 模拟添加行评论', {
|
logger.info('开发环境: 模拟添加行评论', {
|
||||||
commentCount: reviewResult.lineComments.length,
|
commentCount: reviewResult.lineComments.length,
|
||||||
comments: reviewResult.lineComments
|
comments: reviewResult.lineComments,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await giteaService.addLineComments(
|
await giteaService.addLineComments(
|
||||||
@@ -465,7 +474,7 @@ index 1234567..abcdefg 100644
|
|||||||
|
|
||||||
logger.info(`完成PR ${owner}/${repo}#${prNumber} 的代码审查`);
|
logger.info(`完成PR ${owner}/${repo}#${prNumber} 的代码审查`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`审查PR失败:`, error);
|
logger.error('审查PR失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -478,21 +487,22 @@ async function reviewCommit(
|
|||||||
repo: string,
|
repo: string,
|
||||||
commitSha: string,
|
commitSha: string,
|
||||||
commitInfo: {
|
commitInfo: {
|
||||||
sha: string,
|
sha: string;
|
||||||
message: string,
|
message: string;
|
||||||
added: string[],
|
added: string[];
|
||||||
modified: string[],
|
modified: string[];
|
||||||
removed: string[]
|
removed: string[];
|
||||||
},
|
},
|
||||||
relatedPR?: PullRequestDetails | null
|
relatedPR?: PullRequestDetails | null
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info(`开始审查提交 ${owner}/${repo}@${commitSha}`);
|
logger.info(`开始审查提交 ${owner}/${repo}@${commitSha}`);
|
||||||
logger.info('提交信息', {
|
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,
|
added: commitInfo.added.length,
|
||||||
modified: commitInfo.modified.length,
|
modified: commitInfo.modified.length,
|
||||||
removed: commitInfo.removed.length
|
removed: commitInfo.removed.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果是开发环境,打印更多信息但不执行实际审查
|
// 如果是开发环境,打印更多信息但不执行实际审查
|
||||||
@@ -503,47 +513,42 @@ async function reviewCommit(
|
|||||||
commitSha,
|
commitSha,
|
||||||
added: commitInfo.added,
|
added: commitInfo.added,
|
||||||
modified: commitInfo.modified,
|
modified: commitInfo.modified,
|
||||||
removed: commitInfo.removed
|
removed: commitInfo.removed,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建自定义文件列表,因为Gitea API不直接提供
|
// 创建自定义文件列表,因为Gitea API不直接提供
|
||||||
const webhookFiles: PullRequestFile[] = [
|
const webhookFiles: PullRequestFile[] = [
|
||||||
...commitInfo.added.map(filename => ({
|
...commitInfo.added.map((filename) => ({
|
||||||
filename,
|
filename,
|
||||||
status: 'added',
|
status: 'added',
|
||||||
additions: 0, // 不知道具体行数
|
additions: 0, // 不知道具体行数
|
||||||
deletions: 0,
|
deletions: 0,
|
||||||
changes: 0
|
changes: 0,
|
||||||
})),
|
})),
|
||||||
...commitInfo.modified.map(filename => ({
|
...commitInfo.modified.map((filename) => ({
|
||||||
filename,
|
filename,
|
||||||
status: 'modified',
|
status: 'modified',
|
||||||
additions: 0,
|
additions: 0,
|
||||||
deletions: 0,
|
deletions: 0,
|
||||||
changes: 0
|
changes: 0,
|
||||||
})),
|
})),
|
||||||
...commitInfo.removed.map(filename => ({
|
...commitInfo.removed.map((filename) => ({
|
||||||
filename,
|
filename,
|
||||||
status: 'removed',
|
status: 'removed',
|
||||||
additions: 0,
|
additions: 0,
|
||||||
deletions: 0,
|
deletions: 0,
|
||||||
changes: 0
|
changes: 0,
|
||||||
}))
|
})),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 使用AI审查服务分析提交,并传入webhook提供的文件列表
|
// 使用AI审查服务分析提交,并传入webhook提供的文件列表
|
||||||
const reviewResult = await aiReviewService.reviewCommit(
|
const reviewResult = await aiReviewService.reviewCommit(owner, repo, commitSha, webhookFiles);
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
commitSha,
|
|
||||||
webhookFiles
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.info('提交代码审查结果', {
|
logger.info('提交代码审查结果', {
|
||||||
summary: reviewResult.summary.substring(0, 100) + '...',
|
summary: `${reviewResult.summary.substring(0, 100)}...`,
|
||||||
commentCount: reviewResult.lineComments.length
|
commentCount: reviewResult.lineComments.length,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加总结评论到提交
|
// 添加总结评论到提交
|
||||||
@@ -562,7 +567,7 @@ async function reviewCommit(
|
|||||||
// 尝试使用传入的PR信息,或者查找相关的PR
|
// 尝试使用传入的PR信息,或者查找相关的PR
|
||||||
try {
|
try {
|
||||||
// 如果已经有关联PR,直接使用
|
// 如果已经有关联PR,直接使用
|
||||||
if (relatedPR && relatedPR.number) {
|
if (relatedPR?.number) {
|
||||||
logger.info(`使用已知关联的PR #${relatedPR.number}`);
|
logger.info(`使用已知关联的PR #${relatedPR.number}`);
|
||||||
|
|
||||||
// 添加行级评论
|
// 添加行级评论
|
||||||
@@ -579,7 +584,7 @@ async function reviewCommit(
|
|||||||
// 否则尝试查找
|
// 否则尝试查找
|
||||||
logger.info('尝试查找与提交关联的PR');
|
logger.info('尝试查找与提交关联的PR');
|
||||||
const response = await giteaService.getRelatedPullRequest(owner, repo, commitSha);
|
const response = await giteaService.getRelatedPullRequest(owner, repo, commitSha);
|
||||||
if (response && response.number) {
|
if (response?.number) {
|
||||||
logger.info(`找到与提交关联的PR #${response.number}`);
|
logger.info(`找到与提交关联的PR #${response.number}`);
|
||||||
|
|
||||||
// 添加行级评论
|
// 添加行级评论
|
||||||
@@ -602,7 +607,7 @@ async function reviewCommit(
|
|||||||
|
|
||||||
logger.info(`完成提交 ${owner}/${repo}@${commitSha} 的代码审查`);
|
logger.info(`完成提交 ${owner}/${repo}@${commitSha} 的代码审查`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`审查提交失败:`, error);
|
logger.error('审查提交失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
src/index.ts
22
src/index.ts
@@ -1,13 +1,13 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { jwt } from 'hono/jwt';
|
|
||||||
import { serveStatic } from 'hono/bun';
|
import { serveStatic } from 'hono/bun';
|
||||||
import { handleGiteaWebhook } from './controllers/review';
|
import { jwt } from 'hono/jwt';
|
||||||
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 OpenAI from 'openai';
|
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应用实例
|
// 创建Hono应用实例
|
||||||
const app = new Hono();
|
const app = new Hono();
|
||||||
@@ -25,12 +25,12 @@ app.get('/', (c) => {
|
|||||||
webhookSecurityEnabled: webhookSecretConfigured,
|
webhookSecurityEnabled: webhookSecretConfigured,
|
||||||
configuration: {
|
configuration: {
|
||||||
webhookEndpoints: {
|
webhookEndpoints: {
|
||||||
unified: '/webhook/gitea (支持Pull Request和Commit Status事件)'
|
unified: '/webhook/gitea (支持Pull Request和Commit Status事件)',
|
||||||
},
|
},
|
||||||
signature: webhookSecretConfigured
|
signature: webhookSecretConfigured
|
||||||
? '签名验证已启用 (使用X-Gitea-Signature头)'
|
? '签名验证已启用 (使用X-Gitea-Signature头)'
|
||||||
: '警告: 签名验证未配置,建议设置WEBHOOK_SECRET环境变量'
|
: '警告: 签名验证未配置,建议设置WEBHOOK_SECRET环境变量',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -49,7 +49,6 @@ adminProtected.route('/feedback', feedbackRouter);
|
|||||||
adminProtected.route('/config', configRouter);
|
adminProtected.route('/config', configRouter);
|
||||||
app.route('/admin/api', adminProtected);
|
app.route('/admin/api', adminProtected);
|
||||||
|
|
||||||
|
|
||||||
// --- 前端静态文件服务 ---
|
// --- 前端静态文件服务 ---
|
||||||
|
|
||||||
// 优先服务于 public 目录下的静态文件
|
// 优先服务于 public 目录下的静态文件
|
||||||
@@ -58,7 +57,6 @@ app.use('/*', serveStatic({ root: './public' }));
|
|||||||
// 对于所有未匹配到的GET请求,返回 index.html,以支持SPA路由
|
// 对于所有未匹配到的GET请求,返回 index.html,以支持SPA路由
|
||||||
app.get('*', serveStatic({ path: './public/index.html' }));
|
app.get('*', serveStatic({ path: './public/index.html' }));
|
||||||
|
|
||||||
|
|
||||||
// 启动服务器
|
// 启动服务器
|
||||||
const port = config.app.port;
|
const port = config.app.port;
|
||||||
console.log(`⚡️ 服务启动在 http://localhost:${port}`);
|
console.log(`⚡️ 服务启动在 http://localhost:${port}`);
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
||||||
import { FileReviewStore } from '../store/file-review-store';
|
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
||||||
import { mkdtemp, rm, readFile } from 'node:fs/promises';
|
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { FileReviewStore } from '../store/file-review-store';
|
||||||
import type { PullRequestReviewPayload } from '../types';
|
import type { PullRequestReviewPayload } from '../types';
|
||||||
|
|
||||||
function makePRPayload(overrides: Partial<PullRequestReviewPayload> = {}): PullRequestReviewPayload {
|
function makePRPayload(
|
||||||
|
overrides: Partial<PullRequestReviewPayload> = {}
|
||||||
|
): PullRequestReviewPayload {
|
||||||
return {
|
return {
|
||||||
idempotencyKey: 'idem-' + Math.random().toString(36).slice(2, 8),
|
idempotencyKey: `idem-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
eventType: 'pull_request',
|
eventType: 'pull_request',
|
||||||
owner: 'test-owner',
|
owner: 'test-owner',
|
||||||
repo: 'test-repo',
|
repo: 'test-repo',
|
||||||
@@ -305,17 +307,37 @@ describe('FileReviewStore', () => {
|
|||||||
|
|
||||||
await store.addFindings(run.id, [
|
await store.addFindings(run.id, [
|
||||||
{
|
{
|
||||||
id: 'f1', runId: run.id, fingerprint: 'fp1', category: 'correctness',
|
id: 'f1',
|
||||||
severity: 'high', confidence: 0.9, path: 'a.ts', line: 1,
|
runId: run.id,
|
||||||
title: 'Old', detail: 'd', evidence: 'e', suggestion: 's', published: false,
|
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, [
|
await store.addFindings(run.id, [
|
||||||
{
|
{
|
||||||
id: 'f2', runId: run.id, fingerprint: 'fp2', category: 'security',
|
id: 'f2',
|
||||||
severity: 'medium', confidence: 0.8, path: 'b.ts', line: 2,
|
runId: run.id,
|
||||||
title: 'New', detail: 'd', evidence: 'e', suggestion: 's', published: false,
|
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);
|
const { run } = await store.createOrReuseRun(payload);
|
||||||
await store.addFindings(run.id, [
|
await store.addFindings(run.id, [
|
||||||
{
|
{
|
||||||
id: 'f1', runId: run.id, fingerprint: 'fp1', category: 'correctness',
|
id: 'f1',
|
||||||
severity: 'high', confidence: 0.9, path: 'a.ts', line: 1,
|
runId: run.id,
|
||||||
title: 'Bug', detail: 'd', evidence: 'e', suggestion: 's', published: false,
|
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);
|
await store.createOrReuseRun(p1);
|
||||||
// Ensure distinct timestamps for sorting
|
// Ensure distinct timestamps for sorting
|
||||||
await new Promise(r => setTimeout(r, 5));
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
await store.createOrReuseRun(p2);
|
await store.createOrReuseRun(p2);
|
||||||
await new Promise(r => setTimeout(r, 5));
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
await store.createOrReuseRun(p3);
|
await store.createOrReuseRun(p3);
|
||||||
|
|
||||||
const runs = await store.listRuns();
|
const runs = await store.listRuns();
|
||||||
|
|||||||
@@ -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 { mkdtemp, rm } from 'node:fs/promises';
|
||||||
import { tmpdir } from 'node:os';
|
import { tmpdir } from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { FileReviewStore } from '../store/file-review-store';
|
|
||||||
import { JudgeAgent } from '../agents/judge-agent';
|
import { JudgeAgent } from '../agents/judge-agent';
|
||||||
import { applyPublishPolicy } from '../policy/publish-policy';
|
import { applyPublishPolicy } from '../policy/publish-policy';
|
||||||
import type {
|
import { FileReviewStore } from '../store/file-review-store';
|
||||||
PullRequestReviewPayload,
|
import type { Finding, PullRequestReviewPayload } from '../types';
|
||||||
Finding,
|
|
||||||
ReviewRun,
|
|
||||||
} from '../types';
|
|
||||||
|
|
||||||
type PartialFinding = Omit<Finding, 'id' | 'runId' | 'published'>;
|
type PartialFinding = Omit<Finding, 'id' | 'runId' | 'published'>;
|
||||||
|
|
||||||
function makePRPayload(overrides: Partial<PullRequestReviewPayload> = {}): PullRequestReviewPayload {
|
function makePRPayload(
|
||||||
|
overrides: Partial<PullRequestReviewPayload> = {}
|
||||||
|
): PullRequestReviewPayload {
|
||||||
return {
|
return {
|
||||||
idempotencyKey: 'test/repo#1:aaa...bbb',
|
idempotencyKey: 'test/repo#1:aaa...bbb',
|
||||||
eventType: 'pull_request',
|
eventType: 'pull_request',
|
||||||
@@ -27,7 +25,10 @@ function makePRPayload(overrides: Partial<PullRequestReviewPayload> = {}): 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) => ({
|
return Array.from({ length: count }, (_, i) => ({
|
||||||
fingerprint: `fp-${severity}-${i}`,
|
fingerprint: `fp-${severity}-${i}`,
|
||||||
category: 'correctness' as const,
|
category: 'correctness' as const,
|
||||||
|
|||||||
@@ -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 { JudgeAgent } from '../agents/judge-agent';
|
||||||
import type { Finding } from '../types';
|
import type { Finding } from '../types';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ type TestFinding = Omit<Finding, 'id' | 'runId' | 'published'>;
|
|||||||
|
|
||||||
function makeFinding(overrides: Partial<TestFinding> = {}): TestFinding {
|
function makeFinding(overrides: Partial<TestFinding> = {}): TestFinding {
|
||||||
return {
|
return {
|
||||||
fingerprint: 'fp-' + Math.random().toString(36).slice(2, 8),
|
fingerprint: `fp-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
category: 'correctness',
|
category: 'correctness',
|
||||||
severity: 'medium',
|
severity: 'medium',
|
||||||
confidence: 0.8,
|
confidence: 0.8,
|
||||||
@@ -34,8 +34,8 @@ describe('JudgeAgent', () => {
|
|||||||
test('duplicate fingerprints → keeps highest weighted', () => {
|
test('duplicate fingerprints → keeps highest weighted', () => {
|
||||||
const fp = 'same-fingerprint';
|
const fp = 'same-fingerprint';
|
||||||
const findings: TestFinding[] = [
|
const findings: TestFinding[] = [
|
||||||
makeFinding({ fingerprint: fp, severity: 'low', confidence: 0.9 }), // weight: 1 * 0.9 = 0.9
|
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: '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
|
makeFinding({ fingerprint: fp, severity: 'medium', confidence: 0.6 }), // weight: 2 * 0.6 = 1.2
|
||||||
];
|
];
|
||||||
const result = judge.judge(findings);
|
const result = judge.judge(findings);
|
||||||
@@ -59,8 +59,8 @@ describe('JudgeAgent', () => {
|
|||||||
// ─── Sorting by severity × confidence ───
|
// ─── Sorting by severity × confidence ───
|
||||||
test('findings sorted by weight descending', () => {
|
test('findings sorted by weight descending', () => {
|
||||||
const findings: TestFinding[] = [
|
const findings: TestFinding[] = [
|
||||||
makeFinding({ fingerprint: 'a', severity: 'low', confidence: 0.9 }), // 1 * 0.9 = 0.9
|
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: 'b', severity: 'high', confidence: 0.8 }), // 3 * 0.8 = 2.4
|
||||||
makeFinding({ fingerprint: 'c', severity: 'medium', confidence: 0.7 }), // 2 * 0.7 = 1.4
|
makeFinding({ fingerprint: 'c', severity: 'medium', confidence: 0.7 }), // 2 * 0.7 = 1.4
|
||||||
];
|
];
|
||||||
const result = judge.judge(findings);
|
const result = judge.judge(findings);
|
||||||
@@ -99,10 +99,10 @@ describe('JudgeAgent', () => {
|
|||||||
// ─── Dedup + sort combined ───
|
// ─── Dedup + sort combined ───
|
||||||
test('dedup then sort: complex scenario', () => {
|
test('dedup then sort: complex scenario', () => {
|
||||||
const findings: TestFinding[] = [
|
const findings: TestFinding[] = [
|
||||||
makeFinding({ fingerprint: 'x', severity: 'low', confidence: 0.3 }), // weight 0.3 — will be overridden
|
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: '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: '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);
|
const result = judge.judge(findings);
|
||||||
expect(result.findings).toHaveLength(3); // x, y, z (deduped)
|
expect(result.findings).toHaveLength(3); // x, y, z (deduped)
|
||||||
|
|||||||
@@ -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 { applyPublishPolicy } from '../policy/publish-policy';
|
||||||
import type { Finding } from '../types';
|
import type { Finding } from '../types';
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ type TestFinding = Omit<Finding, 'id' | 'runId' | 'published'>;
|
|||||||
|
|
||||||
function makeFinding(overrides: Partial<TestFinding> = {}): TestFinding {
|
function makeFinding(overrides: Partial<TestFinding> = {}): TestFinding {
|
||||||
return {
|
return {
|
||||||
fingerprint: 'fp-' + Math.random().toString(36).slice(2, 8),
|
fingerprint: `fp-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
category: 'correctness',
|
category: 'correctness',
|
||||||
severity: 'medium',
|
severity: 'medium',
|
||||||
confidence: 0.9,
|
confidence: 0.9,
|
||||||
@@ -113,11 +113,11 @@ describe('applyPublishPolicy', () => {
|
|||||||
// ─── Mixed findings ───
|
// ─── Mixed findings ───
|
||||||
test('mixed findings split correctly', () => {
|
test('mixed findings split correctly', () => {
|
||||||
const findings: TestFinding[] = [
|
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: 'medium', confidence: 0.85 }), // → publishable
|
||||||
makeFinding({ severity: 'low', confidence: 0.9 }), // → dropped (low severity, humanGate off)
|
makeFinding({ severity: 'low', confidence: 0.9 }), // → dropped (low severity, humanGate off)
|
||||||
makeFinding({ severity: 'high', confidence: 0.5 }), // → dropped (low confidence)
|
makeFinding({ severity: 'high', confidence: 0.5 }), // → dropped (low confidence)
|
||||||
makeFinding({ severity: 'medium', confidence: 0.6 }), // → dropped (low confidence)
|
makeFinding({ severity: 'medium', confidence: 0.6 }), // → dropped (low confidence)
|
||||||
];
|
];
|
||||||
const result = applyPublishPolicy(findings, MIN_CONFIDENCE, false);
|
const result = applyPublishPolicy(findings, MIN_CONFIDENCE, false);
|
||||||
expect(result.publishable).toHaveLength(2);
|
expect(result.publishable).toHaveLength(2);
|
||||||
@@ -127,9 +127,9 @@ describe('applyPublishPolicy', () => {
|
|||||||
|
|
||||||
test('mixed findings with humanGate on', () => {
|
test('mixed findings with humanGate on', () => {
|
||||||
const findings: TestFinding[] = [
|
const findings: TestFinding[] = [
|
||||||
makeFinding({ severity: 'high', confidence: 0.95 }), // → publishable
|
makeFinding({ severity: 'high', confidence: 0.95 }), // → publishable
|
||||||
makeFinding({ severity: 'low', confidence: 0.9 }), // → gated
|
makeFinding({ severity: 'low', confidence: 0.9 }), // → gated
|
||||||
makeFinding({ severity: 'high', confidence: 0.5 }), // → gated
|
makeFinding({ severity: 'high', confidence: 0.5 }), // → gated
|
||||||
];
|
];
|
||||||
const result = applyPublishPolicy(findings, MIN_CONFIDENCE, true);
|
const result = applyPublishPolicy(findings, MIN_CONFIDENCE, true);
|
||||||
expect(result.publishable).toHaveLength(1);
|
expect(result.publishable).toHaveLength(1);
|
||||||
@@ -161,7 +161,7 @@ describe('applyPublishPolicy', () => {
|
|||||||
const result = applyPublishPolicy(findings, MIN_CONFIDENCE, false);
|
const result = applyPublishPolicy(findings, MIN_CONFIDENCE, false);
|
||||||
// Policy doesn't care about fingerprint - each finding evaluated independently
|
// Policy doesn't care about fingerprint - each finding evaluated independently
|
||||||
expect(result.publishable).toHaveLength(2); // high+medium
|
expect(result.publishable).toHaveLength(2); // high+medium
|
||||||
expect(result.dropped).toHaveLength(1); // low severity
|
expect(result.dropped).toHaveLength(1); // low severity
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Different minConfidence thresholds ───
|
// ─── Different minConfidence thresholds ───
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, test, expect } from 'bun:test';
|
import { describe, expect, test } from 'bun:test';
|
||||||
import { SandboxExec } from '../context/sandbox-exec';
|
import { SandboxExec } from '../context/sandbox-exec';
|
||||||
|
|
||||||
describe('SandboxExec', () => {
|
describe('SandboxExec', () => {
|
||||||
@@ -157,6 +157,6 @@ describe('SandboxExec', () => {
|
|||||||
});
|
});
|
||||||
expect(result.stdout).not.toContain('SUPER_SECRET_TOKEN');
|
expect(result.stdout).not.toContain('SUPER_SECRET_TOKEN');
|
||||||
expect(result.stdout).not.toContain('should-not-leak');
|
expect(result.stdout).not.toContain('should-not-leak');
|
||||||
delete process.env.SUPER_SECRET_TOKEN;
|
process.env.SUPER_SECRET_TOKEN = undefined;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 { SpecialistAgent } from '../agents/specialist-agent';
|
||||||
import { ToolRegistry } from '../tools/registry';
|
import { ToolRegistry } from '../tools/registry';
|
||||||
import { z } from 'zod';
|
|
||||||
import type { ReviewRun, ReviewContext, FindingCategory } from '../types';
|
|
||||||
import type { Tool } from '../tools/types';
|
import type { Tool } from '../tools/types';
|
||||||
|
import type { FindingCategory, ReviewContext, ReviewRun } from '../types';
|
||||||
|
|
||||||
function makeRun(overrides: Partial<ReviewRun> = {}): ReviewRun {
|
function makeRun(overrides: Partial<ReviewRun> = {}): ReviewRun {
|
||||||
return {
|
return {
|
||||||
@@ -139,9 +139,7 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
suggestion: 'Use undefined',
|
suggestion: 'Use undefined',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { client, getCalls } = createMockOpenAI([
|
const { client, getCalls } = createMockOpenAI([() => jsonResponse({ findings: [finding] })]);
|
||||||
() => jsonResponse({ findings: [finding] }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const agent = new SpecialistAgent(client as any, 'gpt-4', category, 'TestAgent', 'bugs');
|
const agent = new SpecialistAgent(client as any, 'gpt-4', category, 'TestAgent', 'bugs');
|
||||||
const result = await agent.review(makeRun(), makeContext());
|
const result = await agent.review(makeRun(), makeContext());
|
||||||
@@ -178,7 +176,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
||||||
|
|
||||||
@@ -203,7 +206,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
await agent.review(makeRun(), makeContext());
|
||||||
|
|
||||||
@@ -222,16 +230,21 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
const registry = new ToolRegistry();
|
const registry = new ToolRegistry();
|
||||||
registry.register(makeDummyTool());
|
registry.register(makeDummyTool());
|
||||||
|
|
||||||
let callCount = 0;
|
const _callCount = 0;
|
||||||
const { client, getCalls } = createMockOpenAI([
|
const { client, getCalls } = createMockOpenAI([
|
||||||
() => jsonResponse({ findings: [], need_more_investigation: true }),
|
() => jsonResponse({ findings: [], need_more_investigation: true }),
|
||||||
() => jsonResponse({ findings: [], need_more_investigation: false }),
|
() => jsonResponse({ findings: [], need_more_investigation: false }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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();
|
const calls = getCalls();
|
||||||
expect(calls.length).toBeGreaterThanOrEqual(2);
|
expect(calls.length).toBeGreaterThanOrEqual(2);
|
||||||
@@ -271,7 +284,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
||||||
|
|
||||||
@@ -314,7 +332,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
||||||
|
|
||||||
@@ -329,11 +352,18 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
registry.register(makeDummyTool());
|
registry.register(makeDummyTool());
|
||||||
|
|
||||||
const { client } = createMockOpenAI([
|
const { client } = createMockOpenAI([
|
||||||
() => { throw new Error('API rate limited'); },
|
() => {
|
||||||
|
throw new Error('API rate limited');
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
||||||
|
|
||||||
@@ -351,9 +381,14 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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();
|
const calls = getCalls();
|
||||||
expect(calls).toHaveLength(2);
|
expect(calls).toHaveLength(2);
|
||||||
@@ -369,7 +404,9 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
const registry = new ToolRegistry();
|
const registry = new ToolRegistry();
|
||||||
registry.register({
|
registry.register({
|
||||||
...makeDummyTool(),
|
...makeDummyTool(),
|
||||||
execute: async () => { throw new Error('Sandbox timeout'); },
|
execute: async () => {
|
||||||
|
throw new Error('Sandbox timeout');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { client, getCalls } = createMockOpenAI([
|
const { client, getCalls } = createMockOpenAI([
|
||||||
@@ -378,7 +415,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
await agent.review(makeRun(), makeContext());
|
||||||
|
|
||||||
@@ -397,7 +439,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
const { client } = createMockOpenAI([() => emptyResponse()]);
|
const { client } = createMockOpenAI([() => emptyResponse()]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
||||||
|
|
||||||
@@ -413,7 +460,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
||||||
|
|
||||||
@@ -440,7 +492,12 @@ describe('SpecialistAgent ReAct loop', () => {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const agent = new SpecialistAgent(
|
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());
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { SpecialistAgent } from './specialist-agent';
|
|
||||||
import { ToolRegistry } from '../tools/registry';
|
|
||||||
import type { LearningSystem } from '../learning/learning-system';
|
import type { LearningSystem } from '../learning/learning-system';
|
||||||
|
import { ToolRegistry } from '../tools/registry';
|
||||||
|
import { SpecialistAgent } from './specialist-agent';
|
||||||
|
|
||||||
export class CorrectnessAgent extends SpecialistAgent {
|
export class CorrectnessAgent extends SpecialistAgent {
|
||||||
constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) {
|
constructor(
|
||||||
super(openai, model, 'correctness', 'Correctness Agent', '业务逻辑正确性、边界条件、空值处理和明显bug', toolRegistry, learningSystem);
|
openai: OpenAI,
|
||||||
|
model: string,
|
||||||
|
toolRegistry?: ToolRegistry,
|
||||||
|
learningSystem?: LearningSystem
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
openai,
|
||||||
|
model,
|
||||||
|
'correctness',
|
||||||
|
'Correctness Agent',
|
||||||
|
'业务逻辑正确性、边界条件、空值处理和明显bug',
|
||||||
|
toolRegistry,
|
||||||
|
learningSystem
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { Finding, ReviewContext } from '../types';
|
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import { Finding, ReviewContext } from '../types';
|
||||||
|
|
||||||
export interface CritiqueResult {
|
export interface CritiqueResult {
|
||||||
qualityScore: number; // 0-1
|
qualityScore: number; // 0-1
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { SpecialistAgent } from './specialist-agent';
|
|
||||||
import { Finding, FindingSeverity } from '../types';
|
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import { Finding, FindingSeverity } from '../types';
|
||||||
|
import { SpecialistAgent } from './specialist-agent';
|
||||||
|
|
||||||
interface AgentOpinion {
|
interface AgentOpinion {
|
||||||
agentName: string;
|
agentName: string;
|
||||||
@@ -23,7 +23,7 @@ export class DebateOrchestrator {
|
|||||||
async conductDebate(
|
async conductDebate(
|
||||||
finding: Omit<Finding, 'id' | 'runId' | 'published'>,
|
finding: Omit<Finding, 'id' | 'runId' | 'published'>,
|
||||||
agents: SpecialistAgent[],
|
agents: SpecialistAgent[],
|
||||||
maxRounds: number = 2
|
maxRounds = 2
|
||||||
): Promise<Omit<Finding, 'id' | 'runId' | 'published'>> {
|
): Promise<Omit<Finding, 'id' | 'runId' | 'published'>> {
|
||||||
if (agents.length < 2) {
|
if (agents.length < 2) {
|
||||||
logger.debug('Debate需要至少2个agents,跳过');
|
logger.debug('Debate需要至少2个agents,跳过');
|
||||||
@@ -213,13 +213,15 @@ ${otherOpinions
|
|||||||
|
|
||||||
// 返回当前意见(从opinions Map中获取)
|
// 返回当前意见(从opinions Map中获取)
|
||||||
const currentOpinion = opinions.get(agentName);
|
const currentOpinion = opinions.get(agentName);
|
||||||
return currentOpinion || {
|
return (
|
||||||
agentName,
|
currentOpinion || {
|
||||||
confidence: 0.5,
|
agentName,
|
||||||
severity: 'medium',
|
confidence: 0.5,
|
||||||
reasoning: '修订失败',
|
severity: 'medium',
|
||||||
isValid: true,
|
reasoning: '修订失败',
|
||||||
};
|
isValid: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -297,11 +299,15 @@ ${otherOpinions
|
|||||||
severityVotes[vote.severity] += vote.confidence;
|
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
|
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')}`;
|
.join('\n')}`;
|
||||||
|
|
||||||
logger.info('Debate达成共识', {
|
logger.info('Debate达成共识', {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ReviewDecision, Finding } from '../types';
|
import { Finding, ReviewDecision } from '../types';
|
||||||
|
|
||||||
const severityWeight: Record<Finding['severity'], number> = {
|
const severityWeight: Record<Finding['severity'], number> = {
|
||||||
high: 3,
|
high: 3,
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { SpecialistAgent } from './specialist-agent';
|
|
||||||
import { ToolRegistry } from '../tools/registry';
|
|
||||||
import type { LearningSystem } from '../learning/learning-system';
|
import type { LearningSystem } from '../learning/learning-system';
|
||||||
|
import { ToolRegistry } from '../tools/registry';
|
||||||
|
import { SpecialistAgent } from './specialist-agent';
|
||||||
|
|
||||||
export class MaintainabilityAgent extends SpecialistAgent {
|
export class MaintainabilityAgent extends SpecialistAgent {
|
||||||
constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) {
|
constructor(
|
||||||
super(openai, model, 'maintainability', 'Maintainability Agent', '可维护性、复杂度、接口破坏风险和可测试性不足', toolRegistry, learningSystem);
|
openai: OpenAI,
|
||||||
|
model: string,
|
||||||
|
toolRegistry?: ToolRegistry,
|
||||||
|
learningSystem?: LearningSystem
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
openai,
|
||||||
|
model,
|
||||||
|
'maintainability',
|
||||||
|
'Maintainability Agent',
|
||||||
|
'可维护性、复杂度、接口破坏风险和可测试性不足',
|
||||||
|
toolRegistry,
|
||||||
|
learningSystem
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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 {
|
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 {
|
export class ReflexionAgent extends SpecialistAgent {
|
||||||
@@ -31,7 +34,7 @@ export class ReflexionAgent extends SpecialistAgent {
|
|||||||
async reviewWithReflection(
|
async reviewWithReflection(
|
||||||
run: ReviewRun,
|
run: ReviewRun,
|
||||||
context: ReviewContext,
|
context: ReviewContext,
|
||||||
maxReflectionRounds: number = 2
|
maxReflectionRounds = 2
|
||||||
): Promise<AgentResult> {
|
): Promise<AgentResult> {
|
||||||
let bestFindings: Omit<Finding, 'id' | 'runId' | 'published'>[] = [];
|
let bestFindings: Omit<Finding, 'id' | 'runId' | 'published'>[] = [];
|
||||||
let bestQualityScore = 0;
|
let bestQualityScore = 0;
|
||||||
@@ -165,7 +168,9 @@ ${context.diff.slice(0, 3000)}
|
|||||||
return validated.findings.map((finding) => ({
|
return validated.findings.map((finding) => ({
|
||||||
...finding,
|
...finding,
|
||||||
category: this.category,
|
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) {
|
} catch (error) {
|
||||||
logger.error(`${this.agentName} Refine失败`, {
|
logger.error(`${this.agentName} Refine失败`, {
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { SpecialistAgent } from './specialist-agent';
|
|
||||||
import { ToolRegistry } from '../tools/registry';
|
|
||||||
import type { LearningSystem } from '../learning/learning-system';
|
import type { LearningSystem } from '../learning/learning-system';
|
||||||
|
import { ToolRegistry } from '../tools/registry';
|
||||||
|
import { SpecialistAgent } from './specialist-agent';
|
||||||
|
|
||||||
export class ReliabilityAgent extends SpecialistAgent {
|
export class ReliabilityAgent extends SpecialistAgent {
|
||||||
constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) {
|
constructor(
|
||||||
super(openai, model, 'reliability', 'Reliability Agent', '错误处理、重试策略、幂等性、并发一致性和资源释放', toolRegistry, learningSystem);
|
openai: OpenAI,
|
||||||
|
model: string,
|
||||||
|
toolRegistry?: ToolRegistry,
|
||||||
|
learningSystem?: LearningSystem
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
openai,
|
||||||
|
model,
|
||||||
|
'reliability',
|
||||||
|
'Reliability Agent',
|
||||||
|
'错误处理、重试策略、幂等性、并发一致性和资源释放',
|
||||||
|
toolRegistry,
|
||||||
|
learningSystem
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { SpecialistAgent } from './specialist-agent';
|
|
||||||
import { ToolRegistry } from '../tools/registry';
|
|
||||||
import type { LearningSystem } from '../learning/learning-system';
|
import type { LearningSystem } from '../learning/learning-system';
|
||||||
|
import { ToolRegistry } from '../tools/registry';
|
||||||
|
import { SpecialistAgent } from './specialist-agent';
|
||||||
|
|
||||||
export class SecurityAgent extends SpecialistAgent {
|
export class SecurityAgent extends SpecialistAgent {
|
||||||
constructor(openai: OpenAI, model: string, toolRegistry?: ToolRegistry, learningSystem?: LearningSystem) {
|
constructor(
|
||||||
super(openai, model, 'security', 'Security Agent', '注入漏洞、权限绕过、敏感信息泄露、反序列化和输入校验缺失', toolRegistry, learningSystem);
|
openai: OpenAI,
|
||||||
|
model: string,
|
||||||
|
toolRegistry?: ToolRegistry,
|
||||||
|
learningSystem?: LearningSystem
|
||||||
|
) {
|
||||||
|
super(
|
||||||
|
openai,
|
||||||
|
model,
|
||||||
|
'security',
|
||||||
|
'Security Agent',
|
||||||
|
'注入漏洞、权限绕过、敏感信息泄露、反序列化和输入校验缺失',
|
||||||
|
toolRegistry,
|
||||||
|
learningSystem
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import OpenAI from 'openai';
|
|
||||||
import { createHash } from 'node:crypto';
|
import { createHash } from 'node:crypto';
|
||||||
|
import OpenAI from 'openai';
|
||||||
import { logger } from '../../utils/logger';
|
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 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 {
|
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 {
|
function toCompactContext(context: ReviewContext): string {
|
||||||
@@ -58,7 +61,10 @@ function toCompactContext(context: ReviewContext): string {
|
|||||||
let result = tryBuild(maxChangesPerFile, maxFileContentsEntries);
|
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) {
|
if (maxChangesPerFile > 20) {
|
||||||
maxChangesPerFile = Math.max(20, Math.floor(maxChangesPerFile * 0.7));
|
maxChangesPerFile = Math.max(20, Math.floor(maxChangesPerFile * 0.7));
|
||||||
} else if (maxFileContentsEntries > 0) {
|
} else if (maxFileContentsEntries > 0) {
|
||||||
@@ -74,7 +80,7 @@ function toCompactContext(context: ReviewContext): string {
|
|||||||
originalSize: result.length,
|
originalSize: result.length,
|
||||||
limit: MAX_CONTEXT_CHARS,
|
limit: MAX_CONTEXT_CHARS,
|
||||||
});
|
});
|
||||||
result = result.slice(0, MAX_CONTEXT_CHARS) + '\n... [truncated]';
|
result = `${result.slice(0, MAX_CONTEXT_CHARS)}\n... [truncated]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -137,7 +143,8 @@ ${toCompactContext(context)}`;
|
|||||||
const findings = parsed.findings.map((item) => ({
|
const findings = parsed.findings.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
category: this.category,
|
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 {
|
return {
|
||||||
@@ -259,7 +266,9 @@ confidence取值范围0到1。line必须是正整数且引用新增行。`,
|
|||||||
// 使用schema验证findings,防止畸形数据流入发布系统
|
// 使用schema验证findings,防止畸形数据流入发布系统
|
||||||
const validated = findingResponseSchema.parse({ findings: parsed.findings });
|
const validated = findingResponseSchema.parse({ findings: parsed.findings });
|
||||||
for (const item of validated.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 覆盖前一次
|
// 基于 fingerprint 去重:后续迭代产生的同一 finding 覆盖前一次
|
||||||
findingsMap.set(fp, {
|
findingsMap.set(fp, {
|
||||||
...item,
|
...item,
|
||||||
@@ -278,7 +287,8 @@ confidence取值范围0到1。line必须是正整数且引用新增行。`,
|
|||||||
messages.push(choice.message as OpenAI.Chat.ChatCompletionMessageParam);
|
messages.push(choice.message as OpenAI.Chat.ChatCompletionMessageParam);
|
||||||
messages.push({
|
messages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: '请使用工具进行更深入的调查。如果你已经获得了足够的信息,请将 need_more_investigation 设为 false 并输出最终结果。',
|
content:
|
||||||
|
'请使用工具进行更深入的调查。如果你已经获得了足够的信息,请将 need_more_investigation 设为 false 并输出最终结果。',
|
||||||
});
|
});
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
logger.error(`${this.agentName} 解析响应失败`, {
|
logger.error(`${this.agentName} 解析响应失败`, {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { readFile, lstat } from 'node:fs/promises';
|
import { lstat, readFile } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { DiffFile, ReviewContext, ReviewRun, ChangedFile } from '../types';
|
import { ChangedFile, DiffFile, ReviewContext, ReviewRun } from '../types';
|
||||||
import { SandboxExec } from './sandbox-exec';
|
|
||||||
import { LocalRepoManager } from './local-repo-manager';
|
import { LocalRepoManager } from './local-repo-manager';
|
||||||
|
import { SandboxExec } from './sandbox-exec';
|
||||||
|
|
||||||
function toStatus(status: string): ChangedFile['status'] {
|
function toStatus(status: string): ChangedFile['status'] {
|
||||||
const value = status.trim().charAt(0).toUpperCase();
|
const value = status.trim().charAt(0).toUpperCase();
|
||||||
@@ -33,7 +33,11 @@ export class DiffExtractor {
|
|||||||
return this.sandboxExec;
|
return this.sandboxExec;
|
||||||
}
|
}
|
||||||
|
|
||||||
async buildContext(run: ReviewRun, mirrorPath: string, workspacePath: string): Promise<ReviewContext> {
|
async buildContext(
|
||||||
|
run: ReviewRun,
|
||||||
|
mirrorPath: string,
|
||||||
|
workspacePath: string
|
||||||
|
): Promise<ReviewContext> {
|
||||||
const targetSha = run.headSha || run.commitSha;
|
const targetSha = run.headSha || run.commitSha;
|
||||||
if (!targetSha) {
|
if (!targetSha) {
|
||||||
throw new Error('缺少 target sha,无法构建审查上下文');
|
throw new Error('缺少 target sha,无法构建审查上下文');
|
||||||
@@ -41,7 +45,8 @@ export class DiffExtractor {
|
|||||||
|
|
||||||
let baseSha = run.baseSha;
|
let baseSha = run.baseSha;
|
||||||
if (!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
|
// Root commit场景:没有parent,使用git show获取完整diff
|
||||||
@@ -55,7 +60,7 @@ export class DiffExtractor {
|
|||||||
: await this.getChangedFiles(workspacePath, baseSha!, targetSha);
|
: await this.getChangedFiles(workspacePath, baseSha!, targetSha);
|
||||||
|
|
||||||
// 构建允许的文件路径集合,确保parsedDiff也受REVIEW_MAX_FILES_PER_RUN限制
|
// 构建允许的文件路径集合,确保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 parsedDiff = this.parseDiff(diff, allowedPaths);
|
||||||
|
|
||||||
const fileContents = await this.readChangedFileContents(workspacePath, changedFiles);
|
const fileContents = await this.readChangedFileContents(workspacePath, changedFiles);
|
||||||
@@ -86,32 +91,51 @@ export class DiffExtractor {
|
|||||||
targetSha: string
|
targetSha: string
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
if (eventType === 'pull_request') {
|
if (eventType === 'pull_request') {
|
||||||
const response = await this.sandboxExec.run('git', ['diff', '--unified=3', `${baseSha}...${targetSha}`], {
|
const response = await this.sandboxExec.run(
|
||||||
cwd: workspacePath,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['diff', '--unified=3', `${baseSha}...${targetSha}`],
|
||||||
});
|
{
|
||||||
|
cwd: workspacePath,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
return response.stdout;
|
return response.stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.sandboxExec.run('git', ['show', '--format=', '--unified=3', targetSha], {
|
const response = await this.sandboxExec.run(
|
||||||
cwd: workspacePath,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['show', '--format=', '--unified=3', targetSha],
|
||||||
});
|
{
|
||||||
|
cwd: workspacePath,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
return response.stdout;
|
return response.stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getRootCommitChangedFiles(workspacePath: string, sha: string): Promise<ChangedFile[]> {
|
private async getRootCommitChangedFiles(
|
||||||
|
workspacePath: string,
|
||||||
|
sha: string
|
||||||
|
): Promise<ChangedFile[]> {
|
||||||
// Root commit:所有文件都是新增的(A状态)
|
// Root commit:所有文件都是新增的(A状态)
|
||||||
// --root flag是必需的,否则diff-tree对root commit返回空输出
|
// --root flag是必需的,否则diff-tree对root commit返回空输出
|
||||||
const statusResult = await this.sandboxExec.run('git', ['diff-tree', '--root', '--no-commit-id', '--name-status', '-r', sha], {
|
const statusResult = await this.sandboxExec.run(
|
||||||
cwd: workspacePath,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['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], {
|
const numStatResult = await this.sandboxExec.run(
|
||||||
cwd: workspacePath,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['diff-tree', '--root', '--no-commit-id', '--numstat', '-r', sha],
|
||||||
});
|
{
|
||||||
|
cwd: workspacePath,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const numMap = new Map<string, { additions: number; deletions: number }>();
|
const numMap = new Map<string, { additions: number; deletions: number }>();
|
||||||
for (const line of numStatResult.stdout.split('\n')) {
|
for (const line of numStatResult.stdout.split('\n')) {
|
||||||
@@ -155,16 +179,28 @@ export class DiffExtractor {
|
|||||||
return changedFiles;
|
return changedFiles;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getChangedFiles(workspacePath: string, baseSha: string, targetSha: string): Promise<ChangedFile[]> {
|
private async getChangedFiles(
|
||||||
const statusResult = await this.sandboxExec.run('git', ['diff', '--name-status', `${baseSha}...${targetSha}`], {
|
workspacePath: string,
|
||||||
cwd: workspacePath,
|
baseSha: string,
|
||||||
timeoutMs: this.commandTimeoutMs,
|
targetSha: string
|
||||||
});
|
): Promise<ChangedFile[]> {
|
||||||
|
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}`], {
|
const numStatResult = await this.sandboxExec.run(
|
||||||
cwd: workspacePath,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['diff', '--numstat', `${baseSha}...${targetSha}`],
|
||||||
});
|
{
|
||||||
|
cwd: workspacePath,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const numMap = new Map<string, { additions: number; deletions: number }>();
|
const numMap = new Map<string, { additions: number; deletions: number }>();
|
||||||
for (const line of numStatResult.stdout.split('\n')) {
|
for (const line of numStatResult.stdout.split('\n')) {
|
||||||
@@ -229,9 +265,7 @@ export class DiffExtractor {
|
|||||||
|
|
||||||
const content = await readFile(filePath, 'utf-8');
|
const content = await readFile(filePath, 'utf-8');
|
||||||
result[file.path] = content.slice(0, this.maxFileContentChars);
|
result[file.path] = content.slice(0, this.maxFileContentChars);
|
||||||
} catch {
|
} catch {}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -277,7 +311,7 @@ export class DiffExtractor {
|
|||||||
|
|
||||||
if (line.startsWith('@@')) {
|
if (line.startsWith('@@')) {
|
||||||
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
||||||
if (match && match[1]) {
|
if (match?.[1]) {
|
||||||
lineNumber = Number.parseInt(match[1], 10) - 1;
|
lineNumber = Number.parseInt(match[1], 10) - 1;
|
||||||
inHunk = true;
|
inHunk = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
|
import { createHash } from 'node:crypto';
|
||||||
import { access, mkdir, rm } from 'node:fs/promises';
|
import { access, mkdir, rm } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { createHash } from 'node:crypto';
|
|
||||||
import { SandboxExec } from './sandbox-exec';
|
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import { SandboxExec } from './sandbox-exec';
|
||||||
|
|
||||||
export interface LocalRepoPaths {
|
export interface LocalRepoPaths {
|
||||||
mirrorPath: string;
|
mirrorPath: string;
|
||||||
@@ -91,16 +91,24 @@ export class LocalRepoManager {
|
|||||||
|
|
||||||
if (!mirrorExists) {
|
if (!mirrorExists) {
|
||||||
logger.info('创建本地 mirror 仓库', { owner, repo, mirrorPath });
|
logger.info('创建本地 mirror 仓库', { owner, repo, mirrorPath });
|
||||||
await this.sandboxExec.run('git', [...authArgs, 'clone', '--mirror', cloneUrl, mirrorPath], {
|
await this.sandboxExec.run(
|
||||||
cwd: this.workDir,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
[...authArgs, 'clone', '--mirror', cloneUrl, mirrorPath],
|
||||||
});
|
{
|
||||||
|
cwd: this.workDir,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// 更新remote URL(不含认证信息)
|
// 更新remote URL(不含认证信息)
|
||||||
await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote', 'set-url', 'origin', cloneUrl], {
|
await this.sandboxExec.run(
|
||||||
cwd: this.workDir,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['--git-dir', mirrorPath, 'remote', 'set-url', 'origin', cloneUrl],
|
||||||
});
|
{
|
||||||
|
cwd: this.workDir,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
// fetch使用认证参数
|
// fetch使用认证参数
|
||||||
await this.sandboxExec.run(
|
await this.sandboxExec.run(
|
||||||
'git',
|
'git',
|
||||||
@@ -117,28 +125,47 @@ export class LocalRepoManager {
|
|||||||
logger.info('Fork PR检测,添加head remote', { owner, repo, headCloneUrl });
|
logger.info('Fork PR检测,添加head remote', { owner, repo, headCloneUrl });
|
||||||
|
|
||||||
// 检查head remote是否已存在,存在则更新URL
|
// 检查head remote是否已存在,存在则更新URL
|
||||||
const remoteListResult = await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote'], {
|
const remoteListResult = await this.sandboxExec.run(
|
||||||
cwd: this.workDir,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['--git-dir', mirrorPath, 'remote'],
|
||||||
});
|
{
|
||||||
|
cwd: this.workDir,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
const hasHeadRemote = remoteListResult.stdout.includes('head');
|
const hasHeadRemote = remoteListResult.stdout.includes('head');
|
||||||
|
|
||||||
if (hasHeadRemote) {
|
if (hasHeadRemote) {
|
||||||
await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote', 'set-url', 'head', headCloneUrl], {
|
await this.sandboxExec.run(
|
||||||
cwd: this.workDir,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['--git-dir', mirrorPath, 'remote', 'set-url', 'head', headCloneUrl],
|
||||||
});
|
{
|
||||||
|
cwd: this.workDir,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'remote', 'add', 'head', headCloneUrl], {
|
await this.sandboxExec.run(
|
||||||
cwd: this.workDir,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['--git-dir', mirrorPath, 'remote', 'add', 'head', headCloneUrl],
|
||||||
});
|
{
|
||||||
|
cwd: this.workDir,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch head remote
|
// Fetch head remote
|
||||||
await this.sandboxExec.run(
|
await this.sandboxExec.run(
|
||||||
'git',
|
'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,
|
cwd: this.workDir,
|
||||||
timeoutMs: this.commandTimeoutMs,
|
timeoutMs: this.commandTimeoutMs,
|
||||||
@@ -156,10 +183,14 @@ export class LocalRepoManager {
|
|||||||
timeoutMs: this.commandTimeoutMs,
|
timeoutMs: this.commandTimeoutMs,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.sandboxExec.run('git', ['--git-dir', mirrorPath, 'worktree', 'add', '--detach', workspacePath, targetSha], {
|
await this.sandboxExec.run(
|
||||||
cwd: this.workDir,
|
'git',
|
||||||
timeoutMs: this.commandTimeoutMs,
|
['--git-dir', mirrorPath, 'worktree', 'add', '--detach', workspacePath, targetSha],
|
||||||
});
|
{
|
||||||
|
cwd: this.workDir,
|
||||||
|
timeoutMs: this.commandTimeoutMs,
|
||||||
|
}
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// 确保锁总是被释放,在所有mirror-mutating操作(fetch/prune/add)完成后释放
|
// 确保锁总是被释放,在所有mirror-mutating操作(fetch/prune/add)完成后释放
|
||||||
unlock();
|
unlock();
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ export class SandboxExec {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async run(command: string, args: string[], options: SandboxRunOptions): Promise<SandboxCommandResult> {
|
async run(
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options: SandboxRunOptions
|
||||||
|
): Promise<SandboxCommandResult> {
|
||||||
if (!this.allowedCommands.has(command)) {
|
if (!this.allowedCommands.has(command)) {
|
||||||
throw new Error(`命令未在白名单中: ${command}`);
|
throw new Error(`命令未在白名单中: ${command}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ class ReviewEngine {
|
|||||||
config.review.maxFilesPerRun,
|
config.review.maxFilesPerRun,
|
||||||
config.review.maxFileContentChars
|
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 started = false;
|
||||||
private activeRunsCount = 0;
|
private activeRunsCount = 0;
|
||||||
@@ -61,7 +65,9 @@ class ReviewEngine {
|
|||||||
this.started = false;
|
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();
|
await this.store.init();
|
||||||
return this.store.createOrReuseRun(payload);
|
return this.store.createOrReuseRun(payload);
|
||||||
}
|
}
|
||||||
@@ -75,7 +81,9 @@ class ReviewEngine {
|
|||||||
return this.store.listRuns(limit);
|
return this.store.listRuns(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRunDetails(runId: string): Promise<Awaited<ReturnType<FileReviewStore['getRunDetails']>>> {
|
async getRunDetails(
|
||||||
|
runId: string
|
||||||
|
): Promise<Awaited<ReturnType<FileReviewStore['getRunDetails']>>> {
|
||||||
return this.store.getRunDetails(runId);
|
return this.store.getRunDetails(runId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import OpenAI from 'openai';
|
||||||
|
import config from '../../config';
|
||||||
|
import { logger } from '../../utils/logger';
|
||||||
import { VectorMemoryStore } from '../memory/vector-store';
|
import { VectorMemoryStore } from '../memory/vector-store';
|
||||||
import { FileReviewStore } from '../store/file-review-store';
|
import { FileReviewStore } from '../store/file-review-store';
|
||||||
import { Finding, FindingCategory } from '../types';
|
import { Finding, FindingCategory } from '../types';
|
||||||
import { logger } from '../../utils/logger';
|
|
||||||
import OpenAI from 'openai';
|
|
||||||
import config from '../../config';
|
|
||||||
|
|
||||||
export class LearningSystem {
|
export class LearningSystem {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -100,7 +100,9 @@ export class LearningSystem {
|
|||||||
{ key: 'approved', match: { value: true } },
|
{ 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[] = [];
|
const examples: OpenAI.Chat.ChatCompletionMessageParam[] = [];
|
||||||
@@ -160,11 +162,7 @@ export class LearningSystem {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async learnFromApproval(
|
async learnFromApproval(finding: Finding, _owner: string, _repo: string): Promise<void> {
|
||||||
finding: Finding,
|
|
||||||
_owner: string,
|
|
||||||
_repo: string
|
|
||||||
): Promise<void> {
|
|
||||||
// 将已批准的finding存储为正样本
|
// 将已批准的finding存储为正样本
|
||||||
await this.memoryStore.storeFinding(finding, true, _owner, _repo);
|
await this.memoryStore.storeFinding(finding, true, _owner, _repo);
|
||||||
|
|
||||||
@@ -199,9 +197,11 @@ export class LearningSystem {
|
|||||||
|
|
||||||
if (maxSimilarity > 0.9) {
|
if (maxSimilarity > 0.9) {
|
||||||
return -0.3; // 高度相似的误报,大幅降低置信度
|
return -0.3; // 高度相似的误报,大幅降低置信度
|
||||||
} else if (maxSimilarity > 0.8) {
|
}
|
||||||
|
if (maxSimilarity > 0.8) {
|
||||||
return -0.15; // 中度相似,适度降低
|
return -0.15; // 中度相似,适度降低
|
||||||
} else if (maxSimilarity > 0.7) {
|
}
|
||||||
|
if (maxSimilarity > 0.7) {
|
||||||
return -0.05; // 低度相似,略微降低
|
return -0.05; // 低度相似,略微降低
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { QdrantClient } from '@qdrant/js-client-rest';
|
import { QdrantClient } from '@qdrant/js-client-rest';
|
||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import { MemoryEntry, MemorySearchResult } from './types';
|
|
||||||
import { Finding } from '../types';
|
|
||||||
import { logger } from '../../utils/logger';
|
import { logger } from '../../utils/logger';
|
||||||
|
import { Finding } from '../types';
|
||||||
|
import { MemoryEntry, MemorySearchResult } from './types';
|
||||||
|
|
||||||
export class VectorMemoryStore {
|
export class VectorMemoryStore {
|
||||||
private client: QdrantClient;
|
private client: QdrantClient;
|
||||||
@@ -70,11 +70,7 @@ export class VectorMemoryStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchSimilar(
|
async searchSimilar(query: string, limit = 5, filter?: any): Promise<MemorySearchResult[]> {
|
||||||
query: string,
|
|
||||||
limit: number = 5,
|
|
||||||
filter?: any
|
|
||||||
): Promise<MemorySearchResult[]> {
|
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
|
|
||||||
const queryEmbedding = await this.getEmbedding(query);
|
const queryEmbedding = await this.getEmbedding(query);
|
||||||
@@ -121,7 +117,12 @@ export class VectorMemoryStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeFinding(finding: Finding, approved: boolean, owner: string, repo: string): Promise<void> {
|
async storeFinding(
|
||||||
|
finding: Finding,
|
||||||
|
approved: boolean,
|
||||||
|
owner: string,
|
||||||
|
repo: string
|
||||||
|
): Promise<void> {
|
||||||
const content = `${finding.title}\n${finding.detail}\nEvidence: ${finding.evidence}`;
|
const content = `${finding.title}\n${finding.detail}\nEvidence: ${finding.evidence}`;
|
||||||
|
|
||||||
// 使用repo-scoped ID防止不同仓库的findings相互覆盖
|
// 使用repo-scoped ID防止不同仓库的findings相互覆盖
|
||||||
|
|||||||
@@ -3,20 +3,20 @@ import OpenAI from 'openai';
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { giteaService } from '../services/gitea';
|
import { giteaService } from '../services/gitea';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
import { DebateOrchestrator } from './agents/debate-orchestrator';
|
||||||
import { JudgeAgent } from './agents/judge-agent';
|
import { JudgeAgent } from './agents/judge-agent';
|
||||||
import { ReflexionAgent } from './agents/reflexion-agent';
|
import { ReflexionAgent } from './agents/reflexion-agent';
|
||||||
import { DebateOrchestrator } from './agents/debate-orchestrator';
|
|
||||||
import { DiffExtractor } from './context/diff-extractor';
|
import { DiffExtractor } from './context/diff-extractor';
|
||||||
import { LocalRepoManager, LocalRepoPaths } from './context/local-repo-manager';
|
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 { applyPublishPolicy } from './policy/publish-policy';
|
||||||
import { FileReviewStore } from './store/file-review-store';
|
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 { createCodeSearchTool } from './tools/code-search-tool';
|
||||||
import { createFunctionReferenceSearchTool } from './tools/function-reference-search-tool';
|
|
||||||
import { createFileReadTool } from './tools/file-read-tool';
|
import { createFileReadTool } from './tools/file-read-tool';
|
||||||
import { VectorMemoryStore } from './memory/vector-store';
|
import { createFunctionReferenceSearchTool } from './tools/function-reference-search-tool';
|
||||||
import { LearningSystem } from './learning/learning-system';
|
import { ToolRegistry } from './tools/registry';
|
||||||
|
import { Finding, ReviewRun } from './types';
|
||||||
|
|
||||||
interface LineCommentInput {
|
interface LineCommentInput {
|
||||||
path: string;
|
path: string;
|
||||||
@@ -24,7 +24,9 @@ interface LineCommentInput {
|
|||||||
comment: string;
|
comment: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findingToLineComment(finding: Omit<Finding, 'id' | 'runId' | 'published'>): LineCommentInput {
|
function findingToLineComment(
|
||||||
|
finding: Omit<Finding, 'id' | 'runId' | 'published'>
|
||||||
|
): LineCommentInput {
|
||||||
return {
|
return {
|
||||||
path: finding.path,
|
path: finding.path,
|
||||||
line: finding.line,
|
line: finding.line,
|
||||||
@@ -172,7 +174,11 @@ export class ReviewOrchestrator {
|
|||||||
startedAt: new Date(contextStart).toISOString(),
|
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({
|
await this.store.addStep({
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
@@ -291,17 +297,17 @@ export class ReviewOrchestrator {
|
|||||||
// summary comment特征:status='published' 且 path字段为空
|
// summary comment特征:status='published' 且 path字段为空
|
||||||
// line comment特征:status='published' 且 path字段存在
|
// line comment特征:status='published' 且 path字段存在
|
||||||
const runDetails = await this.store.getRunDetails(run.id);
|
const runDetails = await this.store.getRunDetails(run.id);
|
||||||
const summaryPublished = runDetails?.comments.some(
|
const summaryPublished =
|
||||||
(comment) => comment.status === 'published' && !comment.path
|
runDetails?.comments.some((comment) => comment.status === 'published' && !comment.path) ||
|
||||||
) || false;
|
false;
|
||||||
const lineCommentsPublished = runDetails?.comments.some(
|
const lineCommentsPublished =
|
||||||
(comment) => comment.status === 'published' && comment.path
|
runDetails?.comments.some((comment) => comment.status === 'published' && comment.path) ||
|
||||||
) || false;
|
false;
|
||||||
|
|
||||||
if (lineCommentsPublished) {
|
if (lineCommentsPublished) {
|
||||||
logger.info('检测到重试且line comments已发布,跳过line comments和findings标记', {
|
logger.info('检测到重试且line comments已发布,跳过line comments和findings标记', {
|
||||||
runId: run.id,
|
runId: run.id,
|
||||||
existingLineComments: runDetails?.comments.filter(c => c.path).length,
|
existingLineComments: runDetails?.comments.filter((c) => c.path).length,
|
||||||
});
|
});
|
||||||
// 重试场景:line comments已发布,跳过line comments发布步骤
|
// 重试场景:line comments已发布,跳过line comments发布步骤
|
||||||
// 注意:不能return,需要继续执行summary和pending gate记录(即使summary已存在)
|
// 注意:不能return,需要继续执行summary和pending gate记录(即使summary已存在)
|
||||||
@@ -362,7 +368,8 @@ export class ReviewOrchestrator {
|
|||||||
// 关键:即使summary已存在,仍需添加gated findings到pending队列
|
// 关键:即使summary已存在,仍需添加gated findings到pending队列
|
||||||
// 防止crash发生在publishSummary之后、addCommentRecord之前时丢失待审批findings
|
// 防止crash发生在publishSummary之后、addCommentRecord之前时丢失待审批findings
|
||||||
// 使用幂等性检查防止retry时重复添加
|
// 使用幂等性检查防止retry时重复添加
|
||||||
const existingPendingComments = runDetails?.comments.filter(c => c.status === 'pending') || [];
|
const existingPendingComments =
|
||||||
|
runDetails?.comments.filter((c) => c.status === 'pending') || [];
|
||||||
|
|
||||||
// 跟踪本次循环中已添加的location,防止同一run中多个findings在同一位置导致重复pending记录
|
// 跟踪本次循环中已添加的location,防止同一run中多个findings在同一位置导致重复pending记录
|
||||||
const addedLocations = new Set<string>();
|
const addedLocations = new Set<string>();
|
||||||
@@ -373,7 +380,7 @@ export class ReviewOrchestrator {
|
|||||||
// 检查是否已存在相同的pending记录(通过runId + path + line去重)
|
// 检查是否已存在相同的pending记录(通过runId + path + line去重)
|
||||||
// 需要同时检查:1) 之前run的记录 2) 本次循环已添加的记录
|
// 需要同时检查:1) 之前run的记录 2) 本次循环已添加的记录
|
||||||
const alreadyPending =
|
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);
|
addedLocations.has(locationKey);
|
||||||
|
|
||||||
if (!alreadyPending) {
|
if (!alreadyPending) {
|
||||||
@@ -398,10 +405,17 @@ export class ReviewOrchestrator {
|
|||||||
// 将已发布的findings存储到向量记忆(自动标记为已批准)
|
// 将已发布的findings存储到向量记忆(自动标记为已批准)
|
||||||
if (this.memoryStore && policyResult.publishable.length > 0) {
|
if (this.memoryStore && policyResult.publishable.length > 0) {
|
||||||
for (const finding of policyResult.publishable) {
|
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) {
|
if (persistedFinding) {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
logger.warn('存储finding到向量记忆失败', {
|
logger.warn('存储finding到向量记忆失败', {
|
||||||
findingId: persistedFinding.id,
|
findingId: persistedFinding.id,
|
||||||
@@ -456,10 +470,13 @@ export class ReviewOrchestrator {
|
|||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
} catch (storeError) {
|
} catch (storeError) {
|
||||||
logger.error('Failed to persist summary comment record (non-fatal, may cause duplicate on retry)', {
|
logger.error(
|
||||||
runId: run.id,
|
'Failed to persist summary comment record (non-fatal, may cause duplicate on retry)',
|
||||||
error: storeError instanceof Error ? storeError.message : String(storeError),
|
{
|
||||||
});
|
runId: run.id,
|
||||||
|
error: storeError instanceof Error ? storeError.message : String(storeError),
|
||||||
|
}
|
||||||
|
);
|
||||||
// 不抛出,允许审查流程继续
|
// 不抛出,允许审查流程继续
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -475,16 +492,22 @@ export class ReviewOrchestrator {
|
|||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
} catch (storeError) {
|
} catch (storeError) {
|
||||||
logger.error('Failed to persist summary comment record (non-fatal, may cause duplicate on retry)', {
|
logger.error(
|
||||||
runId: run.id,
|
'Failed to persist summary comment record (non-fatal, may cause duplicate on retry)',
|
||||||
error: storeError instanceof Error ? storeError.message : String(storeError),
|
{
|
||||||
});
|
runId: run.id,
|
||||||
|
error: storeError instanceof Error ? storeError.message : String(storeError),
|
||||||
|
}
|
||||||
|
);
|
||||||
// 不抛出,允许审查流程继续
|
// 不抛出,允许审查流程继续
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async publishLineComments(run: ReviewRun, comments: LineCommentInput[]): Promise<boolean> {
|
private async publishLineComments(
|
||||||
|
run: ReviewRun,
|
||||||
|
comments: LineCommentInput[]
|
||||||
|
): Promise<boolean> {
|
||||||
if (comments.length === 0) {
|
if (comments.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -518,12 +541,15 @@ export class ReviewOrchestrator {
|
|||||||
body: comment.comment,
|
body: comment.comment,
|
||||||
});
|
});
|
||||||
} catch (storeError) {
|
} catch (storeError) {
|
||||||
logger.error('Failed to persist line comment record (non-fatal, may cause duplicate on retry)', {
|
logger.error(
|
||||||
runId: run.id,
|
'Failed to persist line comment record (non-fatal, may cause duplicate on retry)',
|
||||||
path: comment.path,
|
{
|
||||||
line: comment.line,
|
runId: run.id,
|
||||||
error: storeError instanceof Error ? storeError.message : String(storeError),
|
path: comment.path,
|
||||||
});
|
line: comment.line,
|
||||||
|
error: storeError instanceof Error ? storeError.message : String(storeError),
|
||||||
|
}
|
||||||
|
);
|
||||||
// 不抛出,继续处理下一条comment
|
// 不抛出,继续处理下一条comment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { mkdir, readFile, writeFile, rename } from 'node:fs/promises';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { randomUUID } from 'node:crypto';
|
import { randomUUID } from 'node:crypto';
|
||||||
|
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
import {
|
import {
|
||||||
CommitReviewPayload,
|
CommitReviewPayload,
|
||||||
Finding,
|
Finding,
|
||||||
@@ -76,9 +76,7 @@ export class FileReviewStore {
|
|||||||
this.data = createEmptyData();
|
this.data = createEmptyData();
|
||||||
await this.persist();
|
await this.persist();
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(`Store初始化失败 - 拒绝擦除数据: ${error.message || String(error)}`);
|
||||||
`Store初始化失败 - 拒绝擦除数据: ${error.message || String(error)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +186,10 @@ export class FileReviewStore {
|
|||||||
await this.markRunFinished(runId, 'ignored', reason);
|
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();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
const run = this.data.runs.find((item) => item.id === runId);
|
const run = this.data.runs.find((item) => item.id === runId);
|
||||||
@@ -287,7 +288,12 @@ export class FileReviewStore {
|
|||||||
return runs.slice(0, limit);
|
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();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
const run = this.data.runs.find((item) => item.id === runId);
|
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<void> {
|
private async markRunFinished(
|
||||||
|
runId: string,
|
||||||
|
status: ReviewRunStatus,
|
||||||
|
error?: string
|
||||||
|
): Promise<void> {
|
||||||
await this.ensureInitialized();
|
await this.ensureInitialized();
|
||||||
|
|
||||||
const run = this.data.runs.find((item) => item.id === runId);
|
const run = this.data.runs.find((item) => item.id === runId);
|
||||||
@@ -382,21 +392,20 @@ export class FileReviewStore {
|
|||||||
// 追踪当前write操作是否成功,失败时立即抛出给调用者(防止静默数据丢失)
|
// 追踪当前write操作是否成功,失败时立即抛出给调用者(防止静默数据丢失)
|
||||||
let currentWriteError: Error | null = null;
|
let currentWriteError: Error | null = null;
|
||||||
|
|
||||||
this.writeChain = this.writeChain
|
this.writeChain = this.writeChain.then(async () => {
|
||||||
.then(async () => {
|
try {
|
||||||
try {
|
// 原子写入:先写临时文件,再 rename 覆盖目标文件
|
||||||
// 原子写入:先写临时文件,再 rename 覆盖目标文件
|
// POSIX rename 是原子操作,即使进程在 rename 中间崩溃,文件也不会损坏
|
||||||
// POSIX rename 是原子操作,即使进程在 rename 中间崩溃,文件也不会损坏
|
const tempPath = `${this.statePath}.tmp`;
|
||||||
const tempPath = `${this.statePath}.tmp`;
|
await writeFile(tempPath, JSON.stringify(this.data, null, 2), 'utf-8');
|
||||||
await writeFile(tempPath, JSON.stringify(this.data, null, 2), 'utf-8');
|
await rename(tempPath, this.statePath);
|
||||||
await rename(tempPath, this.statePath);
|
currentWriteError = null; // 写入成功
|
||||||
currentWriteError = null; // 写入成功
|
} catch (error) {
|
||||||
} catch (error) {
|
// 捕获错误但不重新throw,保持chain为resolved状态(允许后续persist()重试)
|
||||||
// 捕获错误但不重新throw,保持chain为resolved状态(允许后续persist()重试)
|
currentWriteError = error instanceof Error ? error : new Error(String(error));
|
||||||
currentWriteError = error instanceof Error ? error : new Error(String(error));
|
console.error('Store persist failed:', currentWriteError);
|
||||||
console.error('Store persist failed:', currentWriteError);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await this.writeChain;
|
await this.writeChain;
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Tool } from './types';
|
|
||||||
import { SandboxExec } from '../context/sandbox-exec';
|
import { SandboxExec } from '../context/sandbox-exec';
|
||||||
|
import { Tool } from './types';
|
||||||
|
|
||||||
export function createCodeSearchTool(sandbox: SandboxExec): Tool {
|
export function createCodeSearchTool(sandbox: SandboxExec): Tool {
|
||||||
return {
|
return {
|
||||||
name: 'search_code',
|
name: 'search_code',
|
||||||
description:
|
description: '在代码库中搜索匹配给定模式的代码,支持正则表达式。用于发现相似问题或影响范围。',
|
||||||
'在代码库中搜索匹配给定模式的代码,支持正则表达式。用于发现相似问题或影响范围。',
|
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
pattern: z.string().describe('要搜索的正则表达式模式'),
|
pattern: z.string().describe('要搜索的正则表达式模式'),
|
||||||
file_types: z
|
file_types: z.array(z.string()).optional().describe('限制搜索的文件类型,如["ts", "js"]'),
|
||||||
.array(z.string())
|
|
||||||
.optional()
|
|
||||||
.describe('限制搜索的文件类型,如["ts", "js"]'),
|
|
||||||
max_results: z.number().default(20).describe('最大返回结果数'),
|
max_results: z.number().default(20).describe('最大返回结果数'),
|
||||||
}),
|
}),
|
||||||
execute: async (params, context) => {
|
execute: async (params, context) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
|
||||||
import { Tool } from './types';
|
|
||||||
import { readFile, realpath } from 'node:fs/promises';
|
import { readFile, realpath } from 'node:fs/promises';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { Tool } from './types';
|
||||||
|
|
||||||
export function createFileReadTool(): Tool {
|
export function createFileReadTool(): Tool {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Tool } from './types';
|
|
||||||
import { SandboxExec } from '../context/sandbox-exec';
|
import { SandboxExec } from '../context/sandbox-exec';
|
||||||
|
import { Tool } from './types';
|
||||||
|
|
||||||
// 转义正则元字符,将identifier中的特殊字符转义为字面量
|
// 转义正则元字符,将identifier中的特殊字符转义为字面量
|
||||||
function escapeRegex(str: string): string {
|
function escapeRegex(str: string): string {
|
||||||
@@ -10,7 +10,8 @@ function escapeRegex(str: string): string {
|
|||||||
export function createFunctionReferenceSearchTool(sandbox: SandboxExec): Tool {
|
export function createFunctionReferenceSearchTool(sandbox: SandboxExec): Tool {
|
||||||
return {
|
return {
|
||||||
name: 'search_function_references',
|
name: 'search_function_references',
|
||||||
description: '搜索指定函数、方法或类的所有引用和定义(支持所有编程语言)。用于理解代码影响范围和调用关系。',
|
description:
|
||||||
|
'搜索指定函数、方法或类的所有引用和定义(支持所有编程语言)。用于理解代码影响范围和调用关系。',
|
||||||
parameters: z.object({
|
parameters: z.object({
|
||||||
identifier: z.string().describe('要搜索的标识符(函数名、类名、方法名等)'),
|
identifier: z.string().describe('要搜索的标识符(函数名、类名、方法名等)'),
|
||||||
file_types: z
|
file_types: z
|
||||||
@@ -31,25 +32,25 @@ export function createFunctionReferenceSearchTool(sandbox: SandboxExec): Tool {
|
|||||||
|
|
||||||
// 定义调用模式(适配多种语言)
|
// 定义调用模式(适配多种语言)
|
||||||
const callPatterns: string[] = [
|
const callPatterns: string[] = [
|
||||||
`${escapedId}\\s*\\(`, // 直接调用: functionName(
|
`${escapedId}\\s*\\(`, // 直接调用: functionName(
|
||||||
`\\.${escapedId}\\s*\\(`, // 方法调用: obj.methodName(
|
`\\.${escapedId}\\s*\\(`, // 方法调用: obj.methodName(
|
||||||
`::${escapedId}\\s*\\(`, // C++/Rust静态调用: Class::method(
|
`::${escapedId}\\s*\\(`, // C++/Rust静态调用: Class::method(
|
||||||
`${escapedId}\\s*<[^>]+>\\s*\\(`, // 泛型调用: functionName<T>( (修复:限制<>内容)
|
`${escapedId}\\s*<[^>]+>\\s*\\(`, // 泛型调用: functionName<T>( (修复:限制<>内容)
|
||||||
];
|
];
|
||||||
|
|
||||||
// 定义声明模式(多语言)
|
// 定义声明模式(多语言)
|
||||||
const definitionPatterns: string[] = [
|
const definitionPatterns: string[] = [
|
||||||
`func\\s+${escapedId}\\s*\\(`, // Go: func functionName(
|
`func\\s+${escapedId}\\s*\\(`, // Go: func functionName(
|
||||||
`fn\\s+${escapedId}\\s*\\(`, // Rust: fn functionName(
|
`fn\\s+${escapedId}\\s*\\(`, // Rust: fn functionName(
|
||||||
`def\\s+${escapedId}\\s*\\(`, // Python: def functionName(
|
`def\\s+${escapedId}\\s*\\(`, // Python: def functionName(
|
||||||
`function\\s+${escapedId}\\s*\\(`, // JavaScript: function functionName(
|
`function\\s+${escapedId}\\s*\\(`, // JavaScript: function functionName(
|
||||||
`${escapedId}\\s*:\\s*function`, // JS对象方法: methodName: function
|
`${escapedId}\\s*:\\s*function`, // JS对象方法: methodName: function
|
||||||
`${escapedId}\\s*=\\s*\\([^)]*\\)\\s*=>`, // Arrow function: const fn = () => (修复:限制参数)
|
`${escapedId}\\s*=\\s*\\([^)]*\\)\\s*=>`, // Arrow function: const fn = () => (修复:限制参数)
|
||||||
`class\\s+${escapedId}\\s*[{<]`, // 类定义: class ClassName {
|
`class\\s+${escapedId}\\s*[{<]`, // 类定义: class ClassName {
|
||||||
`interface\\s+${escapedId}\\s*[{<]`, // 接口: interface InterfaceName {
|
`interface\\s+${escapedId}\\s*[{<]`, // 接口: interface InterfaceName {
|
||||||
`type\\s+${escapedId}\\s*=`, // 类型别名: type TypeName =
|
`type\\s+${escapedId}\\s*=`, // 类型别名: type TypeName =
|
||||||
`struct\\s+${escapedId}\\s*[{]`, // Go/Rust struct: struct StructName {
|
`struct\\s+${escapedId}\\s*[{]`, // Go/Rust struct: struct StructName {
|
||||||
`public\\s+[^(]*\\s+${escapedId}\\s*\\(`, // Java方法: public void methodName(
|
`public\\s+[^(]*\\s+${escapedId}\\s*\\(`, // Java方法: public void methodName(
|
||||||
`private\\s+[^(]*\\s+${escapedId}\\s*\\(`, // Java私有方法
|
`private\\s+[^(]*\\s+${escapedId}\\s*\\(`, // Java私有方法
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -133,7 +134,7 @@ export function createFunctionReferenceSearchTool(sandbox: SandboxExec): Tool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 去重(同一位置可能同时匹配调用和定义模式)
|
// 去重(同一位置可能同时匹配调用和定义模式)
|
||||||
const uniqueRefs = new Map<string, typeof allReferences[0]>();
|
const uniqueRefs = new Map<string, (typeof allReferences)[0]>();
|
||||||
for (const ref of allReferences) {
|
for (const ref of allReferences) {
|
||||||
const key = `${ref.path}:${ref.line}`;
|
const key = `${ref.path}:${ref.line}`;
|
||||||
if (!uniqueRefs.has(key)) {
|
if (!uniqueRefs.has(key)) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
import zodToJsonSchema from 'zod-to-json-schema';
|
import zodToJsonSchema from 'zod-to-json-schema';
|
||||||
import type { JsonSchema7Type } from 'zod-to-json-schema';
|
import type { JsonSchema7Type } from 'zod-to-json-schema';
|
||||||
import { z } from 'zod';
|
|
||||||
import { Tool } from './types';
|
import { Tool } from './types';
|
||||||
|
|
||||||
export class ToolRegistry {
|
export class ToolRegistry {
|
||||||
|
|||||||
@@ -2,20 +2,11 @@ export type ReviewEngineMode = 'legacy' | 'agent';
|
|||||||
|
|
||||||
export type ReviewEventType = 'pull_request' | 'commit_status';
|
export type ReviewEventType = 'pull_request' | 'commit_status';
|
||||||
|
|
||||||
export type ReviewRunStatus =
|
export type ReviewRunStatus = 'queued' | 'in_progress' | 'succeeded' | 'failed' | 'ignored';
|
||||||
| 'queued'
|
|
||||||
| 'in_progress'
|
|
||||||
| 'succeeded'
|
|
||||||
| 'failed'
|
|
||||||
| 'ignored';
|
|
||||||
|
|
||||||
export type FindingSeverity = 'high' | 'medium' | 'low';
|
export type FindingSeverity = 'high' | 'medium' | 'low';
|
||||||
|
|
||||||
export type FindingCategory =
|
export type FindingCategory = 'correctness' | 'security' | 'reliability' | 'maintainability';
|
||||||
| 'correctness'
|
|
||||||
| 'security'
|
|
||||||
| 'reliability'
|
|
||||||
| 'maintainability';
|
|
||||||
|
|
||||||
export interface ReviewRun {
|
export interface ReviewRun {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import OpenAI from 'openai';
|
import OpenAI from 'openai';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import { giteaService, PullRequestFile } from './gitea';
|
import { PullRequestFile, giteaService } from './gitea';
|
||||||
|
|
||||||
// 创建OpenAI客户端
|
// 创建OpenAI客户端
|
||||||
const openai = new OpenAI({
|
const openai = new OpenAI({
|
||||||
@@ -92,7 +92,7 @@ export const aiReviewService = {
|
|||||||
logger.warn('提交差异为空,无法进行代码审查');
|
logger.warn('提交差异为空,无法进行代码审查');
|
||||||
return {
|
return {
|
||||||
summary: '提交差异为空,无法进行代码审查',
|
summary: '提交差异为空,无法进行代码审查',
|
||||||
lineComments: []
|
lineComments: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export const aiReviewService = {
|
|||||||
const context: ReviewContext = {
|
const context: ReviewContext = {
|
||||||
changedFiles: files,
|
changedFiles: files,
|
||||||
fileContents,
|
fileContents,
|
||||||
diffContent
|
diffContent,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用上下文进行总体评价
|
// 使用上下文进行总体评价
|
||||||
@@ -153,7 +153,7 @@ export const aiReviewService = {
|
|||||||
return {
|
return {
|
||||||
changedFiles,
|
changedFiles,
|
||||||
fileContents,
|
fileContents,
|
||||||
diffContent
|
diffContent,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('获取审查上下文失败:', error);
|
logger.error('获取审查上下文失败:', error);
|
||||||
@@ -161,7 +161,7 @@ export const aiReviewService = {
|
|||||||
return {
|
return {
|
||||||
changedFiles: [],
|
changedFiles: [],
|
||||||
fileContents: {},
|
fileContents: {},
|
||||||
diffContent
|
diffContent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -174,13 +174,13 @@ export const aiReviewService = {
|
|||||||
async generateSummary(context: ReviewContext): Promise<string> {
|
async generateSummary(context: ReviewContext): Promise<string> {
|
||||||
try {
|
try {
|
||||||
// 准备上下文信息
|
// 准备上下文信息
|
||||||
const fileInfo = context.changedFiles.map(file => {
|
const fileInfo = context.changedFiles.map((file) => {
|
||||||
return {
|
return {
|
||||||
path: file.filename,
|
path: file.filename,
|
||||||
status: file.status,
|
status: file.status,
|
||||||
additions: file.additions,
|
additions: file.additions,
|
||||||
deletions: file.deletions,
|
deletions: file.deletions,
|
||||||
content: context.fileContents[file.filename] || '无法获取文件内容'
|
content: context.fileContents[file.filename] || '无法获取文件内容',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,9 +205,10 @@ export const aiReviewService = {
|
|||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: '你是一个专业的代码审查助手,擅长识别代码中的严重问题和bug。你会查看代码的完整上下文,而不是为了评论而评论。如无明显问题,应给予简短肯定。'
|
content:
|
||||||
|
'你是一个专业的代码审查助手,擅长识别代码中的严重问题和bug。你会查看代码的完整上下文,而不是为了评论而评论。如无明显问题,应给予简短肯定。',
|
||||||
},
|
},
|
||||||
{ role: 'user', content: summaryPrompt }
|
{ role: 'user', content: summaryPrompt },
|
||||||
],
|
],
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
});
|
});
|
||||||
@@ -234,7 +235,7 @@ export const aiReviewService = {
|
|||||||
// 对每个文件的变更行进行审查
|
// 对每个文件的变更行进行审查
|
||||||
for (const file of diffFiles) {
|
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;
|
if (addedLines.length === 0) continue;
|
||||||
|
|
||||||
// 获取文件的完整内容作为上下文
|
// 获取文件的完整内容作为上下文
|
||||||
@@ -257,7 +258,7 @@ export const aiReviewService = {
|
|||||||
${fileContent}
|
${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格式返回评论,格式如下:
|
请以JSON格式返回评论,格式如下:
|
||||||
[
|
[
|
||||||
@@ -276,9 +277,10 @@ export const aiReviewService = {
|
|||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: '你是一个谨慎的代码审查助手,只对有明显bug或严重问题的代码行提供评论。大多数情况下,如果代码没有严重问题,你应该返回空数组。请以JSON格式返回结果。'
|
content:
|
||||||
|
'你是一个谨慎的代码审查助手,只对有明显bug或严重问题的代码行提供评论。大多数情况下,如果代码没有严重问题,你应该返回空数组。请以JSON格式返回结果。',
|
||||||
},
|
},
|
||||||
{ role: 'user', content: filePrompt }
|
{ role: 'user', content: filePrompt },
|
||||||
],
|
],
|
||||||
temperature: 0.1,
|
temperature: 0.1,
|
||||||
response_format: { type: 'json_object' },
|
response_format: { type: 'json_object' },
|
||||||
@@ -290,7 +292,9 @@ export const aiReviewService = {
|
|||||||
try {
|
try {
|
||||||
// 解析JSON响应
|
// 解析JSON响应
|
||||||
const responseObject = JSON.parse(content);
|
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) {
|
for (const comment of comments) {
|
||||||
@@ -298,7 +302,7 @@ export const aiReviewService = {
|
|||||||
lineComments.push({
|
lineComments.push({
|
||||||
path: file.path,
|
path: file.path,
|
||||||
line: comment.line,
|
line: comment.line,
|
||||||
comment: comment.comment
|
comment: comment.comment,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,17 +325,17 @@ export const aiReviewService = {
|
|||||||
*/
|
*/
|
||||||
parseDiff(diffContent: string): Array<{
|
parseDiff(diffContent: string): Array<{
|
||||||
path: string;
|
path: string;
|
||||||
changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }>
|
changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }>;
|
||||||
}> {
|
}> {
|
||||||
const files: Array<{
|
const files: Array<{
|
||||||
path: string;
|
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');
|
const diffLines = diffContent.split('\n');
|
||||||
let currentFile: {
|
let currentFile: {
|
||||||
path: string;
|
path: string;
|
||||||
changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }>
|
changes: Array<{ lineNumber: number; content: string; type: 'add' | 'context' }>;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
let lineNumber = 0;
|
let lineNumber = 0;
|
||||||
@@ -355,8 +359,8 @@ export const aiReviewService = {
|
|||||||
// Hunk头,记录起始行号
|
// Hunk头,记录起始行号
|
||||||
else if (line.startsWith('@@')) {
|
else if (line.startsWith('@@')) {
|
||||||
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
||||||
if (match && match[1]) {
|
if (match?.[1]) {
|
||||||
lineNumber = parseInt(match[1], 10) - 1; // 因为下面会+1
|
lineNumber = Number.parseInt(match[1], 10) - 1; // 因为下面会+1
|
||||||
inHunk = true;
|
inHunk = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -368,7 +372,7 @@ export const aiReviewService = {
|
|||||||
currentFile.changes.push({
|
currentFile.changes.push({
|
||||||
lineNumber,
|
lineNumber,
|
||||||
content: line.substring(1),
|
content: line.substring(1),
|
||||||
type: 'add'
|
type: 'add',
|
||||||
});
|
});
|
||||||
} else if (line.startsWith(' ')) {
|
} else if (line.startsWith(' ')) {
|
||||||
// 上下文行
|
// 上下文行
|
||||||
@@ -376,7 +380,7 @@ export const aiReviewService = {
|
|||||||
currentFile.changes.push({
|
currentFile.changes.push({
|
||||||
lineNumber,
|
lineNumber,
|
||||||
content: line.substring(1),
|
content: line.substring(1),
|
||||||
type: 'context'
|
type: 'context',
|
||||||
});
|
});
|
||||||
} else if (line.startsWith('-')) {
|
} else if (line.startsWith('-')) {
|
||||||
// 删除的行,不增加行号
|
// 删除的行,不增加行号
|
||||||
@@ -394,5 +398,5 @@ export const aiReviewService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
return files;
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { logger } from '../utils/logger';
|
import * as crypto from 'node:crypto';
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
import * as crypto from 'crypto';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
export class FeishuService {
|
export class FeishuService {
|
||||||
private webhookUrl: string;
|
private webhookUrl: string;
|
||||||
@@ -42,14 +42,14 @@ export class FeishuService {
|
|||||||
const message: any = {
|
const message: any = {
|
||||||
msg_type: 'text',
|
msg_type: 'text',
|
||||||
content: {
|
content: {
|
||||||
text: content
|
text: content,
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果需要@用户,添加at信息
|
// 如果需要@用户,添加at信息
|
||||||
if (usernames.length > 0) {
|
if (usernames.length > 0) {
|
||||||
message.content.text += '\n';
|
message.content.text += '\n';
|
||||||
usernames.forEach(username => {
|
usernames.forEach((username) => {
|
||||||
message.content.text += `@${username} `;
|
message.content.text += `@${username} `;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,11 @@ export class FeishuService {
|
|||||||
* @param issueUrl 工单链接
|
* @param issueUrl 工单链接
|
||||||
* @param assigneeUsernames 被指派人用户名列表
|
* @param assigneeUsernames 被指派人用户名列表
|
||||||
*/
|
*/
|
||||||
async sendIssueCreatedNotification(issueTitle: string, issueUrl: string, assigneeUsernames: string[]): Promise<void> {
|
async sendIssueCreatedNotification(
|
||||||
|
issueTitle: string,
|
||||||
|
issueUrl: string,
|
||||||
|
assigneeUsernames: string[]
|
||||||
|
): Promise<void> {
|
||||||
const content = `📝 新工单已创建\n标题: ${issueTitle}\n链接: ${issueUrl}`;
|
const content = `📝 新工单已创建\n标题: ${issueTitle}\n链接: ${issueUrl}`;
|
||||||
await this.sendMessage(content, assigneeUsernames);
|
await this.sendMessage(content, assigneeUsernames);
|
||||||
}
|
}
|
||||||
@@ -96,7 +100,11 @@ export class FeishuService {
|
|||||||
* @param issueUrl 工单链接
|
* @param issueUrl 工单链接
|
||||||
* @param creatorUsername 创建者用户名
|
* @param creatorUsername 创建者用户名
|
||||||
*/
|
*/
|
||||||
async sendIssueClosedNotification(issueTitle: string, issueUrl: string, creatorUsername: string): Promise<void> {
|
async sendIssueClosedNotification(
|
||||||
|
issueTitle: string,
|
||||||
|
issueUrl: string,
|
||||||
|
creatorUsername: string
|
||||||
|
): Promise<void> {
|
||||||
const content = `✅ 工单已关闭\n标题: ${issueTitle}\n链接: ${issueUrl}`;
|
const content = `✅ 工单已关闭\n标题: ${issueTitle}\n链接: ${issueUrl}`;
|
||||||
await this.sendMessage(content, [creatorUsername]);
|
await this.sendMessage(content, [creatorUsername]);
|
||||||
}
|
}
|
||||||
@@ -107,7 +115,11 @@ export class FeishuService {
|
|||||||
* @param issueUrl 工单链接
|
* @param issueUrl 工单链接
|
||||||
* @param assigneeUsernames 被指派人用户名列表
|
* @param assigneeUsernames 被指派人用户名列表
|
||||||
*/
|
*/
|
||||||
async sendIssueAssignedNotification(issueTitle: string, issueUrl: string, assigneeUsernames: string[]): Promise<void> {
|
async sendIssueAssignedNotification(
|
||||||
|
issueTitle: string,
|
||||||
|
issueUrl: string,
|
||||||
|
assigneeUsernames: string[]
|
||||||
|
): Promise<void> {
|
||||||
const content = `👤 工单已指派给你\n标题: ${issueTitle}\n链接: ${issueUrl}`;
|
const content = `👤 工单已指派给你\n标题: ${issueTitle}\n链接: ${issueUrl}`;
|
||||||
await this.sendMessage(content, assigneeUsernames);
|
await this.sendMessage(content, assigneeUsernames);
|
||||||
}
|
}
|
||||||
@@ -118,7 +130,11 @@ export class FeishuService {
|
|||||||
* @param prUrl PR链接
|
* @param prUrl PR链接
|
||||||
* @param reviewerUsernames 审阅者用户名列表
|
* @param reviewerUsernames 审阅者用户名列表
|
||||||
*/
|
*/
|
||||||
async sendPrCreatedNotification(prTitle: string, prUrl: string, reviewerUsernames: string[]): Promise<void> {
|
async sendPrCreatedNotification(
|
||||||
|
prTitle: string,
|
||||||
|
prUrl: string,
|
||||||
|
reviewerUsernames: string[]
|
||||||
|
): Promise<void> {
|
||||||
const content = `🔄 新PR等待你审阅\n标题: ${prTitle}\n链接: ${prUrl}`;
|
const content = `🔄 新PR等待你审阅\n标题: ${prTitle}\n链接: ${prUrl}`;
|
||||||
await this.sendMessage(content, reviewerUsernames);
|
await this.sendMessage(content, reviewerUsernames);
|
||||||
}
|
}
|
||||||
@@ -129,7 +145,11 @@ export class FeishuService {
|
|||||||
* @param prUrl PR链接
|
* @param prUrl PR链接
|
||||||
* @param reviewerUsernames 审阅者用户名列表
|
* @param reviewerUsernames 审阅者用户名列表
|
||||||
*/
|
*/
|
||||||
async sendPrReviewerAssignedNotification(prTitle: string, prUrl: string, reviewerUsernames: string[]): Promise<void> {
|
async sendPrReviewerAssignedNotification(
|
||||||
|
prTitle: string,
|
||||||
|
prUrl: string,
|
||||||
|
reviewerUsernames: string[]
|
||||||
|
): Promise<void> {
|
||||||
const content = `👀 你被指定为PR审阅者\n标题: ${prTitle}\n链接: ${prUrl}`;
|
const content = `👀 你被指定为PR审阅者\n标题: ${prTitle}\n链接: ${prUrl}`;
|
||||||
await this.sendMessage(content, reviewerUsernames);
|
await this.sendMessage(content, reviewerUsernames);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export interface LineComment {
|
|||||||
const giteaClient = axios.create({
|
const giteaClient = axios.create({
|
||||||
baseURL: config.gitea.apiUrl,
|
baseURL: config.gitea.apiUrl,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `token ${config.gitea.accessToken}`,
|
Authorization: `token ${config.gitea.accessToken}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -21,7 +21,7 @@ const giteaClient = axios.create({
|
|||||||
const giteaAdminClient = axios.create({
|
const giteaAdminClient = axios.create({
|
||||||
baseURL: config.gitea.apiUrl,
|
baseURL: config.gitea.apiUrl,
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `token ${config.admin.giteaAdminToken || config.gitea.accessToken}`,
|
Authorization: `token ${config.admin.giteaAdminToken || config.gitea.accessToken}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'User-Agent': 'curl/7.81.0', // 伪装成 curl
|
'User-Agent': 'curl/7.81.0', // 伪装成 curl
|
||||||
},
|
},
|
||||||
@@ -46,13 +46,22 @@ export interface GiteaService {
|
|||||||
getCommitFiles(owner: string, repo: string, commitSha: string): Promise<PullRequestFile[]>;
|
getCommitFiles(owner: string, repo: string, commitSha: string): Promise<PullRequestFile[]>;
|
||||||
|
|
||||||
// 获取与提交关联的Pull Request
|
// 获取与提交关联的Pull Request
|
||||||
getRelatedPullRequest(owner: string, repo: string, commitSha: string): Promise<PullRequestDetails | null>;
|
getRelatedPullRequest(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
commitSha: string
|
||||||
|
): Promise<PullRequestDetails | null>;
|
||||||
|
|
||||||
// 获取文件内容
|
// 获取文件内容
|
||||||
getFileContent(owner: string, repo: string, path: string, ref?: string): Promise<string>;
|
getFileContent(owner: string, repo: string, path: string, ref?: string): Promise<string>;
|
||||||
|
|
||||||
// 获取引用的相关文件
|
// 获取引用的相关文件
|
||||||
getRelatedFiles(owner: string, repo: string, files: PullRequestFile[], commitSha: string): Promise<Record<string, string>>;
|
getRelatedFiles(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
files: PullRequestFile[],
|
||||||
|
commitSha: string
|
||||||
|
): Promise<Record<string, string>>;
|
||||||
|
|
||||||
// 添加PR评论
|
// 添加PR评论
|
||||||
addPullRequestComment(owner: string, repo: string, prNumber: number, body: string): Promise<void>;
|
addPullRequestComment(owner: string, repo: string, prNumber: number, body: string): Promise<void>;
|
||||||
@@ -70,7 +79,11 @@ export interface GiteaService {
|
|||||||
addCommitComment(owner: string, repo: string, commitSha: string, body: string): Promise<void>;
|
addCommitComment(owner: string, repo: string, commitSha: string, body: string): Promise<void>;
|
||||||
|
|
||||||
// 管理后台方法
|
// 管理后台方法
|
||||||
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<any[]>;
|
listWebhooks(owner: string, repo: string): Promise<any[]>;
|
||||||
createWebhook(owner: string, repo: string, webhookUrl: string): Promise<void>;
|
createWebhook(owner: string, repo: string, webhookUrl: string): Promise<void>;
|
||||||
deleteWebhook(owner: string, repo: string, hookId: number): Promise<void>;
|
deleteWebhook(owner: string, repo: string, hookId: number): Promise<void>;
|
||||||
@@ -118,7 +131,11 @@ export const giteaService: GiteaService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取PR详情
|
// 获取PR详情
|
||||||
async getPullRequestDetails(owner: string, repo: string, prNumber: number): Promise<PullRequestDetails> {
|
async getPullRequestDetails(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
prNumber: number
|
||||||
|
): Promise<PullRequestDetails> {
|
||||||
try {
|
try {
|
||||||
const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls/${prNumber}`);
|
const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls/${prNumber}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
@@ -129,7 +146,11 @@ export const giteaService: GiteaService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取PR变更的文件列表
|
// 获取PR变更的文件列表
|
||||||
async getPullRequestFiles(owner: string, repo: string, prNumber: number): Promise<PullRequestFile[]> {
|
async getPullRequestFiles(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
prNumber: number
|
||||||
|
): Promise<PullRequestFile[]> {
|
||||||
try {
|
try {
|
||||||
const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls/${prNumber}/files`);
|
const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls/${prNumber}/files`);
|
||||||
return response.data || [];
|
return response.data || [];
|
||||||
@@ -153,7 +174,9 @@ export const giteaService: GiteaService = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 使用官方API获取差异,使用diff格式
|
// 使用官方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 || '';
|
return diffResponse.data || '';
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('获取提交差异失败:', error);
|
logger.error('获取提交差异失败:', error);
|
||||||
@@ -175,10 +198,9 @@ export const giteaService: GiteaService = {
|
|||||||
if (response.data.files) {
|
if (response.data.files) {
|
||||||
// 如果API返回了文件列表,则使用它
|
// 如果API返回了文件列表,则使用它
|
||||||
return response.data.files;
|
return response.data.files;
|
||||||
} else {
|
|
||||||
// 否则返回空数组,依赖控制器中webhook提供的文件列表
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
// 否则返回空数组,依赖控制器中webhook提供的文件列表
|
||||||
|
return [];
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('获取提交文件列表失败:', error);
|
logger.error('获取提交文件列表失败:', error);
|
||||||
throw new Error(`获取提交文件列表失败: ${error.message}`);
|
throw new Error(`获取提交文件列表失败: ${error.message}`);
|
||||||
@@ -186,7 +208,11 @@ export const giteaService: GiteaService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取与提交关联的Pull Request
|
// 获取与提交关联的Pull Request
|
||||||
async getRelatedPullRequest(owner: string, repo: string, commitSha: string): Promise<PullRequestDetails | null> {
|
async getRelatedPullRequest(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
commitSha: string
|
||||||
|
): Promise<PullRequestDetails | null> {
|
||||||
try {
|
try {
|
||||||
// 获取仓库中所有开放的PR
|
// 获取仓库中所有开放的PR
|
||||||
const response = await giteaClient.get(`/repos/${owner}/${repo}/pulls?state=open`);
|
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);
|
const prDetails = await giteaService.getPullRequestDetails(owner, repo, pr.number);
|
||||||
|
|
||||||
// 检查PR的提交列表
|
// 检查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 || [];
|
const commits = commitsResponse.data || [];
|
||||||
|
|
||||||
// 检查提交是否在PR中
|
// 检查提交是否在PR中
|
||||||
@@ -239,7 +267,12 @@ export const giteaService: GiteaService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 获取引用的相关文件
|
// 获取引用的相关文件
|
||||||
async getRelatedFiles(owner: string, repo: string, files: PullRequestFile[], commitSha: string): Promise<Record<string, string>> {
|
async getRelatedFiles(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
files: PullRequestFile[],
|
||||||
|
commitSha: string
|
||||||
|
): Promise<Record<string, string>> {
|
||||||
const result: Record<string, string> = {};
|
const result: Record<string, string> = {};
|
||||||
|
|
||||||
// 对每个修改过的文件,获取其完整内容
|
// 对每个修改过的文件,获取其完整内容
|
||||||
@@ -261,7 +294,12 @@ export const giteaService: GiteaService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 添加PR评论
|
// 添加PR评论
|
||||||
async addPullRequestComment(owner: string, repo: string, prNumber: number, body: string): Promise<void> {
|
async addPullRequestComment(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
prNumber: number,
|
||||||
|
body: string
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await giteaClient.post(`/repos/${owner}/${repo}/issues/${prNumber}/comments`, { body });
|
await giteaClient.post(`/repos/${owner}/${repo}/issues/${prNumber}/comments`, { body });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -288,7 +326,7 @@ export const giteaService: GiteaService = {
|
|||||||
await giteaClient.post(`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
|
await giteaClient.post(`/repos/${owner}/${repo}/pulls/${prNumber}/reviews`, {
|
||||||
event: 'COMMENT',
|
event: 'COMMENT',
|
||||||
commit_id: commitId,
|
commit_id: commitId,
|
||||||
comments: comments.map(comment => ({
|
comments: comments.map((comment) => ({
|
||||||
path: comment.path,
|
path: comment.path,
|
||||||
body: comment.comment,
|
body: comment.comment,
|
||||||
new_position: comment.line,
|
new_position: comment.line,
|
||||||
@@ -308,7 +346,7 @@ export const giteaService: GiteaService = {
|
|||||||
commit_id: commitId,
|
commit_id: commitId,
|
||||||
path: comment.path,
|
path: comment.path,
|
||||||
line: comment.line,
|
line: comment.line,
|
||||||
position: comment.line, // Gitea使用position参数表示行号
|
position: comment.line, // Gitea使用position参数表示行号
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
logger.info(`成功逐条添加 ${comments.length} 条评论`);
|
logger.info(`成功逐条添加 ${comments.length} 条评论`);
|
||||||
@@ -320,7 +358,12 @@ export const giteaService: GiteaService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 添加提交评论
|
// 添加提交评论
|
||||||
async addCommitComment(owner: string, repo: string, commitSha: string, body: string): Promise<void> {
|
async addCommitComment(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
commitSha: string,
|
||||||
|
body: string
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await giteaClient.post(`/repos/${owner}/${repo}/git/commits/${commitSha}/comments`, { body });
|
await giteaClient.post(`/repos/${owner}/${repo}/git/commits/${commitSha}/comments`, { body });
|
||||||
} catch (error: any) {
|
} 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 {
|
try {
|
||||||
const response = await giteaAdminClient.get('/repos/search', {
|
const response = await giteaAdminClient.get('/repos/search', {
|
||||||
params: {
|
params: {
|
||||||
@@ -339,7 +386,7 @@ export const giteaService: GiteaService = {
|
|||||||
q: query,
|
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 };
|
return { repos: response.data.data, totalCount };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error('获取所有仓库列表失败:', error);
|
logger.error('获取所有仓库列表失败:', error);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function formatMessage(level: LogLevel, message: string, meta?: any): string {
|
|||||||
if (meta) {
|
if (meta) {
|
||||||
try {
|
try {
|
||||||
formattedMessage += ` - ${JSON.stringify(meta)}`;
|
formattedMessage += ` - ${JSON.stringify(meta)}`;
|
||||||
} catch (error) {
|
} catch (_error) {
|
||||||
formattedMessage += ` - ${meta}`;
|
formattedMessage += ` - ${meta}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user