mirror of
https://github.com/hustcer/deepseek-review.git
synced 2026-05-13 05:16:05 +08:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3ea289921 | ||
|
|
4af2f6036b | ||
|
|
3dd6b23201 | ||
|
|
c2b26fdc30 | ||
|
|
06d80785b1 | ||
|
|
84dbd263c9 | ||
|
|
c0d78b92b9 | ||
|
|
5652f31856 | ||
|
|
68e582fb95 | ||
|
|
534d6230b7 | ||
|
|
6af37c1d98 | ||
|
|
9b1a390493 | ||
|
|
4249b38d5b | ||
|
|
12e1537975 | ||
|
|
fd3a97c3b6 | ||
|
|
d84f126292 | ||
|
|
8dde3bd95f | ||
|
|
2288595e98 | ||
|
|
d064f64443 | ||
|
|
c3d18ccb03 |
9
.github/workflows/cr.yml
vendored
9
.github/workflows/cr.yml
vendored
@@ -25,9 +25,12 @@ jobs:
|
||||
uses: hustcer/deepseek-review@develop
|
||||
with:
|
||||
max-length: 15000
|
||||
# SiliconFlow's DeepSeek model
|
||||
model: 'deepseek-ai/DeepSeek-V3'
|
||||
base-url: 'https://api.siliconflow.cn/v1'
|
||||
# model: 'deepseek-v3' # Infinigence's DeepSeek V3 model
|
||||
model: 'deepseek-r1' # Infinigence's DeepSeek R1 model
|
||||
base-url: 'https://cloud.infini-ai.com/maas/v1' # Infinigence's API base URL
|
||||
# model: 'deepseek-ai/DeepSeek-V3' # SiliconFlow's DeepSeek V3 model
|
||||
# model: 'deepseek-ai/DeepSeek-R1' # SiliconFlow's DeepSeek R1 model
|
||||
# base-url: 'https://api.siliconflow.cn/v1' # SiliconFlow's API base URL
|
||||
# Store the chat token in GitHub Secrets, don't expose it in the workflow file
|
||||
chat-token: ${{ secrets.CHAT_TOKEN }}
|
||||
sys-prompt: >
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.env
|
||||
.env.dev
|
||||
.env.local
|
||||
config.yml
|
||||
prompts.yaml
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -1,6 +1,45 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [1.11.0] - 2025-02-13
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Do not show reasoning parts if reviewed by DeepSeek V3 model (#121)
|
||||
- Fix `reasoning_content` field may not exists error
|
||||
- Fix `include` & `exclude` pattern matching error (#122)
|
||||
|
||||
## [1.10.0] - 2025-02-12
|
||||
|
||||
### Features
|
||||
|
||||
- Separate the reasoning and the content part of the output (#115)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Improve error handling of API response for streaming-output (#117)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Make `awk` version check works for both `awk` and `gawk` (#109)
|
||||
- Refine DeepSeek review output, clarify debug logs, and adjust error messaging for better context (#111)
|
||||
|
||||
### Performance
|
||||
|
||||
- Save the last reply of the model to db instead of a file (#113)
|
||||
|
||||
## [1.9.0] - 2025-02-12
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix no repo column error for local code review (#102)
|
||||
|
||||
### Features
|
||||
|
||||
- Add streaming output support for local code review (#103)
|
||||
- Add support for custom patch commands by `-c, --patch-cmd` flag in local code review (#106)
|
||||
- Add DeepSeek R1 model support (#107)
|
||||
|
||||
## [1.8.0] - 2025-02-10
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
18
README.md
18
README.md
@@ -9,7 +9,9 @@
|
||||
- Automate PR Reviews with DeepSeek via GitHub Action
|
||||
- Review Remote GitHub PRs Directly from Your Local CLI
|
||||
- Analyze Commit Changes with DeepSeek for Any Local Repository with CLI
|
||||
- Streaming Output Support for Local Code Review
|
||||
- Fully Customizable: Choose Models, Base URLs, and Prompts
|
||||
- Support Both DeepSeek's V3 & R1 Models
|
||||
- Supports Self-Hosted DeepSeek Models for Enhanced Flexibility
|
||||
- Perform Code Reviews for Changes That either Include or Exclude Specific Files
|
||||
- Add `skip cr` or `skip review` to PR title or body to disable code review in GitHub Actions
|
||||
@@ -18,6 +20,7 @@
|
||||
## Planned Features
|
||||
|
||||
- [ ] **Trigger Code Review on Mention**: Automatically initiate code review when the `github-actions` bot is mentioned in a PR comment.
|
||||
- [ ] **Generate Commit Message Locally**: Generate a commit message for the code changes in any local repository.
|
||||
|
||||
## Code Review with GitHub Action
|
||||
|
||||
@@ -153,7 +156,7 @@ To perform code reviews locally(should works for `macOS`, `Ubuntu`, and `Windows
|
||||
Use DeepSeek AI to review code changes locally or in GitHub Actions
|
||||
|
||||
Usage:
|
||||
> cr {flags} (token)
|
||||
> nu cr {flags} (token)
|
||||
|
||||
Flags:
|
||||
-d, --debug: Debug mode
|
||||
@@ -162,13 +165,15 @@ Flags:
|
||||
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||
-t, --diff-to <string>: Diff to git REF
|
||||
-f, --diff-from <string>: Diff from git REF
|
||||
-c, --patch-cmd <string>: The `git show` or `git diff` command to get the diff content, for local CR only
|
||||
-l, --max-length <int>: Maximum length of the content for review, 0 means no limit.
|
||||
-m, --model <string>: Model name, deepseek-chat by default (default: 'deepseek-chat')
|
||||
-b, --base-url <string> (default: 'https://api.deepseek.com')
|
||||
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
|
||||
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
|
||||
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||
-i, --include <string>: Comma separated file patterns to include in the code review
|
||||
-x, --exclude <string>: Comma separated file patterns to exclude in the code review
|
||||
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `1.0`
|
||||
-h, --help: Display the help message for this command
|
||||
|
||||
Parameters:
|
||||
@@ -195,6 +200,13 @@ nu cr
|
||||
nu cr --diff-from f536acc
|
||||
# Perform code review on the `git diff f536acc 0dd0eb5` changes in the local DEFAULT_LOCAL_REPO repo
|
||||
nu cr --diff-from f536acc --diff-to 0dd0eb5
|
||||
# Review the changes in the local `DEFAULT_LOCAL_REPO` repo using the `--patch-cmd` flag
|
||||
nu cr --patch-cmd 'git diff head~3'
|
||||
nu cr -c 'git show head~3'
|
||||
nu cr -c 'git diff 2393375 71f5a31'
|
||||
nu cr -c 'git diff 2393375 71f5a31 nu/*'
|
||||
nu cr -c 'git diff 2393375 71f5a31 :!nu/*'
|
||||
# Dangerous commands like `nu cr -c 'git show head~3; rm ./*'` will not be allowed
|
||||
# Perform code review on PR #31 in the remote DEFAULT_GITHUB_REPO repo
|
||||
nu cr --pr-number 31
|
||||
# Perform code review on PR #31 in the remote hustcer/deepseek-review repo
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
- 通过 GitHub Action 使用 DeepSeek 进行自动化 PR 审查
|
||||
- 通过本地 CLI 直接审查远程 GitHub PR
|
||||
- 通过本地 CLI 使用 DeepSeek 分析任何本地仓库的提交变更
|
||||
- 本地代码审查的时候支持流式输出
|
||||
- 完全可定制:选择模型、基础 URL 和提示词
|
||||
- 支持 DeepSeek V3 和 R1 模型
|
||||
- 支持自托管 DeepSeek 模型,提供更强的灵活性
|
||||
- 在 PR 的标题或描述中添加 `skip cr` or `skip review` 可跳过 GitHub Actions 里的代码审查
|
||||
- 对指定文件变更进行包含/排除式代码审查
|
||||
@@ -16,6 +18,7 @@
|
||||
## 计划支持特性
|
||||
|
||||
- [ ] **通过提及触发代码审查**:当 PR 评论中提及 `github-actions bot` 时,自动触发代码审查
|
||||
- [ ] **本地生成提交信息**:为本地仓库的代码变更生成 Commit Message
|
||||
|
||||
## 通过 GitHub Action 进行代码审查
|
||||
|
||||
@@ -150,7 +153,7 @@ DeepSeek 接口调用入参:
|
||||
Use DeepSeek AI to review code changes locally or in GitHub Actions
|
||||
|
||||
Usage:
|
||||
> cr {flags} (token)
|
||||
> nu cr {flags} (token)
|
||||
|
||||
Flags:
|
||||
-d, --debug: Debug mode
|
||||
@@ -159,13 +162,15 @@ Flags:
|
||||
-k, --gh-token <string>: Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||
-t, --diff-to <string>: Diff to git REF
|
||||
-f, --diff-from <string>: Diff from git REF
|
||||
-c, --patch-cmd <string>: The `git show` or `git diff` command to get the diff content, for local CR only
|
||||
-l, --max-length <int>: Maximum length of the content for review, 0 means no limit.
|
||||
-m, --model <string>: Model name, deepseek-chat by default (default: 'deepseek-chat')
|
||||
-b, --base-url <string> (default: 'https://api.deepseek.com')
|
||||
-m, --model <string>: Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
|
||||
-b, --base-url <string>: DeepSeek API base URL, fallback to BASE_URL env var
|
||||
-s, --sys-prompt <string>: Default to $DEFAULT_OPTIONS.SYS_PROMPT,
|
||||
-u, --user-prompt <string>: Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||
-i, --include <string>: Comma separated file patterns to include in the code review
|
||||
-x, --exclude <string>: Comma separated file patterns to exclude in the code review
|
||||
-T, --temperature <float>: Temperature for the model, between `0` and `2`, default value `1.0`
|
||||
-h, --help: Display the help message for this command
|
||||
|
||||
Parameters:
|
||||
@@ -191,6 +196,13 @@ nu cr
|
||||
nu cr --diff-from f536acc
|
||||
# 对本地 DEFAULT_LOCAL_REPO 仓库 `git diff f536acc 0dd0eb5` 修改内容进行代码审查
|
||||
nu cr --diff-from f536acc --diff-to 0dd0eb5
|
||||
# 通过 --patch-cmd 参数对本地 DEFAULT_LOCAL_REPO 仓库变更内容进行审查
|
||||
nu cr --patch-cmd 'git diff head~3'
|
||||
nu cr -c 'git show head~3'
|
||||
nu cr -c 'git diff 2393375 71f5a31'
|
||||
nu cr -c 'git diff 2393375 71f5a31 nu/*'
|
||||
nu cr -c 'git diff 2393375 71f5a31 :!nu/*'
|
||||
# 像 `nu cr -c 'git show head~3; rm ./*'` 这样危险的命令将会被禁止
|
||||
# 对远程 DEFAULT_GITHUB_REPO 仓库编号为 31 的 PR 进行代码审查
|
||||
nu cr --pr-number 31
|
||||
# 对远程 hustcer/deepseek-review 仓库编号为 31 的 PR 进行代码审查
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
|
||||
words:
|
||||
- psql
|
||||
- MPFR
|
||||
- nuon
|
||||
- nuons
|
||||
- ECODE
|
||||
- endfor
|
||||
- dotenv
|
||||
@@ -16,4 +19,5 @@ words:
|
||||
- linewise
|
||||
- Subshell
|
||||
- subshells
|
||||
- Infinigence
|
||||
ignorePaths:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "deepseek-review",
|
||||
"version": "1.8.0",
|
||||
"actionVer": "v1.8",
|
||||
"version": "1.11.0",
|
||||
"actionVer": "v1.11",
|
||||
"author": "hustcer",
|
||||
"license": "MIT",
|
||||
"github": "https://github.com/hustcer/deepseek-review",
|
||||
|
||||
187
nu/kv.nu
Normal file
187
nu/kv.nu
Normal file
@@ -0,0 +1,187 @@
|
||||
# NOTE: This file was copied from https://github.com/nushell/nushell/tree/main/crates/nu-std/std-rfc/kv
|
||||
# And will be removed after the Nu v0.103 release
|
||||
# More examples could be found here: https://github.com/nushell/nushell/discussions/14965
|
||||
#
|
||||
# kv module
|
||||
#
|
||||
# use std-rfc/kv *
|
||||
#
|
||||
# Easily store and retrieve key-value pairs
|
||||
# in a pipeline.
|
||||
#
|
||||
# A common request is to be able to assign a
|
||||
# pipeline result to a variable. While it's
|
||||
# not currently possible to use a "let" statement
|
||||
# within a pipeline, this module provides an
|
||||
# alternative. Think of each key as a variable
|
||||
# that can be set and retrieved.
|
||||
|
||||
# Stores the pipeline value for later use
|
||||
#
|
||||
# If the key already exists, it is updated to the new value provided.
|
||||
export def "kv set" [
|
||||
key: string
|
||||
value_or_closure?: any
|
||||
--return (-r): string # Whether and what to return to the pipeline output
|
||||
--universal (-u)
|
||||
] {
|
||||
# Pipeline input is preferred, but prioritize
|
||||
# parameter if present. This allows $in to be
|
||||
# used in the parameter if needed.
|
||||
let input = $in
|
||||
|
||||
# If passed a closure, execute it
|
||||
let arg_type = ($value_or_closure | describe)
|
||||
let value = match $arg_type {
|
||||
closure => { $input | do $value_or_closure }
|
||||
_ => ($value_or_closure | default $input)
|
||||
}
|
||||
|
||||
# Store values as nuons for type-integrity
|
||||
let kv_pair = {
|
||||
session: '' # Placeholder
|
||||
key: $key
|
||||
value: ($value | to nuon)
|
||||
}
|
||||
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
try {
|
||||
# Delete the existing key if it does exist
|
||||
do $db_open | query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $key }
|
||||
}
|
||||
|
||||
match $universal {
|
||||
true => { $kv_pair | into sqlite (universal_db_path) -t std_kv_store }
|
||||
false => { $kv_pair | stor insert -t std_kv_store }
|
||||
}
|
||||
|
||||
# The value that should be returned from `kv set`
|
||||
# By default, this is the input to `kv set`, even if
|
||||
# overridden by a positional parameter.
|
||||
# This can also be:
|
||||
# input: (Default) The pipeline input to `kv set`, even if
|
||||
# overridden by a positional parameter. `null` if no
|
||||
# pipeline input was used.
|
||||
# ---
|
||||
# value: If a positional parameter was used for the value, then
|
||||
# return it, otherwise return the input (whatever was set).
|
||||
# If the positional was a closure, return the result of the
|
||||
# closure on the pipeline input.
|
||||
# ---
|
||||
# all: The entire contents of the existing kv table are returned
|
||||
match ($return | default 'input') {
|
||||
'all' => (kv list --universal=$universal)
|
||||
'a' => (kv list --universal=$universal)
|
||||
'value' => $value
|
||||
'v' => $value
|
||||
'input' => $input
|
||||
'in' => $input
|
||||
'i' => $input
|
||||
_ => {
|
||||
error make {
|
||||
msg: "Invalid --return option"
|
||||
label: {
|
||||
text: "Must be 'all'/'a', 'value'/'v', or 'input'/'in'/'i'"
|
||||
span: (metadata $return).span
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Retrieves a stored value by key
|
||||
#
|
||||
# Counterpart of "kv set". Returns null if the key is not found.
|
||||
export def "kv get" [
|
||||
key: string # Key of the kv-pair to retrieve
|
||||
--universal (-u)
|
||||
] {
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
do $db_open
|
||||
# Hack to turn a SQLiteDatabase into a table
|
||||
| $in.std_kv_store | wrap temp | get temp
|
||||
| where key == $key
|
||||
# Should only be one occurrence of each key in the stor
|
||||
| get -i value.0
|
||||
| match $in {
|
||||
# Key not found
|
||||
null => null
|
||||
# Key found
|
||||
_ => { from nuon }
|
||||
}
|
||||
}
|
||||
|
||||
# List the currently stored key-value pairs
|
||||
#
|
||||
# Returns results as the Nushell value rather than the stored nuon.
|
||||
export def "kv list" [
|
||||
--universal (-u)
|
||||
] {
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
|
||||
do $db_open | $in.std_kv_store? | each {|kv_pair|
|
||||
{
|
||||
key: $kv_pair.key
|
||||
value: ($kv_pair.value | from nuon )
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Returns and removes a key-value pair
|
||||
export def --env "kv drop" [
|
||||
key: string # Key of the kv-pair to drop
|
||||
--universal (-u)
|
||||
] {
|
||||
let db_open = (db_setup --universal=$universal)
|
||||
|
||||
let value = (kv get --universal=$universal $key)
|
||||
|
||||
try {
|
||||
do $db_open
|
||||
# Hack to turn a SQLiteDatabase into a table
|
||||
| query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $key }
|
||||
}
|
||||
|
||||
if $universal and ($env.NU_KV_UNIVERSALS? | default false) {
|
||||
hide-env $key
|
||||
}
|
||||
|
||||
$value
|
||||
}
|
||||
|
||||
def universal_db_path [] {
|
||||
$env.NU_UNIVERSAL_KV_PATH?
|
||||
| default (
|
||||
$nu.data-dir | path join "std_kv_variables.sqlite3"
|
||||
)
|
||||
}
|
||||
|
||||
def db_setup [
|
||||
--universal
|
||||
] : nothing -> closure {
|
||||
try {
|
||||
match $universal {
|
||||
true => {
|
||||
# Ensure universal sqlite db and table exists
|
||||
let uuid = (random uuid)
|
||||
let dummy_record = {
|
||||
session: ''
|
||||
key: $uuid
|
||||
value: ''
|
||||
}
|
||||
$dummy_record | into sqlite (universal_db_path) -t std_kv_store
|
||||
open (universal_db_path) | query db "DELETE FROM std_kv_store WHERE key = :key" --params { key: $uuid }
|
||||
}
|
||||
false => {
|
||||
# Create the stor table if it doesn't exist
|
||||
stor create -t std_kv_store -c {session: str, key: str, value: str} | ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Return the correct closure for opening on-disk vs. in-memory
|
||||
match $universal {
|
||||
true => {{|| open (universal_db_path)}}
|
||||
false => {{|| stor open}}
|
||||
}
|
||||
}
|
||||
184
nu/review.nu
184
nu/review.nu
@@ -7,6 +7,8 @@
|
||||
# [√] Debug mode
|
||||
# [√] Output token usage info
|
||||
# [√] Perform CR for changes that either include or exclude specific files
|
||||
# [√] Support streaming output for local code review
|
||||
# [√] Support using custom patch command to get diff content
|
||||
# [ ] Add more action outputs
|
||||
# Description: A script to do code review by DeepSeek
|
||||
# REF:
|
||||
@@ -23,6 +25,8 @@
|
||||
# - Local Repo Review: just cr -f HEAD~1 --debug
|
||||
# - Local PR Review: just cr -r hustcer/deepseek-review -n 32
|
||||
|
||||
use kv.nu *
|
||||
|
||||
# Commonly used exit codes
|
||||
export const ECODE = {
|
||||
SUCCESS: 0,
|
||||
@@ -35,6 +39,8 @@ export const ECODE = {
|
||||
CONDITION_NOT_SATISFIED: 8,
|
||||
}
|
||||
|
||||
const RESPONSE_END = 'data: [DONE]'
|
||||
|
||||
const GITHUB_API_BASE = 'https://api.github.com'
|
||||
|
||||
# It takes longer to respond to requests made with unknown/rare user agents.
|
||||
@@ -61,6 +67,7 @@ export def --env deepseek-review [
|
||||
--gh-token(-k): string, # Your GitHub token, fallback to GITHUB_TOKEN env var
|
||||
--diff-to(-t): string, # Diff to git REF
|
||||
--diff-from(-f): string, # Diff from git REF
|
||||
--patch-cmd(-c): string, # The `git show` or `git diff` command to get the diff content, for local CR only
|
||||
--max-length(-l): int, # Maximum length of the content for review, 0 means no limit.
|
||||
--model(-m): string, # Model name, or read from CHAT_MODEL env var, `deepseek-chat` by default
|
||||
--base-url(-b): string, # DeepSeek API base URL, fallback to BASE_URL env var
|
||||
@@ -68,11 +75,12 @@ export def --env deepseek-review [
|
||||
--user-prompt(-u): string # Default to $DEFAULT_OPTIONS.USER_PROMPT,
|
||||
--include(-i): string, # Comma separated file patterns to include in the code review
|
||||
--exclude(-x): string, # Comma separated file patterns to exclude in the code review
|
||||
--temperature: float, # Temperature for the model
|
||||
--temperature(-T): float, # Temperature for the model, between `0` and `2`, default value `1.0`, Only for V3
|
||||
]: nothing -> nothing {
|
||||
|
||||
$env.config.table.mode = 'psql'
|
||||
let is_action = ($env.GITHUB_ACTIONS? == 'true')
|
||||
let stream = if $is_action { false } else { true }
|
||||
let token = $token | default $env.CHAT_TOKEN?
|
||||
let repo = $repo | default $env.DEFAULT_GITHUB_REPO?
|
||||
let CHAT_HEADER = [Authorization $'Bearer ($token)']
|
||||
@@ -94,6 +102,7 @@ export def --env deepseek-review [
|
||||
exclude: $exclude,
|
||||
diff_to: $diff_to,
|
||||
diff_from: $diff_from,
|
||||
patch_cmd: $patch_cmd,
|
||||
pr_number: $pr_number,
|
||||
max_length: $max_length,
|
||||
local_repo: $local_repo,
|
||||
@@ -111,11 +120,14 @@ export def --env deepseek-review [
|
||||
$'🚀 Initiate the code review by DeepSeek AI for PR (ansi g)#($pr_number)(ansi reset) in (ansi g)($repo)(ansi reset) ...'
|
||||
}
|
||||
print $hint; print -n (char nl)
|
||||
if ($pr_number | is-empty) { $setting | compact-record | reject repo | print }
|
||||
if ($pr_number | is-empty) {
|
||||
print 'Current Settings:'; hr-line
|
||||
$setting | compact-record | reject -i repo | print; print -n (char nl)
|
||||
}
|
||||
|
||||
let content = (
|
||||
get-diff --pr-number $pr_number --repo $repo --diff-to $diff_to
|
||||
--diff-from $diff_from --include $include --exclude $exclude)
|
||||
--diff-from $diff_from --include $include --exclude $exclude --patch-cmd $patch_cmd)
|
||||
let length = $content | str stats | get unicode-width
|
||||
if ($max_length != 0) and ($length > $max_length) {
|
||||
print $'(char nl)(ansi r)The content length ($length) exceeds the maximum limit ($max_length), review skipped.(ansi reset)'
|
||||
@@ -126,41 +138,90 @@ export def --env deepseek-review [
|
||||
let user_prompt = $user_prompt | default (load-prompt-from-env USER_PROMPT) | default $DEFAULT_OPTIONS.USER_PROMPT
|
||||
let payload = {
|
||||
model: $model,
|
||||
stream: false,
|
||||
stream: $stream,
|
||||
temperature: $temperature,
|
||||
messages: [
|
||||
{ role: 'system', content: $sys_prompt },
|
||||
{ role: 'user', content: $"($user_prompt):\n($content)" }
|
||||
]
|
||||
}
|
||||
if $debug { print $'Code Changes:'; hr-line; print $content }
|
||||
print $'(char nl)Waiting for response from (ansi g)($url)(ansi reset) ...'
|
||||
if $debug { print $'(char nl)Code Changes:'; hr-line; print $content }
|
||||
print $'(char nl)Waiting for response from (ansi g)($base_url)(ansi reset) ...'
|
||||
if $stream { streaming-output $url $payload --headers $CHAT_HEADER --debug=$debug; return }
|
||||
|
||||
let response = http post -e -H $CHAT_HEADER -t application/json $url $payload
|
||||
if ($response | is-empty) {
|
||||
print $'(ansi r)Oops, No response returned from DeepSeek API.(ansi reset)'
|
||||
print $'(ansi r)Oops, No response returned from ($base_url) ...(ansi reset)'
|
||||
exit $ECODE.SERVER_ERROR
|
||||
}
|
||||
if $debug { print $'DeepSeek Response:'; hr-line; $response | table -e | print }
|
||||
if $debug { print $'DeepSeek Model Response:'; hr-line; $response | table -e | print }
|
||||
if ($response | describe) == 'string' {
|
||||
print $'❌ Code review failed!Error: '; hr-line; print $response
|
||||
print $'✖️ Code review failed!Error: '; hr-line; print $response
|
||||
exit $ECODE.SERVER_ERROR
|
||||
}
|
||||
let reason = $response | get -i choices.0.message.reasoning_content
|
||||
let review = $response | get -i choices.0.message.content
|
||||
let result = ['<details>' '<summary> Reasoning Details</summary>' $reason "</details>\n" $review] | str join "\n"
|
||||
if ($review | is-empty) {
|
||||
print $'❌ Code review failed!No review result returned from DeepSeek API.'
|
||||
print $'✖️ Code review failed!No review result returned from ($base_url) ...'
|
||||
exit $ECODE.SERVER_ERROR
|
||||
}
|
||||
let result = if ($reason | is-empty) { $review } else { $result }
|
||||
if not $is_action {
|
||||
print $'Code Review Result:'; hr-line; print $review
|
||||
print $'Code Review Result:'; hr-line; print $result
|
||||
} else {
|
||||
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json ...$HTTP_HEADERS]
|
||||
http post -t application/json -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/issues/($pr_number)/comments' { body: $review }
|
||||
http post -t application/json -H $BASE_HEADER $'($GITHUB_API_BASE)/repos/($repo)/issues/($pr_number)/comments' { body: $result }
|
||||
print $'✅ Code review finished!PR (ansi g)#($pr_number)(ansi reset) review result was posted as a comment.'
|
||||
}
|
||||
print $'(char nl)Token Usage Info:'; hr-line
|
||||
print $'(char nl)Token Usage:'; hr-line
|
||||
$response.usage | table -e | print
|
||||
}
|
||||
|
||||
# Output the streaming response of review result from DeepSeek API
|
||||
def streaming-output [
|
||||
url: string, # The Full DeepSeek API URL
|
||||
payload: record, # The payload to send to DeepSeek API
|
||||
--debug, # Debug mode
|
||||
--headers: list, # The headers to send to DeepSeek API
|
||||
] {
|
||||
print -n (char nl)
|
||||
kv set content 0
|
||||
kv set reasoning 0
|
||||
http post -e -H $headers -t application/json $url $payload
|
||||
| tee {
|
||||
let res = $in
|
||||
let type = $res | describe
|
||||
let record_error = $type =~ '^record'
|
||||
let other_error = $type =~ '^string' and $res !~ 'data: '
|
||||
if $record_error or $other_error {
|
||||
$res | table -e | print
|
||||
exit $ECODE.SERVER_ERROR
|
||||
}
|
||||
}
|
||||
| try { lines } catch { print $'(ansi r)Error Happened ...(ansi reset)'; exit $ECODE.SERVER_ERROR }
|
||||
| each {|line|
|
||||
if $line == $RESPONSE_END { return }
|
||||
if ($line | is-empty) { return }
|
||||
let $last = $line | str substring 6.. | from json
|
||||
if $last == '-alive' { print $last; return }
|
||||
if $debug { $last | to json | kv set last-reply }
|
||||
$last | get -i choices.0.delta | if ($in | is-not-empty) {
|
||||
let delta = $in
|
||||
if ($delta.reasoning_content? | is-not-empty) { kv set reasoning ((kv get reasoning) + 1) }
|
||||
if (kv get reasoning) == 1 { print $'(char nl)Reasoning Details:'; hr-line }
|
||||
if ($delta.content | is-not-empty) { kv set content ((kv get content) + 1) }
|
||||
if (kv get content) == 1 { print $'(char nl)Review Details:'; hr-line }
|
||||
print -n ($delta.reasoning_content? | default $delta.content)
|
||||
}
|
||||
}
|
||||
|
||||
if $debug and (kv get last-reply | is-not-empty) {
|
||||
print $'(char nl)(char nl)Model & Token Usage:'; hr-line
|
||||
kv get last-reply | from json | select -i model usage | table -e | print
|
||||
}
|
||||
}
|
||||
|
||||
# Load the prompt content from the specified env var
|
||||
export def load-prompt-from-env [
|
||||
prompt_key: string,
|
||||
@@ -188,6 +249,7 @@ export def get-diff [
|
||||
--diff-from: string, # Diff from git ref
|
||||
--include: string, # Comma separated file patterns to include in the code review
|
||||
--exclude: string, # Comma separated file patterns to exclude in the code review
|
||||
--patch-cmd: string, # The `git show` or `git diff` command to get the diff content
|
||||
] {
|
||||
let BASE_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3+json]
|
||||
let DIFF_HEADER = [Authorization $'Bearer ($env.GH_TOKEN)' Accept application/vnd.github.v3.diff]
|
||||
@@ -223,13 +285,17 @@ export def get-diff [
|
||||
} else if not (git-check $local_repo --check-repo=1) {
|
||||
print $'Current directory ($local_repo) is (ansi r)NOT(ansi reset) a git repo, bye...(char nl)'
|
||||
exit $ECODE.CONDITION_NOT_SATISFIED
|
||||
} else if ($patch_cmd | is-not-empty) {
|
||||
let valid = is-safe-git $patch_cmd
|
||||
if not $valid { exit $ECODE.INVALID_PARAMETER }
|
||||
nu -c $patch_cmd
|
||||
} else { git diff }
|
||||
|
||||
if ($content | is-empty) {
|
||||
print $'(ansi g)Nothing to review.(ansi reset)'; exit $ECODE.SUCCESS
|
||||
}
|
||||
let awk_bin = (prepare-awk)
|
||||
let outdated_awk = $'You may using an (ansi r)outdated awk version(ansi reset), please upgrade to the latest version or use gawk latest instead.'
|
||||
let outdated_awk = $'If you are using an (ansi r)outdated awk version(ansi reset), please upgrade to the latest version or use gawk latest instead.'
|
||||
if ($include | is-not-empty) {
|
||||
let patterns = $include | split row ','
|
||||
$content = $content | try { ^$awk_bin (generate-include-regex $patterns) } catch { print $outdated_awk; exit $ECODE.OUTDATED }
|
||||
@@ -249,7 +315,10 @@ export def prepare-awk [] {
|
||||
let gawk_installed = is-installed gawk
|
||||
|
||||
if $awk_installed {
|
||||
let awk_version = awk --version | lines | first | split row ' ' | last
|
||||
# AWK family version check for both awk and gawk
|
||||
# awk: awk version 20250116 -> 20250116
|
||||
# gawk: GNU Awk 5.3.1, API 4.0, (GNU MPFR 4.2.1, GNU MP 6.3.0) -> 5.3.1
|
||||
let awk_version = awk --version | lines | first | split row , | first | split row ' ' | last
|
||||
print $'Current awk version: ($awk_version)'
|
||||
if (compare-ver $awk_version $MIN_AWK_VERSION) >= 0 { return 'awk' }
|
||||
}
|
||||
@@ -337,18 +406,33 @@ export def hr-line [
|
||||
}
|
||||
|
||||
print $'(ansi $color)(build-line $width)(if $with_arrow {'>'})(ansi reset)'
|
||||
if $blank_line { char nl }
|
||||
if $blank_line { char nl | print -n }
|
||||
}
|
||||
|
||||
# Convert glob patterns to regex patterns
|
||||
# Pass in *.nu directly as a regular expression does not work, because * in
|
||||
# a regular expression needs to be attached to the previous pattern, the correct
|
||||
# form should be .* So we should convert each glob pattern to a regex pattern:
|
||||
# 1. Convert * to .*
|
||||
# 2. Convert ? to . (optional, as needed)
|
||||
# 3. Convert / to \/
|
||||
def glob-to-regex [patterns: list<string>] {
|
||||
$patterns
|
||||
| each { |pat|
|
||||
$pat | str replace "*" ".*" | str replace "?" "." | str replace "/" "\\/"
|
||||
}
|
||||
| str join "|"
|
||||
}
|
||||
|
||||
# Generate the awk include regex pattern string for the specified patterns
|
||||
export def generate-include-regex [patterns: list<string>] {
|
||||
let pattern = $patterns | each {|pat| $pat | str replace '/' '\/' } | str join '|'
|
||||
let pattern = glob-to-regex $patterns
|
||||
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}p"
|
||||
}
|
||||
|
||||
# Generate the awk exclude regex pattern string for the specified patterns
|
||||
def generate-exclude-regex [patterns: list<string>] {
|
||||
let pattern = $patterns | each {|pat| $pat | str replace '/' '\/' } | str join '|'
|
||||
export def generate-exclude-regex [patterns: list<string>] {
|
||||
let pattern = glob-to-regex $patterns
|
||||
$"/^diff --git/{p=/^diff --git a\\/($pattern)/}!p"
|
||||
}
|
||||
|
||||
@@ -398,4 +482,66 @@ export def compare-ver [v1: string, v2: string] {
|
||||
0
|
||||
}
|
||||
|
||||
# Check if the git command is safe to run in the shell
|
||||
# Validate command examples:
|
||||
# - git show
|
||||
# - git diff
|
||||
# - git show head~1
|
||||
# - git diff --since=2025-02-09 HEAD
|
||||
# - git diff 2393375 71f5a31
|
||||
# - git diff 2393375 71f5a31 nu/*
|
||||
# - git diff 2393375 71f5a31 :!nu/*
|
||||
export def is-safe-git [cmd: string] {
|
||||
# Normalize the command string by trimming and converting to lowercase
|
||||
let normalized_cmd = ($cmd | str trim | str downcase)
|
||||
|
||||
# More strict regex for git commands, allow:
|
||||
# 1. --since parameter with ISO date format
|
||||
# 2. File path patterns with or without colon (e.g. :!nu/*, nu/*)
|
||||
let allowed_regex = '^git\s+(show|diff)(?:\s+(?:--since=\d{4}-\d{2}-\d{2}|[a-zA-Z0-9_\-\.~/]+))*(?:\s+(?::[!]?)?[a-zA-Z0-9_\-\.\*\/]+)?$'
|
||||
|
||||
# Dangerous patterns to check (expanded list)
|
||||
let dangerous_patterns = [
|
||||
# Command chaining/injection
|
||||
';', '&&', '||', '|',
|
||||
# Shell expansion
|
||||
'?', '[', ']', '{', '}',
|
||||
# Command substitution
|
||||
'`', '$(',
|
||||
# IO redirection
|
||||
'>', '>>', '<', '<<',
|
||||
# Special characters
|
||||
'\n', '\r', '\t',
|
||||
# Path traversal
|
||||
'..',
|
||||
# Environment variables
|
||||
'$', '%',
|
||||
# Quotes that might be used for injection
|
||||
'"', "'"
|
||||
]
|
||||
|
||||
# First check: Command must match the allowed pattern
|
||||
if ($normalized_cmd | find -r $allowed_regex | is-empty) {
|
||||
print $'ERROR: Invalid git command format. (ansi r)Only simple `git show` or `git diff` commands are allowed(ansi reset).'
|
||||
return false
|
||||
}
|
||||
|
||||
# Second check: No dangerous patterns allowed
|
||||
for pattern in $dangerous_patterns {
|
||||
if ($cmd | str contains $pattern) {
|
||||
print $'(ansi r)ERROR: Dangerous pattern detected: `($pattern)`(ansi reset)'
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
# Third check: Command parts validation (increased limit to accommodate path patterns)
|
||||
let cmd_parts = $normalized_cmd | split row ' '
|
||||
if ($cmd_parts | length) > 6 {
|
||||
print $'ERROR: Command too complex. (ansi r)Only simple `git show` or `git diff` commands are allowed(ansi reset).'
|
||||
return false
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
alias main = deepseek-review
|
||||
|
||||
Reference in New Issue
Block a user