jfox-cli 0.2.0__tar.gz → 0.2.2__tar.gz
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.
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/CLAUDE.md +1 -1
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/PKG-INFO +1 -1
- jfox_cli-0.2.2/docs/superpowers/specs/2026-04-13-pr-auto-code-review-design.md +286 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/__init__.py +1 -1
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/cli.py +173 -12
- jfox_cli-0.2.2/jfox/git_extractor.py +178 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/performance.py +4 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/pyproject.toml +1 -1
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skills-recommend/claude-code/jfox-common/SKILL.md +91 -7
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skills-recommend/claude-code/jfox-ingest/SKILL.md +43 -31
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skills-recommend/claude-code/jfox-organize/SKILL.md +13 -3
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_cli_format.py +49 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_bm25_batch.py +55 -0
- jfox_cli-0.2.2/tests/unit/test_git_extractor.py +284 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_index_kb_param.py +41 -0
- jfox_cli-0.2.2/tests/unit/test_logging_config.py +34 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/uv.lock +1 -1
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/.githooks/pre-push +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/.github/workflows/integration-test.yml +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/.github/workflows/publish.yml +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/.gitignore +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/.python-version +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/AGENTS.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/CHANGELOG.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/DEVELOPMENT_PLAN.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/README.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/SESSION.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/SESSION_SUMMARY.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-11-bulk-import-bm25-fix.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-11-edit-command.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-11-unify-format-option.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-ci-coverage-optimization.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-edit-content-file.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-fix-index-rebuild-clear.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-fix-index-verify-id-mismatch.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-fix-jfox-health-skill-kb-param.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-index-kb-param.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-lazy-import-perf.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/plans/2026-04-12-skill-redesign.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/specs/2026-04-03-bugfixes-design.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/docs/superpowers/specs/2026-04-12-skill-redesign-design.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jessica-jones-static-cable.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/__main__.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/bm25_index.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/config.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/embedding_backend.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/formatters.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/global_config.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/graph.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/indexer.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/kb_manager.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/models.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/note.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/search_engine.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/template.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/template_cli.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/jfox/vector_store.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/pytest.ini +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/run_full_test.ps1 +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skill/evals/evals.json +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skill/knowledge-base-notes/SKILL.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skill/knowledge-base-workspace/SKILL.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skills-recommend/README.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/skills-recommend/claude-code/jfox-search/SKILL.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/COVERAGE_PLAN.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/MIGRATION.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/TESTS.md +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/conftest.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/integration/__init__.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/integration/test_backlinks.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/performance/__init__.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/performance/test_performance.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_advanced_features.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_config_unit.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_core_workflow.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_hybrid_search.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_integration.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_kb_current.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/test_suggest_links.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/__init__.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_edit.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_format_unify.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_formatters.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_global_config.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_indexer_clear_before_rebuild.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_indexer_verify.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_kb_manager.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_lazy_import.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_template.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_template_cli.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/unit/test_vector_store_clear.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/utils/__init__.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/utils/assertions.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/utils/jfox_cli.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/utils/note_generator.py +0 -0
- {jfox_cli-0.2.0 → jfox_cli-0.2.2}/tests/utils/temp_kb.py +0 -0
|
@@ -91,7 +91,7 @@ Notes are Markdown files with YAML frontmatter stored under `~/.zettelkasten/<kb
|
|
|
91
91
|
|
|
92
92
|
## Conventions
|
|
93
93
|
|
|
94
|
-
- **Version bump**: 发版时必须同时修改 `pyproject.toml` 和 `
|
|
94
|
+
- **Version bump**: 发版时必须同时修改 `pyproject.toml`、`jfox/__init__.py` 和 `uv.lock` 三处版本号。先改前两个文件,再跑 `uv lock` 更新 lock 文件(曾有 #88 遗漏 `__init__.py` 的教训)
|
|
95
95
|
- **Line length**: 100 chars (black + ruff configured in `pyproject.toml`)
|
|
96
96
|
- **Comments/docs**: Chinese (中文)
|
|
97
97
|
- **Adding a CLI command**: Add `@app.command()` in `cli.py`, implement `_xxx_impl()` helper, add `--kb` and `--format json` support
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
# PR 自动 Code Review 系统设计
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
本系统实现当 GitHub 上任何仓库创建或更新 PR 时,自动触发 Claude Code 的 code review,并将结果提交到 PR 评论中。
|
|
6
|
+
|
|
7
|
+
**核心目标:**
|
|
8
|
+
- 全自动:PR 创建/更新后自动触发,无需人工干预
|
|
9
|
+
- 多仓库:支持监控账号下所有可访问的仓库
|
|
10
|
+
- 轻量级:单 Python 脚本,资源占用低
|
|
11
|
+
- 易部署:Windows 环境一键启动
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 架构
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────┐ ┌─────────────┐ ┌──────────────────────────┐
|
|
19
|
+
│ GitHub Webhook │────▶│ smee.io │────▶│ Flask Webhook Receiver │
|
|
20
|
+
│ (PR opened/ │ │ (proxy) │ │ (Windows 本地运行) │
|
|
21
|
+
│ synchronize) │ └─────────────┘ └──────────┬───────────────┘
|
|
22
|
+
└─────────────────┘ │
|
|
23
|
+
▼
|
|
24
|
+
┌──────────────────────┐
|
|
25
|
+
│ 调用 Claude Code │
|
|
26
|
+
│ claude code │
|
|
27
|
+
│ --print │
|
|
28
|
+
│ /code-review:code-review <PR_URL>
|
|
29
|
+
└──────────┬───────────┘
|
|
30
|
+
│
|
|
31
|
+
▼
|
|
32
|
+
┌──────────────────────┐
|
|
33
|
+
│ 提交 Review 评论 │
|
|
34
|
+
│ (gh pr review ...) │
|
|
35
|
+
└──────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 组件说明
|
|
41
|
+
|
|
42
|
+
### 1. smee.io(Webhook 代理)
|
|
43
|
+
|
|
44
|
+
GitHub Webhook 需要公网地址,smee.io 是 GitHub 官方提供的免费代理服务,将 webhook 事件转发到本地。
|
|
45
|
+
|
|
46
|
+
- 访问 https://smee.io 获取一个唯一 URL
|
|
47
|
+
- 本地客户端连接到 smee.io,接收事件
|
|
48
|
+
- 无需暴露本地端口到公网
|
|
49
|
+
|
|
50
|
+
### 2. Flask Webhook 接收器
|
|
51
|
+
|
|
52
|
+
核心服务,负责:
|
|
53
|
+
|
|
54
|
+
1. **接收事件**:监听来自 smee.io 的 POST 请求
|
|
55
|
+
2. **过滤事件**:只处理 `pull_request.opened` 和 `pull_request.synchronize`
|
|
56
|
+
3. **提取信息**:从 payload 获取 PR URL、仓库、分支等信息
|
|
57
|
+
4. **触发 Review**:调用 Claude Code 执行 code review
|
|
58
|
+
5. **提交结果**:使用 `gh` CLI 将 review 结果提交到 PR
|
|
59
|
+
|
|
60
|
+
**技术栈:**
|
|
61
|
+
- Python 3.10+
|
|
62
|
+
- Flask(轻量级 web 框架)
|
|
63
|
+
- requests(HTTP 客户端)
|
|
64
|
+
|
|
65
|
+
### 3. Claude Code 调用
|
|
66
|
+
|
|
67
|
+
通过 subprocess 调用本地安装的 Claude Code:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
claude code --print /code-review:code-review <PR_URL>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Claude Code 会分析 PR 并返回 review 结果(JSON 或文本格式)。
|
|
74
|
+
|
|
75
|
+
### 4. GitHub CLI (gh)
|
|
76
|
+
|
|
77
|
+
用于提交 review 评论,复用已有的认证:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
gh pr review <PR_URL> --comment -b "<review_result>"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 数据流
|
|
86
|
+
|
|
87
|
+
### PR 创建场景
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
1. 用户在 GitHub 创建 PR
|
|
91
|
+
2. GitHub 发送 webhook 到配置的 smee.io URL
|
|
92
|
+
3. smee.io 转发到本地 Flask 服务
|
|
93
|
+
4. Flask 验证事件签名(可选)
|
|
94
|
+
5. Flask 提取 PR URL: https://github.com/owner/repo/pull/123
|
|
95
|
+
6. Flask 调用: claude code --print /code-review:code-review <URL>
|
|
96
|
+
7. Claude Code 返回 review 结果
|
|
97
|
+
8. Flask 调用: gh pr review <URL> --comment -b "<result>"
|
|
98
|
+
9. 评论出现在 PR 中
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### PR 更新场景(push 新代码)
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
1. 用户 push 新 commit 到 PR 分支
|
|
105
|
+
2. GitHub 发送 `pull_request.synchronize` 事件
|
|
106
|
+
3. 同上流程,重新执行 code review
|
|
107
|
+
4. 新评论追加到 PR
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 配置
|
|
113
|
+
|
|
114
|
+
配置文件:`config.json`
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"smee_url": "https://smee.io/YOUR_UNIQUE_CHANNEL",
|
|
119
|
+
"port": 3000,
|
|
120
|
+
"log_level": "INFO",
|
|
121
|
+
"review_on_open": true,
|
|
122
|
+
"review_on_sync": true,
|
|
123
|
+
"skip_drafts": true,
|
|
124
|
+
"max_pr_age_hours": 24
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**配置项说明:**
|
|
129
|
+
|
|
130
|
+
| 配置项 | 说明 | 默认值 |
|
|
131
|
+
|--------|------|--------|
|
|
132
|
+
| `smee_url` | smee.io 提供的唯一 URL | 必填 |
|
|
133
|
+
| `port` | 本地 Flask 服务端口 | 3000 |
|
|
134
|
+
| `log_level` | 日志级别 | INFO |
|
|
135
|
+
| `review_on_open` | PR 创建时自动 review | true |
|
|
136
|
+
| `review_on_sync` | PR 更新时自动 review | true |
|
|
137
|
+
| `skip_drafts` | 跳过 Draft PR | true |
|
|
138
|
+
| `max_pr_age_hours` | 只 review 24 小时内创建的 PR(防止误触发历史 PR) | 24 |
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## 安装与运行
|
|
143
|
+
|
|
144
|
+
### 前置依赖
|
|
145
|
+
|
|
146
|
+
1. Python 3.10+
|
|
147
|
+
2. Claude Code 已安装并登录
|
|
148
|
+
3. GitHub CLI (gh) 已安装并登录
|
|
149
|
+
|
|
150
|
+
### 安装
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
# 克隆或下载项目
|
|
154
|
+
git clone <repo>
|
|
155
|
+
cd pr-auto-reviewer
|
|
156
|
+
|
|
157
|
+
# 创建虚拟环境
|
|
158
|
+
python -m venv venv
|
|
159
|
+
venv\Scripts\activate
|
|
160
|
+
|
|
161
|
+
# 安装依赖
|
|
162
|
+
pip install -r requirements.txt
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 配置
|
|
166
|
+
|
|
167
|
+
1. 访问 https://smee.io 获取一个新的 channel URL
|
|
168
|
+
2. 复制 `config.example.json` 为 `config.json`
|
|
169
|
+
3. 将 smee_url 填入配置
|
|
170
|
+
|
|
171
|
+
### 启动
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# 方式1:直接运行(前台)
|
|
175
|
+
python webhook_server.py
|
|
176
|
+
|
|
177
|
+
# 方式2:后台运行(Windows)
|
|
178
|
+
start /B python webhook_server.py
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 配置 GitHub Webhook
|
|
182
|
+
|
|
183
|
+
对于要监控的仓库,在 Settings > Webhooks 中添加:
|
|
184
|
+
|
|
185
|
+
- **Payload URL**: 你的 smee.io URL (如 `https://smee.io/abc123`)
|
|
186
|
+
- **Content type**: `application/json`
|
|
187
|
+
- **Events**: 选择 "Pull requests"
|
|
188
|
+
- **Active**: 勾选
|
|
189
|
+
|
|
190
|
+
**注意**:每个仓库都需要单独配置 webhook。如果想要监控所有仓库,可以考虑使用 GitHub App(需要额外配置)。
|
|
191
|
+
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
## 错误处理
|
|
195
|
+
|
|
196
|
+
| 错误场景 | 处理方式 |
|
|
197
|
+
|----------|----------|
|
|
198
|
+
| smee.io 连接断开 | 自动重连,指数退避 |
|
|
199
|
+
| Claude Code 调用失败 | 记录日志,重试 3 次 |
|
|
200
|
+
| gh CLI 认证过期 | 记录错误,通知用户 |
|
|
201
|
+
| PR 不存在或无权访问 | 跳过,记录警告 |
|
|
202
|
+
| 网络超时 | 重试 3 次,每次间隔 5 秒 |
|
|
203
|
+
| Review 结果为空 | 跳过提交评论 |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 日志
|
|
208
|
+
|
|
209
|
+
日志输出到控制台和文件 `logs/webhook.log`:
|
|
210
|
+
|
|
211
|
+
```
|
|
212
|
+
2026-04-13 10:30:15 [INFO] 收到 webhook: pull_request.opened, repo=owner/repo, pr=#123
|
|
213
|
+
2026-04-13 10:30:15 [INFO] 开始 review: https://github.com/owner/repo/pull/123
|
|
214
|
+
2026-04-13 10:31:02 [INFO] Review 完成,提交评论
|
|
215
|
+
2026-04-13 10:31:03 [INFO] 评论提交成功
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## 安全考虑
|
|
221
|
+
|
|
222
|
+
1. **Webhook 签名验证**(可选):
|
|
223
|
+
- 配置 GitHub webhook secret
|
|
224
|
+
- 本地验证 HMAC 签名,防止伪造请求
|
|
225
|
+
|
|
226
|
+
2. **Token 安全**:
|
|
227
|
+
- 复用 gh CLI 的认证,不存储 PAT
|
|
228
|
+
- gh CLI 使用系统密钥管理器存储 token
|
|
229
|
+
|
|
230
|
+
3. **访问控制**:
|
|
231
|
+
- Flask 服务只绑定 localhost(`127.0.0.1`)
|
|
232
|
+
- 不暴露到公网
|
|
233
|
+
|
|
234
|
+
4. **日志脱敏**:
|
|
235
|
+
- 不记录敏感信息
|
|
236
|
+
- PR URL 等基础信息正常记录
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 扩展性
|
|
241
|
+
|
|
242
|
+
### 未来可扩展的功能
|
|
243
|
+
|
|
244
|
+
1. **更细粒度的控制**:
|
|
245
|
+
- 按仓库配置不同的 review 规则
|
|
246
|
+
- 支持 `.github/code-review-config.yml`
|
|
247
|
+
|
|
248
|
+
2. **更多触发条件**:
|
|
249
|
+
- 只在特定标签的 PR 上触发
|
|
250
|
+
- 只在特定分支的 PR 上触发
|
|
251
|
+
|
|
252
|
+
3. **结果通知**:
|
|
253
|
+
- 发送到 Slack/飞书
|
|
254
|
+
- 发送邮件通知
|
|
255
|
+
|
|
256
|
+
4. **Review 缓存**:
|
|
257
|
+
- 对相同 commit 的 PR 返回缓存结果
|
|
258
|
+
- 减少 API 调用和计算成本
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 文件结构
|
|
263
|
+
|
|
264
|
+
```
|
|
265
|
+
pr-auto-reviewer/
|
|
266
|
+
├── webhook_server.py # 主服务
|
|
267
|
+
├── config.py # 配置加载
|
|
268
|
+
├── github_client.py # GitHub API 封装
|
|
269
|
+
├── reviewer.py # Claude Code 调用封装
|
|
270
|
+
├── requirements.txt # 依赖
|
|
271
|
+
├── config.example.json # 配置示例
|
|
272
|
+
├── config.json # 实际配置(gitignore)
|
|
273
|
+
├── logs/ # 日志目录
|
|
274
|
+
│ └── webhook.log
|
|
275
|
+
└── README.md # 使用说明
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 成功标准
|
|
281
|
+
|
|
282
|
+
- [ ] PR 创建后 1 分钟内自动触发 review
|
|
283
|
+
- [ ] Review 结果成功提交到 PR 评论
|
|
284
|
+
- [ ] PR 更新后自动重新 review
|
|
285
|
+
- [ ] 服务稳定运行 7 天无崩溃
|
|
286
|
+
- [ ] 支持同时监控多个仓库
|
|
@@ -37,6 +37,19 @@ from .template_cli import template_app
|
|
|
37
37
|
logging.basicConfig(
|
|
38
38
|
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
39
39
|
)
|
|
40
|
+
|
|
41
|
+
# 抑制第三方库的 INFO/DEBUG 日志,避免污染 CLI 输出
|
|
42
|
+
for _lib in (
|
|
43
|
+
"sentence_transformers",
|
|
44
|
+
"torch",
|
|
45
|
+
"chromadb",
|
|
46
|
+
"tqdm",
|
|
47
|
+
"urllib3",
|
|
48
|
+
"watchdog",
|
|
49
|
+
"PIL",
|
|
50
|
+
):
|
|
51
|
+
logging.getLogger(_lib).setLevel(logging.WARNING)
|
|
52
|
+
|
|
40
53
|
logger = logging.getLogger(__name__)
|
|
41
54
|
|
|
42
55
|
# 创建应用
|
|
@@ -365,7 +378,7 @@ def _add_note_impl(
|
|
|
365
378
|
|
|
366
379
|
@app.command()
|
|
367
380
|
def add(
|
|
368
|
-
content: str = typer.Argument(
|
|
381
|
+
content: Optional[str] = typer.Argument(None, help="笔记内容(支持 [[笔记标题]] 格式链接)"),
|
|
369
382
|
title: Optional[str] = typer.Option(None, "--title", "-t", help="笔记标题"),
|
|
370
383
|
note_type: str = typer.Option(
|
|
371
384
|
"fleeting", "--type", help="笔记类型 (fleeting/literature/permanent)"
|
|
@@ -375,6 +388,9 @@ def add(
|
|
|
375
388
|
template: Optional[str] = typer.Option(
|
|
376
389
|
None, "--template", "-T", help="使用模板创建笔记 (quick/meeting/literature)"
|
|
377
390
|
),
|
|
391
|
+
content_file: Optional[str] = typer.Option(
|
|
392
|
+
None, "--content-file", help="从文件读取内容(用 - 表示 stdin)"
|
|
393
|
+
),
|
|
378
394
|
kb: Optional[str] = typer.Option(None, "--kb", "-k", help="目标知识库名称"),
|
|
379
395
|
output_format: str = typer.Option("table", "--format", "-f", help="输出格式: json, table"),
|
|
380
396
|
json_output: bool = typer.Option(
|
|
@@ -387,6 +403,18 @@ def add(
|
|
|
387
403
|
if json_output:
|
|
388
404
|
output_format = "json"
|
|
389
405
|
|
|
406
|
+
# content 和 --content-file 互斥
|
|
407
|
+
if content is not None and content_file is not None:
|
|
408
|
+
raise ValueError("不能同时指定内容参数和 --content-file,请选择其一")
|
|
409
|
+
|
|
410
|
+
# 从文件读取内容
|
|
411
|
+
if content_file is not None:
|
|
412
|
+
content = _read_content_file(content_file)
|
|
413
|
+
|
|
414
|
+
# 至少提供一种内容来源
|
|
415
|
+
if not content:
|
|
416
|
+
raise ValueError("请提供笔记内容(位置参数或 --content-file)")
|
|
417
|
+
|
|
390
418
|
# 如果指定了知识库,临时切换
|
|
391
419
|
if kb:
|
|
392
420
|
from .config import use_kb
|
|
@@ -956,6 +984,26 @@ def delete(
|
|
|
956
984
|
raise typer.Exit(1)
|
|
957
985
|
|
|
958
986
|
|
|
987
|
+
def _read_content_file(content_file: str) -> str:
|
|
988
|
+
"""从文件或 stdin 读取内容(--content-file 共用逻辑)"""
|
|
989
|
+
if content_file == "-":
|
|
990
|
+
import sys
|
|
991
|
+
|
|
992
|
+
return sys.stdin.read()
|
|
993
|
+
|
|
994
|
+
p = Path(content_file)
|
|
995
|
+
if not p.exists():
|
|
996
|
+
raise ValueError(f"文件不存在: {content_file}")
|
|
997
|
+
if not p.is_file():
|
|
998
|
+
raise ValueError(f"路径不是文件: {content_file}")
|
|
999
|
+
try:
|
|
1000
|
+
return p.read_text(encoding="utf-8")
|
|
1001
|
+
except PermissionError:
|
|
1002
|
+
raise ValueError(f"无权限读取文件: {content_file}")
|
|
1003
|
+
except UnicodeDecodeError:
|
|
1004
|
+
raise ValueError(f"文件编码错误(需要 UTF-8): {content_file}")
|
|
1005
|
+
|
|
1006
|
+
|
|
959
1007
|
def _edit_impl(
|
|
960
1008
|
note_id: str,
|
|
961
1009
|
content: Optional[str],
|
|
@@ -973,17 +1021,7 @@ def _edit_impl(
|
|
|
973
1021
|
|
|
974
1022
|
# 从文件读取内容
|
|
975
1023
|
if content_file is not None:
|
|
976
|
-
|
|
977
|
-
if not p.exists():
|
|
978
|
-
raise ValueError(f"文件不存在: {content_file}")
|
|
979
|
-
if not p.is_file():
|
|
980
|
-
raise ValueError(f"路径不是文件: {content_file}")
|
|
981
|
-
try:
|
|
982
|
-
content = p.read_text(encoding="utf-8")
|
|
983
|
-
except PermissionError:
|
|
984
|
-
raise ValueError(f"无权限读取文件: {content_file}")
|
|
985
|
-
except UnicodeDecodeError:
|
|
986
|
-
raise ValueError(f"文件编码错误(需要 UTF-8): {content_file}")
|
|
1024
|
+
content = _read_content_file(content_file)
|
|
987
1025
|
|
|
988
1026
|
# 验证:至少指定一个编辑字段
|
|
989
1027
|
if all(v is None for v in [content, title, tags, note_type, source]):
|
|
@@ -1702,15 +1740,29 @@ def _index_impl(action: str, output_format: str):
|
|
|
1702
1740
|
console.print("[yellow]Rebuilding index...[/yellow]")
|
|
1703
1741
|
count = indexer.index_all()
|
|
1704
1742
|
|
|
1743
|
+
# 同时重建 BM25 索引
|
|
1744
|
+
from . import note as note_module
|
|
1745
|
+
from .bm25_index import get_bm25_index
|
|
1746
|
+
|
|
1747
|
+
bm25_index = get_bm25_index()
|
|
1748
|
+
notes = note_module.list_notes(limit=10000)
|
|
1749
|
+
bm25_success = bm25_index.rebuild_from_notes(notes)
|
|
1750
|
+
|
|
1705
1751
|
result = {
|
|
1706
1752
|
"success": True,
|
|
1707
1753
|
"indexed": count,
|
|
1754
|
+
"bm25_rebuilt": bm25_success,
|
|
1755
|
+
"bm25_indexed": len(notes),
|
|
1708
1756
|
}
|
|
1709
1757
|
|
|
1710
1758
|
if output_format == "json":
|
|
1711
1759
|
print(output_json(result))
|
|
1712
1760
|
else:
|
|
1713
1761
|
console.print(f"[green]✓[/green] Indexed {count} notes")
|
|
1762
|
+
if bm25_success:
|
|
1763
|
+
console.print(f"[green]✓[/green] BM25 index rebuilt: {len(notes)} notes")
|
|
1764
|
+
else:
|
|
1765
|
+
console.print("[yellow]⚠[/yellow] ChromaDB rebuilt, but BM25 rebuild failed")
|
|
1714
1766
|
|
|
1715
1767
|
elif action == "verify":
|
|
1716
1768
|
verification = indexer.verify_index()
|
|
@@ -2155,6 +2207,115 @@ def kb(
|
|
|
2155
2207
|
# =============================================================================
|
|
2156
2208
|
|
|
2157
2209
|
|
|
2210
|
+
def _ingest_log_impl(
|
|
2211
|
+
repo_path: str,
|
|
2212
|
+
limit: int,
|
|
2213
|
+
note_type: str,
|
|
2214
|
+
batch_size: int,
|
|
2215
|
+
output_format: str,
|
|
2216
|
+
json_output: bool,
|
|
2217
|
+
):
|
|
2218
|
+
"""从 Git 仓库提取 commit 历史并导入为笔记"""
|
|
2219
|
+
from .git_extractor import commits_to_notes, extract_commits
|
|
2220
|
+
from .performance import bulk_import_notes
|
|
2221
|
+
|
|
2222
|
+
# 提取 commits
|
|
2223
|
+
commits = extract_commits(repo_path, limit=limit)
|
|
2224
|
+
|
|
2225
|
+
if not commits:
|
|
2226
|
+
result = {
|
|
2227
|
+
"success": True,
|
|
2228
|
+
"imported": 0,
|
|
2229
|
+
"total": 0,
|
|
2230
|
+
"message": "没有找到 commit 记录",
|
|
2231
|
+
}
|
|
2232
|
+
if output_format == "json":
|
|
2233
|
+
print(output_json(result))
|
|
2234
|
+
else:
|
|
2235
|
+
console.print("[yellow]![/yellow] 没有找到 commit 记录")
|
|
2236
|
+
return
|
|
2237
|
+
|
|
2238
|
+
# 转换为笔记格式
|
|
2239
|
+
notes_data = commits_to_notes(commits, repo_path=repo_path)
|
|
2240
|
+
|
|
2241
|
+
if output_format != "json":
|
|
2242
|
+
console.print(f"[yellow]提取了 {len(notes_data)} 条 commit,正在导入...[/yellow]")
|
|
2243
|
+
|
|
2244
|
+
import_result = bulk_import_notes(
|
|
2245
|
+
notes_data=notes_data,
|
|
2246
|
+
note_type=note_type,
|
|
2247
|
+
batch_size=batch_size,
|
|
2248
|
+
show_progress=output_format != "json",
|
|
2249
|
+
)
|
|
2250
|
+
|
|
2251
|
+
result = {
|
|
2252
|
+
"success": True,
|
|
2253
|
+
"repo_path": str(Path(repo_path).resolve()),
|
|
2254
|
+
"commits_extracted": len(commits),
|
|
2255
|
+
**import_result,
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
if output_format == "json":
|
|
2259
|
+
print(output_json(result))
|
|
2260
|
+
else:
|
|
2261
|
+
console.print(f"[green]✓[/green] 导入: {import_result['imported']}")
|
|
2262
|
+
console.print(f"[red]✗[/red] 失败: {import_result['failed']}")
|
|
2263
|
+
console.print(f"总计: {import_result['total']}")
|
|
2264
|
+
|
|
2265
|
+
|
|
2266
|
+
@app.command()
|
|
2267
|
+
def ingest_log(
|
|
2268
|
+
repo_path: str = typer.Argument(..., help="本地 Git 仓库路径"),
|
|
2269
|
+
limit: int = typer.Option(50, "--limit", "-n", help="提取 commit 数量"),
|
|
2270
|
+
note_type: str = typer.Option("fleeting", "--type", "-t", help="笔记类型"),
|
|
2271
|
+
batch_size: int = typer.Option(32, "--batch-size", "-b", help="批处理大小"),
|
|
2272
|
+
kb: Optional[str] = typer.Option(None, "--kb", "-k", help="目标知识库名称"),
|
|
2273
|
+
output_format: str = typer.Option("table", "--format", "-f", help="输出格式: json, table"),
|
|
2274
|
+
json_output: bool = typer.Option(
|
|
2275
|
+
False, "--json", help="JSON 输出(快捷方式,等同于 --format json)"
|
|
2276
|
+
),
|
|
2277
|
+
):
|
|
2278
|
+
"""
|
|
2279
|
+
从 Git 仓库提取 commit 历史并导入为笔记
|
|
2280
|
+
|
|
2281
|
+
使用 block 分隔符格式提取 git log,自动处理 UTF-8 编码和路径规范化。
|
|
2282
|
+
|
|
2283
|
+
示例:
|
|
2284
|
+
jfox ingest-log ./my-project --limit 50
|
|
2285
|
+
jfox ingest-log ./my-project --kb work --type permanent
|
|
2286
|
+
"""
|
|
2287
|
+
try:
|
|
2288
|
+
# 处理 --json 快捷方式
|
|
2289
|
+
if json_output:
|
|
2290
|
+
output_format = "json"
|
|
2291
|
+
|
|
2292
|
+
# 如果指定了知识库,临时切换
|
|
2293
|
+
if kb:
|
|
2294
|
+
from .config import use_kb
|
|
2295
|
+
|
|
2296
|
+
with use_kb(kb):
|
|
2297
|
+
_ingest_log_impl(
|
|
2298
|
+
repo_path, limit, note_type, batch_size, output_format, json_output
|
|
2299
|
+
)
|
|
2300
|
+
else:
|
|
2301
|
+
_ingest_log_impl(repo_path, limit, note_type, batch_size, output_format, json_output)
|
|
2302
|
+
|
|
2303
|
+
except ValueError as e:
|
|
2304
|
+
result = {"success": False, "error": str(e)}
|
|
2305
|
+
if output_format == "json":
|
|
2306
|
+
print(output_json(result))
|
|
2307
|
+
else:
|
|
2308
|
+
console.print(f"[red]✗[/red] {e}")
|
|
2309
|
+
raise typer.Exit(1)
|
|
2310
|
+
except Exception as e:
|
|
2311
|
+
result = {"success": False, "error": str(e)}
|
|
2312
|
+
if output_format == "json":
|
|
2313
|
+
print(output_json(result))
|
|
2314
|
+
else:
|
|
2315
|
+
console.print(f"[red]✗[/red] Error: {e}")
|
|
2316
|
+
raise typer.Exit(1)
|
|
2317
|
+
|
|
2318
|
+
|
|
2158
2319
|
@app.command()
|
|
2159
2320
|
def bulk_import(
|
|
2160
2321
|
file_path: str = typer.Argument(..., help="JSON 文件路径,包含笔记数据"),
|