@ia-ccun/code-agent-cli 0.0.52-release → 0.0.53-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +6 -2
- package/config/agent/bin/README.md +102 -0
- package/config/agent/extensions/aicode-advisor.ts +13 -0
- package/config/agent/extensions/aicode-btw.ts +66 -14
- package/config/agent/extensions/aicode-cd.ts +834 -0
- package/config/agent/extensions/aicode-grillMe.ts +198 -0
- package/config/agent/extensions/aicode-pets.ts +362 -0
- package/config/agent/extensions/aicode-safety-guard.ts +552 -0
- package/config/agent/extensions/aicode-tips.ts +70 -16
- package/config/agent/extensions/aicode-todo-progress.ts +547 -0
- package/config/agent/extensions/custom.ts +93 -95
- package/config/agent/extensions/error-monitor.ts +1 -18
- package/config/agent/extensions/ponytail.ts +441 -0
- package/config/agent/extensions/tps.ts +87 -0
- package/config/agent/extensions/working-msg.ts +165 -50
- package/config/agent/help.md +0 -7
- package/config/agent/models.json +7 -7
- package/config/agent/settings.json +2 -1
- package/config/agent/skills/chroma/LICENSE +21 -0
- package/config/agent/skills/chroma/chroma-local/SKILL.md +165 -0
- package/config/agent/skills/chroma/chroma-local/cli.md +48 -0
- package/config/agent/skills/chroma/chroma-local/data-model.md +103 -0
- package/config/agent/skills/chroma/chroma-local/error-handling/python.md +166 -0
- package/config/agent/skills/chroma/chroma-local/error-handling/typescript.md +231 -0
- package/config/agent/skills/chroma/chroma-local/local-chroma/python.md +95 -0
- package/config/agent/skills/chroma/chroma-local/local-chroma/typescript.md +101 -0
- package/config/agent/skills/chroma/chroma-local/metadata/python.md +154 -0
- package/config/agent/skills/chroma/chroma-local/metadata/typescript.md +153 -0
- package/config/agent/skills/chroma/chroma-local/querying/python.md +147 -0
- package/config/agent/skills/chroma/chroma-local/querying/typescript.md +157 -0
- package/config/agent/skills/chroma/chroma-local/regex/python.md +69 -0
- package/config/agent/skills/chroma/chroma-local/regex/typescript.md +71 -0
- package/config/agent/skills/chroma/chroma-local/understanding-a-codebase.md +80 -0
- package/config/agent/skills/chroma/chroma-local/updating-deleting/python.md +159 -0
- package/config/agent/skills/chroma/chroma-local/updating-deleting/typescript.md +228 -0
- package/config/agent/skills/milvus-skill/LICENSE +202 -0
- package/config/agent/skills/milvus-skill/README.md +37 -0
- package/config/agent/skills/milvus-skill/SKILL.md +259 -0
- package/config/agent/skills/milvus-skill/references/collection.md +86 -0
- package/config/agent/skills/milvus-skill/references/database.md +24 -0
- package/config/agent/skills/milvus-skill/references/index.md +69 -0
- package/config/agent/skills/milvus-skill/references/partition.md +29 -0
- package/config/agent/skills/milvus-skill/references/patterns.md +150 -0
- package/config/agent/skills/milvus-skill/references/user-role.md +90 -0
- package/config/agent/skills/milvus-skill/references/vector.md +247 -0
- package/config/agent/skills/ponytail/SKILL.md +100 -0
- package/config/agent/skills/ponytail-audit/SKILL.md +40 -0
- package/config/agent/skills/ponytail-debt/SKILL.md +44 -0
- package/config/agent/skills/ponytail-help/SKILL.md +68 -0
- package/config/agent/skills/ponytail-review/SKILL.md +56 -0
- package/config/agent/skills/zread/README.md +115 -0
- package/config/agent/skills/zread/SKILL.md +103 -0
- package/config/agent/skills/zread/references/stdio-protocol.md +312 -0
- package/package.json +6 -5
- package/scripts/postinstall.js +58 -1
- package/scripts/regression/README.md +224 -0
- package/scripts/regression/cases/01-version-and-deps.sh +94 -0
- package/scripts/regression/cases/02-extension-loading.sh +130 -0
- package/scripts/regression/cases/03-rpc-protocol.sh +259 -0
- package/scripts/regression/cases/04-prompt-stream.sh +130 -0
- package/scripts/regression/cases/05-slash-commands-and-skills.sh +119 -0
- package/scripts/regression/cases/06-tool-execution.sh +115 -0
- package/scripts/regression/cases/07-pi-new-features.sh +141 -0
- package/scripts/regression/lib/common.sh +287 -0
- package/scripts/regression/results/.last_pid +1 -0
- package/scripts/regression/results/.last_workdir +1 -0
- package/scripts/regression/results/02-extension-loading.log +29 -0
- package/scripts/regression/results/03-prompt-stream.log +25 -0
- package/scripts/regression/results/03-rpc-protocol.log +38 -0
- package/scripts/regression/results/04-slash-commands-and-skills.log +10 -0
- package/scripts/regression/results/05-tool-execution.log +6 -0
- package/scripts/regression/results/06-pi-new-features.log +33 -0
- package/scripts/regression/run-all.sh +187 -0
- package/config/agent/dangerous-patterns.json +0 -106
- package/config/agent/extensions/bash-check.ts +0 -227
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# run-all.sh — 串联所有回归测试用例,输出汇总报告
|
|
3
|
+
# 用法:
|
|
4
|
+
# ./run-all.sh # 跑全部 6 个 case
|
|
5
|
+
# ./run-all.sh --only 01,03,05 # 只跑指定 case
|
|
6
|
+
# ./run-all.sh --quiet # 减少输出
|
|
7
|
+
# ./run-all.sh --aicode /path/to/aicode # 指定二进制路径
|
|
8
|
+
# AICODE_BIN=/path/to/aicode ./run-all.sh
|
|
9
|
+
#
|
|
10
|
+
# 退出码:0 全部通过 / 1 有失败 / 2 环境问题(缺 jq 等)
|
|
11
|
+
|
|
12
|
+
set -uo pipefail
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
|
|
15
|
+
# 参数解析
|
|
16
|
+
ONLY_CASES=""
|
|
17
|
+
QUIET=0
|
|
18
|
+
while [[ $# -gt 0 ]]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--only) ONLY_CASES="$2"; shift 2 ;;
|
|
21
|
+
--quiet|-q) QUIET=1; shift ;;
|
|
22
|
+
--aicode) export AICODE_BIN="$2"; shift 2 ;;
|
|
23
|
+
--help|-h)
|
|
24
|
+
cat <<EOF
|
|
25
|
+
Usage: $0 [--only CASES] [--quiet] [--aicode PATH]
|
|
26
|
+
|
|
27
|
+
Options:
|
|
28
|
+
--only CASES 只跑指定 case(逗号分隔,如 01,03,05)
|
|
29
|
+
--quiet, -q 减少输出
|
|
30
|
+
--aicode PATH 指定 aicode 二进制路径(默认:PATH 中的 aicode)
|
|
31
|
+
--help, -h 显示帮助
|
|
32
|
+
|
|
33
|
+
Environment:
|
|
34
|
+
AICODE_BIN aicode 二进制路径(同 --aicode)
|
|
35
|
+
AICODE_CONFIG 配置目录(同 --config)
|
|
36
|
+
EOF
|
|
37
|
+
exit 0
|
|
38
|
+
;;
|
|
39
|
+
*) echo "Unknown arg: $1"; exit 2 ;;
|
|
40
|
+
esac
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
[[ $QUIET -eq 1 ]] && export REGRESSION_QUIET=1
|
|
44
|
+
|
|
45
|
+
# 前置依赖检查
|
|
46
|
+
echo "═══ aicode-cli 回归测试 ═══"
|
|
47
|
+
echo ""
|
|
48
|
+
echo "AICODE_BIN: ${AICODE_BIN:-aicode}"
|
|
49
|
+
echo "AICODE_CONFIG: ${AICODE_CONFIG:-$HOME/.aicode-cli}"
|
|
50
|
+
echo "SCRIPT_DIR: $SCRIPT_DIR"
|
|
51
|
+
|
|
52
|
+
# jq 必须
|
|
53
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
54
|
+
echo ""
|
|
55
|
+
echo "ERROR: jq 未安装。请:brew install jq"
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# aicode 必须
|
|
60
|
+
if ! command -v "${AICODE_BIN:-aicode}" >/dev/null 2>&1; then
|
|
61
|
+
echo ""
|
|
62
|
+
echo "ERROR: ${AICODE_BIN:-aicode} 未找到。设 AICODE_BIN=/path/to/aicode 或 --aicode PATH"
|
|
63
|
+
exit 2
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
echo ""
|
|
67
|
+
echo "─── 预检 ───"
|
|
68
|
+
"$AICODE_BIN" -v 2>&1 | head -1 | sed 's/^/ version: /'
|
|
69
|
+
echo ""
|
|
70
|
+
|
|
71
|
+
# 收集 case
|
|
72
|
+
ALL_CASES=($(ls "$SCRIPT_DIR/cases/"*.sh 2>/dev/null | sort | xargs -n1 basename))
|
|
73
|
+
if [[ -n "$ONLY_CASES" ]]; then
|
|
74
|
+
IFS=',' read -ra WANTED <<<"$ONLY_CASES"
|
|
75
|
+
FILTERED=()
|
|
76
|
+
for c in "${ALL_CASES[@]}"; do
|
|
77
|
+
for w in "${WANTED[@]}"; do
|
|
78
|
+
# 支持短前缀匹配:"03" 匹配 "03-rpc-protocol.sh"
|
|
79
|
+
if [[ "$c" == "${w}.sh" || "$c" == "${w}-"* ]]; then
|
|
80
|
+
FILTERED+=("$c")
|
|
81
|
+
break
|
|
82
|
+
fi
|
|
83
|
+
done
|
|
84
|
+
done
|
|
85
|
+
if [[ ${#FILTERED[@]} -gt 0 ]]; then
|
|
86
|
+
CASES_TO_RUN=("${FILTERED[@]}")
|
|
87
|
+
else
|
|
88
|
+
CASES_TO_RUN=()
|
|
89
|
+
fi
|
|
90
|
+
else
|
|
91
|
+
CASES_TO_RUN=("${ALL_CASES[@]}")
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if [[ ${#CASES_TO_RUN[@]} -eq 0 ]]; then
|
|
95
|
+
echo "ERROR: 没有匹配的 case (--only=$ONLY_CASES)"
|
|
96
|
+
exit 2
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
echo "将运行 ${#CASES_TO_RUN[@]} 个 case: ${CASES_TO_RUN[*]}"
|
|
100
|
+
echo ""
|
|
101
|
+
|
|
102
|
+
# 跑每个 case
|
|
103
|
+
TOTAL_PASS=0
|
|
104
|
+
TOTAL_FAIL=0
|
|
105
|
+
TOTAL_SKIP=0
|
|
106
|
+
FAILED_CASES=()
|
|
107
|
+
|
|
108
|
+
for case_file in "${CASES_TO_RUN[@]}"; do
|
|
109
|
+
echo "━━━ $case_file ━━━"
|
|
110
|
+
chmod +x "$SCRIPT_DIR/cases/$case_file" 2>/dev/null
|
|
111
|
+
|
|
112
|
+
if "$SCRIPT_DIR/cases/$case_file"; then
|
|
113
|
+
# 从 case 输出里提取 pass/fail 数(不太可靠,但能汇总)
|
|
114
|
+
:
|
|
115
|
+
else
|
|
116
|
+
FAILED_CASES+=("$case_file")
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
# 统计本 case 的 pass/fail/skip
|
|
120
|
+
last_log="$SCRIPT_DIR/results/${case_file%.sh}.log"
|
|
121
|
+
if [[ -f "$last_log" ]]; then
|
|
122
|
+
# 失败标记
|
|
123
|
+
if grep -q "✗" "$last_log" 2>/dev/null; then
|
|
124
|
+
:
|
|
125
|
+
fi
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
# 解析每个 case 的日志
|
|
130
|
+
echo ""
|
|
131
|
+
echo "═══ 汇总 ═══"
|
|
132
|
+
|
|
133
|
+
for case_file in "${CASES_TO_RUN[@]}"; do
|
|
134
|
+
case_name="${case_file%.sh}"
|
|
135
|
+
log="$SCRIPT_DIR/results/${case_name}.log"
|
|
136
|
+
result="?"
|
|
137
|
+
pass_n=0
|
|
138
|
+
fail_n=0
|
|
139
|
+
|
|
140
|
+
if [[ -f "$log" ]]; then
|
|
141
|
+
# 通过 case 退出码判断(case 脚本内部已有 summary + exit)
|
|
142
|
+
# 同时作为备份,使用 grep 统计 stdout 输出
|
|
143
|
+
case_exit_file="$SCRIPT_DIR/results/.case_exit_${case_name}"
|
|
144
|
+
if [[ -f "$case_exit_file" ]]; then
|
|
145
|
+
case_exit=$(cat "$case_exit_file")
|
|
146
|
+
if [[ "$case_exit" -ne 0 ]]; then
|
|
147
|
+
# 整个 case 失败,用 FAIL 计数(不具体到哪个检查)
|
|
148
|
+
fail_n=1
|
|
149
|
+
pass_n=0
|
|
150
|
+
skip_n=0
|
|
151
|
+
else
|
|
152
|
+
pass_n=1
|
|
153
|
+
fail_n=0
|
|
154
|
+
skip_n=0
|
|
155
|
+
fi
|
|
156
|
+
else
|
|
157
|
+
pass_n=0; fail_n=0; skip_n=0
|
|
158
|
+
fi
|
|
159
|
+
else
|
|
160
|
+
pass_n=0; fail_n=0; skip_n=0
|
|
161
|
+
fi
|
|
162
|
+
|
|
163
|
+
TOTAL_PASS=$((TOTAL_PASS + pass_n))
|
|
164
|
+
TOTAL_FAIL=$((TOTAL_FAIL + fail_n))
|
|
165
|
+
TOTAL_SKIP=$((TOTAL_SKIP + skip_n))
|
|
166
|
+
|
|
167
|
+
if [[ $fail_n -gt 0 ]]; then
|
|
168
|
+
printf " %-40s ✗ %d fails\n" "$case_name" "$fail_n"
|
|
169
|
+
else
|
|
170
|
+
printf " %-40s ✓ pass\n" "$case_name"
|
|
171
|
+
fi
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
echo ""
|
|
175
|
+
echo "─── 总计 ───"
|
|
176
|
+
echo " PASS: $TOTAL_PASS"
|
|
177
|
+
echo " FAIL: $TOTAL_FAIL"
|
|
178
|
+
echo " SKIP: $TOTAL_SKIP"
|
|
179
|
+
echo ""
|
|
180
|
+
|
|
181
|
+
if [[ $TOTAL_FAIL -eq 0 ]]; then
|
|
182
|
+
echo "✓ ALL CASES PASSED"
|
|
183
|
+
exit 0
|
|
184
|
+
else
|
|
185
|
+
echo "✗ $TOTAL_FAIL FAILURES — 详见 results/*.log"
|
|
186
|
+
exit 1
|
|
187
|
+
fi
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"_comment": "危险命令模式配置 - 放置位置: config/agent/dangerous-patterns.json",
|
|
3
|
-
"_comment2": "安装后会复制到 ~/.aicode-cli/agent/",
|
|
4
|
-
"_comment3": "commandPrefix: 命令必须以该前缀开头才触发拦截(防止误拦截路径中的字符串)",
|
|
5
|
-
"patterns": [
|
|
6
|
-
{
|
|
7
|
-
"pattern": "rm\\s+-rf",
|
|
8
|
-
"commandPrefix": "rm",
|
|
9
|
-
"name": "rm -rf",
|
|
10
|
-
"risk": "🔴 高危",
|
|
11
|
-
"description": "递归强制删除文件和目录",
|
|
12
|
-
"impact": "删除后数据不可恢复",
|
|
13
|
-
"explanation": "rm -rf 是 Linux/Unix 中最危险的命令之一。-r 表示递归遍历所有子目录,-f 表示强制执行不提示确认。这意味着将无提示地删除指定路径下的所有文件和目录,且无法通过回收站或普通方式恢复。"
|
|
14
|
-
},
|
|
15
|
-
{
|
|
16
|
-
"pattern": "git\\s+reset\\s+--hard",
|
|
17
|
-
"commandPrefix": "git",
|
|
18
|
-
"name": "git reset --hard",
|
|
19
|
-
"risk": "🔴 高危",
|
|
20
|
-
"description": "强制重置到指定提交",
|
|
21
|
-
"impact": "丢失未提交的更改",
|
|
22
|
-
"explanation": "git reset --hard 会将 HEAD 指针移动到指定提交,并丢弃所有未提交的更改。这包括工作区和暂存区的修改,执行后这些更改将永久丢失,无法通过 git 恢复。"
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"pattern": "git\\s+push\\s+(--force|-f)",
|
|
26
|
-
"commandPrefix": "git",
|
|
27
|
-
"name": "git push --force",
|
|
28
|
-
"risk": "🔴 高危",
|
|
29
|
-
"description": "强制推送到远程",
|
|
30
|
-
"impact": "覆盖远程历史,可能丢失他人提交",
|
|
31
|
-
"explanation": "git push --force 会强制用本地分支覆盖远程分支,忽略远程的更新历史。在多人协作项目中,这可能覆盖他人的提交,导致协作者的提交丢失或造成分支冲突。"
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"pattern": "git\\s+push\\s+--delete",
|
|
35
|
-
"commandPrefix": "git",
|
|
36
|
-
"name": "git push --delete",
|
|
37
|
-
"risk": "🔴 高危",
|
|
38
|
-
"description": "删除远程分支",
|
|
39
|
-
"impact": "分支删除后不可恢复",
|
|
40
|
-
"explanation": "git push --delete 会从远程仓库删除指定的分支。分支一旦删除,如果没有本地备份或其他人有该分支的副本,所有提交历史都将永久丢失。"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
"pattern": "git\\s+branch\\s+-D",
|
|
44
|
-
"commandPrefix": "git",
|
|
45
|
-
"name": "git branch -D",
|
|
46
|
-
"risk": "🔴 高危",
|
|
47
|
-
"description": "强制删除本地分支",
|
|
48
|
-
"impact": "分支删除后不可恢复",
|
|
49
|
-
"explanation": "git branch -D 会强制删除本地分支(不检查是否已合并)。所有在该分支上的提交历史都将丢失,除非有其他分支包含这些提交。"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
"pattern": "git\\s+rebase\\s+-i",
|
|
53
|
-
"commandPrefix": "git",
|
|
54
|
-
"name": "git rebase -i",
|
|
55
|
-
"risk": "🔴 高危",
|
|
56
|
-
"description": "交互式变基",
|
|
57
|
-
"impact": "改变提交历史,多人协作时易冲突",
|
|
58
|
-
"explanation": "git rebase -i 允许你交互式地修改、合并、删除或重排提交。这会改变提交的历史哈希值,在已推送的分支上使用会导致后续 push 需要 --force,可能影响协作者。"
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
"pattern": "git\\s+clean\\s+-f(d)?",
|
|
62
|
-
"commandPrefix": "git",
|
|
63
|
-
"name": "git clean",
|
|
64
|
-
"risk": "🔴 高危",
|
|
65
|
-
"description": "删除未跟踪文件",
|
|
66
|
-
"impact": "删除的文件不可恢复",
|
|
67
|
-
"explanation": "git clean 会删除工作区中未被 git 跟踪的文件(新建的文件)。这些文件不在版本控制中,删除后无法通过 git 恢复。"
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
"pattern": "shutdown|halt|init\\s+0",
|
|
71
|
-
"commandPrefix": "shutdown",
|
|
72
|
-
"name": "系统关机",
|
|
73
|
-
"risk": "🔴 高危",
|
|
74
|
-
"description": "关闭或停止系统",
|
|
75
|
-
"impact": "系统将关机或停止服务",
|
|
76
|
-
"explanation": "这些命令会立即关闭或停止系统。如果在服务器上执行,可能导致正在运行的服务中断,数据丢失或损坏。在 SSH 会话中执行可能导致无法重新连接的严重后果。"
|
|
77
|
-
},
|
|
78
|
-
{
|
|
79
|
-
"pattern": "kill\\s+(-9)?\\s*\\$?(1|init|sshd|systemd)",
|
|
80
|
-
"commandPrefix": "kill",
|
|
81
|
-
"name": "杀系统进程",
|
|
82
|
-
"risk": "🔴 高危",
|
|
83
|
-
"description": "终止系统关键进程",
|
|
84
|
-
"impact": "可能导致系统不稳定或崩溃",
|
|
85
|
-
"explanation": "kill 命令用于终止进程。杀死 PID 1 (init/systemd)、sshd 等系统关键进程可能导致系统服务崩溃、SSH 连接中断或系统完全无法响应。"
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
"pattern": "chmod\\s+0[0-7]{3}\\s+\\/",
|
|
89
|
-
"commandPrefix": "chmod",
|
|
90
|
-
"name": "修改根目录权限",
|
|
91
|
-
"risk": "🔴 高危",
|
|
92
|
-
"description": "修改系统关键路径权限",
|
|
93
|
-
"impact": "可能导致系统无法正常启动",
|
|
94
|
-
"explanation": "chmod 修改文件或目录权限。将根目录 / 的权限改为 0xxx 会影响所有系统文件和程序,导致系统服务无法正常运行,甚至无法重启恢复。"
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
"pattern": "dd\\s+if=.*of=\\/(dev|sd)",
|
|
98
|
-
"commandPrefix": "dd",
|
|
99
|
-
"name": "dd 磁盘操作",
|
|
100
|
-
"risk": "🔴 高危",
|
|
101
|
-
"description": "直接写入磁盘设备",
|
|
102
|
-
"impact": "可能导致数据永久丢失",
|
|
103
|
-
"explanation": "dd 直接读取和写入磁盘设备,绕过文件系统。如果目标指向 /dev/sda 等磁盘设备,会直接覆盖磁盘数据,导致分区表、文件系统、所有数据永久丢失,无法恢复。"
|
|
104
|
-
}
|
|
105
|
-
]
|
|
106
|
-
}
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @author: xujianjiang
|
|
3
|
-
* Bash 命令危险检测扩展
|
|
4
|
-
*
|
|
5
|
-
* 放置位置: config/agent/extensions/
|
|
6
|
-
* 安装后会复制到 ~/.aicode-cli/agent/extensions/
|
|
7
|
-
*
|
|
8
|
-
* 拦截危险 bash 命令,执行前需要用户回答数学题验证
|
|
9
|
-
*
|
|
10
|
-
* 危险模式配置: config/agent/dangerous-patterns.json
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { readFileSync, existsSync } from "fs";
|
|
15
|
-
import { join } from "path";
|
|
16
|
-
import { homedir } from "os";
|
|
17
|
-
|
|
18
|
-
// === 加载 JSON 配置文件 ===
|
|
19
|
-
interface DangerPattern {
|
|
20
|
-
pattern: string;
|
|
21
|
-
commandPrefix: string;
|
|
22
|
-
name: string;
|
|
23
|
-
risk: string;
|
|
24
|
-
description: string;
|
|
25
|
-
impact: string;
|
|
26
|
-
explanation: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface DangerPatternsConfig {
|
|
30
|
-
patterns: DangerPattern[];
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function loadDangerPatterns(): DangerPattern[] {
|
|
34
|
-
const configPath = join(homedir(), ".aicode-cli", "agent", "dangerous-patterns.json");
|
|
35
|
-
|
|
36
|
-
if (existsSync(configPath)) {
|
|
37
|
-
try {
|
|
38
|
-
const content = readFileSync(configPath, "utf-8");
|
|
39
|
-
const config: DangerPatternsConfig = JSON.parse(content);
|
|
40
|
-
return config.patterns || [];
|
|
41
|
-
} catch (e) {
|
|
42
|
-
console.error(`[bash-check] Failed to load config from ${configPath}:`, e);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
console.warn("[bash-check] No dangerous-patterns.json found, using empty patterns");
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// === 编译危险模式(同步加载)===
|
|
51
|
-
const patterns = loadDangerPatterns();
|
|
52
|
-
const compiledPatterns = patterns.map(p => ({
|
|
53
|
-
...p,
|
|
54
|
-
compiled: new RegExp(p.pattern, ""),
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
// === 生成数学验证题目 ===
|
|
58
|
-
function generateMathProblem(): { question: string, answer: number } {
|
|
59
|
-
const operators = ['+', '-'];
|
|
60
|
-
const operator = operators[Math.floor(Math.random() * operators.length)];
|
|
61
|
-
let a = Math.floor(Math.random() * 10) + 1;
|
|
62
|
-
let b = Math.floor(Math.random() * 10) + 1;
|
|
63
|
-
|
|
64
|
-
// 确保减法结果非负
|
|
65
|
-
if (operator === '-' && a < b) {
|
|
66
|
-
[a, b] = [b, a];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const question = `${a} ${operator} ${b} = ?`;
|
|
70
|
-
const answer = operator === '+' ? a + b : a - b;
|
|
71
|
-
|
|
72
|
-
return { question, answer };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export default function (pi: ExtensionAPI) {
|
|
76
|
-
// === 拦截 bash 工具 - 检查危险操作 ===
|
|
77
|
-
pi.on("tool_call", async (event, ctx) => {
|
|
78
|
-
if (event.toolName !== "bash") return;
|
|
79
|
-
|
|
80
|
-
let command = event.input.command || "";
|
|
81
|
-
|
|
82
|
-
// 过滤掉 git commit -m 后面的 message 内容,只检查实际命令部分
|
|
83
|
-
const commitMsgMatch = command.match(/^(git\s+commit\s+.*?-m\s+)(.*)$/s);
|
|
84
|
-
if (commitMsgMatch) {
|
|
85
|
-
// 只检查 -m 之前的命令部分,忽略 message 内容
|
|
86
|
-
command = commitMsgMatch[1];
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// 检查是否匹配危险模式
|
|
90
|
-
for (const danger of compiledPatterns) {
|
|
91
|
-
// 1. 首先检查命令是否以指定前缀开头
|
|
92
|
-
const trimmedCmd = command.trim();
|
|
93
|
-
if (!trimmedCmd.startsWith(danger.commandPrefix)) continue;
|
|
94
|
-
|
|
95
|
-
// 2. 然后检查是否匹配危险模式(使用编译后的正则)
|
|
96
|
-
if (!danger.compiled.test(command)) continue;
|
|
97
|
-
|
|
98
|
-
// 解析命令中的路径参数
|
|
99
|
-
const { readdirSync, statSync, existsSync } = await import("fs");
|
|
100
|
-
const details: string[] = [];
|
|
101
|
-
|
|
102
|
-
// 提取相关路径
|
|
103
|
-
const pathMatch = command.match(/(['"])([^\1]+)\1|\S+(?=\s|$)/g);
|
|
104
|
-
if (pathMatch) {
|
|
105
|
-
for (const p of pathMatch) {
|
|
106
|
-
const cleanPath = p.replace(/^['"]|['"]$/g, '').trim();
|
|
107
|
-
if (cleanPath && cleanPath.length > 1 && cleanPath.length < 200) {
|
|
108
|
-
try {
|
|
109
|
-
if (existsSync(cleanPath)) {
|
|
110
|
-
const stat = statSync(cleanPath);
|
|
111
|
-
if (stat.isDirectory()) {
|
|
112
|
-
const files = readdirSync(cleanPath);
|
|
113
|
-
details.push(`📁 ${cleanPath}/ (${files.length} 个文件)`);
|
|
114
|
-
} else {
|
|
115
|
-
details.push(`📄 ${cleanPath}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
} catch (e) {
|
|
119
|
-
// 忽略读取错误
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 构建确认消息
|
|
126
|
-
const truncatedCmd = command.length > 100 ? command.slice(0, 100) + '...' : command;
|
|
127
|
-
const fileInfo = details.length > 0
|
|
128
|
-
? `\n\n🗂️ 将操作的文件:\n${details.slice(0, 5).join('\n')}${details.length > 5 ? `\n... 还有 ${details.length - 5} 个` : ''}`
|
|
129
|
-
: '';
|
|
130
|
-
|
|
131
|
-
// 让用户选择操作
|
|
132
|
-
const choice = await ctx.ui.select(
|
|
133
|
-
`${danger.risk} 危险操作检测: ${danger.name}`,
|
|
134
|
-
[
|
|
135
|
-
`⚠️ 强制执行 (${danger.name})`,
|
|
136
|
-
"🔍 让 AI 解读此命令",
|
|
137
|
-
"❌ 取消操作",
|
|
138
|
-
]
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
if (!choice) {
|
|
142
|
-
ctx.ui.notify("⛔ 操作已取消", "warning");
|
|
143
|
-
return { block: true, reason: "用户取消" };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (choice.includes("取消")) {
|
|
147
|
-
ctx.ui.notify("⛔ 操作已取消", "warning");
|
|
148
|
-
return { block: true, reason: "用户取消" };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (choice.includes("AI 解读")) {
|
|
152
|
-
// 显示详细解释
|
|
153
|
-
const explanation = `
|
|
154
|
-
📋 命令: ${danger.name}
|
|
155
|
-
|
|
156
|
-
📝 描述: ${danger.description}
|
|
157
|
-
|
|
158
|
-
⚠️ 风险等级: ${danger.risk}
|
|
159
|
-
|
|
160
|
-
💥 影响: ${danger.impact}
|
|
161
|
-
|
|
162
|
-
📖 详细解释:
|
|
163
|
-
${danger.explanation}
|
|
164
|
-
|
|
165
|
-
🗂️ 涉及路径:${fileInfo || '\n (无)'}`.trim();
|
|
166
|
-
|
|
167
|
-
ctx.ui.notify(explanation, "info");
|
|
168
|
-
|
|
169
|
-
// 解释后再让用户选择
|
|
170
|
-
const confirmChoice = await ctx.ui.select(
|
|
171
|
-
"请确认操作",
|
|
172
|
-
[
|
|
173
|
-
`⚠️ 我已了解风险,继续执行 (${danger.name})`,
|
|
174
|
-
"❌ 取消操作",
|
|
175
|
-
]
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
if (!confirmChoice || confirmChoice.includes("取消")) {
|
|
179
|
-
ctx.ui.notify("⛔ 操作已取消", "warning");
|
|
180
|
-
return { block: true, reason: "用户取消" };
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 执行数学验证
|
|
184
|
-
const mathProblem = generateMathProblem();
|
|
185
|
-
const mathAnswer = await ctx.ui.input(
|
|
186
|
-
`🔴 最终验证 - 请回答数学题\n\n⚠️ 命令: ${danger.name}\n\n${truncatedCmd}${fileInfo}\n\n📝 请计算: ${mathProblem.question}`,
|
|
187
|
-
"输入答案后回车"
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
if (!mathAnswer) {
|
|
191
|
-
ctx.ui.notify("⛔ 操作已取消", "warning");
|
|
192
|
-
return { block: true, reason: "用户未输入答案" };
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const userAnswer = parseInt(mathAnswer.trim(), 10);
|
|
196
|
-
if (isNaN(userAnswer) || userAnswer !== mathProblem.answer) {
|
|
197
|
-
ctx.ui.notify(`⛔ 答案错误!正确答案是: ${mathProblem.answer}`, "error");
|
|
198
|
-
return { block: true, reason: "数学验证失败" };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
ctx.ui.notify(`✅ 验证通过!执行: ${danger.name}`, "success");
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// 直接执行 - 数学验证
|
|
206
|
-
const mathProblem = generateMathProblem();
|
|
207
|
-
const mathAnswer = await ctx.ui.input(
|
|
208
|
-
`🔴 最终验证 - 请回答数学题\n\n⚠️ 命令: ${danger.name}\n\n${truncatedCmd}${fileInfo}\n\n📝 请计算: ${mathProblem.question}`,
|
|
209
|
-
"输入答案后回车"
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
if (!mathAnswer) {
|
|
213
|
-
ctx.ui.notify("⛔ 操作已取消", "warning");
|
|
214
|
-
return { block: true, reason: "用户未输入答案" };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const userAnswer = parseInt(mathAnswer.trim(), 10);
|
|
218
|
-
if (isNaN(userAnswer) || userAnswer !== mathProblem.answer) {
|
|
219
|
-
ctx.ui.notify(`⛔ 答案错误!正确答案是: ${mathProblem.answer}`, "error");
|
|
220
|
-
return { block: true, reason: "数学验证失败" };
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
ctx.ui.notify(`✅ 验证通过!执行: ${danger.name}`, "success");
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
}
|