@hupan56/wlkj 2.0.0
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 +213 -0
- package/package.json +11 -0
- package/templates/cli.js +198 -0
- package/templates/qoder/commands/wl-code.md +43 -0
- package/templates/qoder/commands/wl-commit.md +30 -0
- package/templates/qoder/commands/wl-init.md +80 -0
- package/templates/qoder/commands/wl-insight.md +51 -0
- package/templates/qoder/commands/wl-prd.md +199 -0
- package/templates/qoder/commands/wl-report.md +166 -0
- package/templates/qoder/commands/wl-search.md +52 -0
- package/templates/qoder/commands/wl-spec.md +18 -0
- package/templates/qoder/commands/wl-status.md +51 -0
- package/templates/qoder/commands/wl-task.md +71 -0
- package/templates/qoder/commands/wl-test.md +42 -0
- package/templates/qoder/config.toml +5 -0
- package/templates/qoder/config.yaml +141 -0
- package/templates/qoder/hooks/inject-workflow-state.py +117 -0
- package/templates/qoder/hooks/session-start.py +204 -0
- package/templates/qoder/rules/wl-pipeline.md +105 -0
- package/templates/qoder/scripts/add_session.py +245 -0
- package/templates/qoder/scripts/benchmark.py +209 -0
- package/templates/qoder/scripts/build_style_index.py +268 -0
- package/templates/qoder/scripts/code_index.py +41 -0
- package/templates/qoder/scripts/collect_prds.py +31 -0
- package/templates/qoder/scripts/common/__init__.py +0 -0
- package/templates/qoder/scripts/common/active_task.py +230 -0
- package/templates/qoder/scripts/common/atomicio.py +172 -0
- package/templates/qoder/scripts/common/developer.py +161 -0
- package/templates/qoder/scripts/common/eval_api.py +144 -0
- package/templates/qoder/scripts/common/feishu.py +278 -0
- package/templates/qoder/scripts/common/filelock.py +211 -0
- package/templates/qoder/scripts/common/identity.py +285 -0
- package/templates/qoder/scripts/common/mentions.py +134 -0
- package/templates/qoder/scripts/common/paths.py +311 -0
- package/templates/qoder/scripts/common/reqid.py +218 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -0
- package/templates/qoder/scripts/common/task_utils.py +342 -0
- package/templates/qoder/scripts/common/terms.py +234 -0
- package/templates/qoder/scripts/common/utf8.py +38 -0
- package/templates/qoder/scripts/context_pack.py +196 -0
- package/templates/qoder/scripts/eval_prd.py +225 -0
- package/templates/qoder/scripts/export.py +487 -0
- package/templates/qoder/scripts/git_sync.py +1087 -0
- package/templates/qoder/scripts/handoff.py +22 -0
- package/templates/qoder/scripts/init_developer.py +76 -0
- package/templates/qoder/scripts/init_doctor.py +527 -0
- package/templates/qoder/scripts/install_qoderwork.py +339 -0
- package/templates/qoder/scripts/learn.py +67 -0
- package/templates/qoder/scripts/notify.py +5 -0
- package/templates/qoder/scripts/parse_prds.py +33 -0
- package/templates/qoder/scripts/report.py +281 -0
- package/templates/qoder/scripts/role.py +39 -0
- package/templates/qoder/scripts/run_weekly_update.bat +17 -0
- package/templates/qoder/scripts/run_weekly_update.sh +20 -0
- package/templates/qoder/scripts/search_index.py +352 -0
- package/templates/qoder/scripts/setup.py +453 -0
- package/templates/qoder/scripts/setup_weekly_cron.bat +22 -0
- package/templates/qoder/scripts/setup_weekly_cron.sh +19 -0
- package/templates/qoder/scripts/status.py +389 -0
- package/templates/qoder/scripts/syncgate.py +330 -0
- package/templates/qoder/scripts/task.py +954 -0
- package/templates/qoder/scripts/team.py +29 -0
- package/templates/qoder/scripts/team_sync.py +419 -0
- package/templates/qoder/scripts/workspace_init.py +102 -0
- package/templates/qoder/settings.json +53 -0
- package/templates/qoder/skills/design-review/SKILL.md +25 -0
- package/templates/qoder/skills/prd-generator/SKILL.md +180 -0
- package/templates/qoder/skills/prd-review/SKILL.md +36 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +141 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +69 -0
- package/templates/qoder/skills/spec-generator/SKILL.md +67 -0
- package/templates/qoder/skills/test-generator/SKILL.md +72 -0
- package/templates/qoder/skills/wl-commit/SKILL.md +76 -0
- package/templates/qoder/skills/wl-init/SKILL.md +67 -0
- package/templates/qoder/skills/wl-insight/SKILL.md +81 -0
- package/templates/qoder/skills/wl-report/SKILL.md +87 -0
- package/templates/qoder/skills/wl-search/SKILL.md +75 -0
- package/templates/qoder/skills/wl-status/SKILL.md +61 -0
- package/templates/qoder/skills/wl-task/SKILL.md +58 -0
- package/templates/qoder/templates/prd-full-template.md +103 -0
- package/templates/qoder/templates/prd-quick-template.md +69 -0
- package/templates/qoder/templates/prototype-app.html +344 -0
- package/templates/qoder/templates/prototype-web.html +310 -0
- package/templates/root/AGENTS.md +182 -0
- package/templates/root/README-pipeline.md +56 -0
- package/templates/root/ROLES.md +85 -0
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
QODER Pipeline - 会话日志记录
|
|
5
|
+
|
|
6
|
+
每次 AI 会话结束后, 记录一条日志到开发者的工作空间。
|
|
7
|
+
日志文件按行数自动轮转 (默认 2000 行)。
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python add_session.py --title "Title" --commit "hash" --summary "Summary"
|
|
11
|
+
python add_session.py --title "Title" --branch "feat/my-branch"
|
|
12
|
+
|
|
13
|
+
日志文件: .qoder/workspace/<developer>/journal-N.md
|
|
14
|
+
索引文件: .qoder/workspace/<developer>/index.md
|
|
15
|
+
|
|
16
|
+
参考: Trellis 的 add_session.py 设计
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import re
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import os
|
|
26
|
+
from datetime import datetime
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
30
|
+
|
|
31
|
+
from common.paths import (
|
|
32
|
+
FILE_JOURNAL_PREFIX,
|
|
33
|
+
get_repo_root,
|
|
34
|
+
get_developer,
|
|
35
|
+
get_workspace_dir,
|
|
36
|
+
get_active_journal_file,
|
|
37
|
+
count_lines,
|
|
38
|
+
)
|
|
39
|
+
from common.developer import ensure_developer
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# 默认配置
|
|
44
|
+
# =============================================================================
|
|
45
|
+
|
|
46
|
+
MAX_JOURNAL_LINES = 2000 # 每个日志文件最大行数
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# =============================================================================
|
|
50
|
+
# 核心函数
|
|
51
|
+
# =============================================================================
|
|
52
|
+
|
|
53
|
+
def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]:
|
|
54
|
+
"""获取最新日志文件信息。
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
(文件路径, 文件编号, 行数)
|
|
58
|
+
"""
|
|
59
|
+
latest_file = None
|
|
60
|
+
latest_num = 0
|
|
61
|
+
|
|
62
|
+
for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
|
|
63
|
+
if not f.is_file():
|
|
64
|
+
continue
|
|
65
|
+
match = re.search(r"(\d+)$", f.stem)
|
|
66
|
+
if match:
|
|
67
|
+
num = int(match.group(1))
|
|
68
|
+
if num > latest_num:
|
|
69
|
+
latest_num = num
|
|
70
|
+
latest_file = f
|
|
71
|
+
|
|
72
|
+
if latest_file:
|
|
73
|
+
lines = count_lines(latest_file)
|
|
74
|
+
return latest_file, latest_num, lines
|
|
75
|
+
|
|
76
|
+
return None, 0, 0
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_current_session(index_file: Path) -> int:
|
|
80
|
+
"""从 index.md 读取当前会话编号。"""
|
|
81
|
+
if not index_file.is_file():
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
content = index_file.read_text(encoding="utf-8")
|
|
86
|
+
for line in content.splitlines():
|
|
87
|
+
if "Total Sessions" in line:
|
|
88
|
+
match = re.search(r":\s*(\d+)", line)
|
|
89
|
+
if match:
|
|
90
|
+
return int(match.group(1))
|
|
91
|
+
except (OSError, IOError):
|
|
92
|
+
pass
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def create_new_journal_file(
|
|
97
|
+
dev_dir: Path, num: int, developer: str, max_lines: int = MAX_JOURNAL_LINES
|
|
98
|
+
) -> Path:
|
|
99
|
+
"""创建新的日志文件。"""
|
|
100
|
+
prev_num = num - 1
|
|
101
|
+
new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md"
|
|
102
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
103
|
+
|
|
104
|
+
content = f"""# Journal - {developer} (Part {num})
|
|
105
|
+
|
|
106
|
+
> Continuation from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived at ~{max_lines} lines)
|
|
107
|
+
> Started: {today}
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
new_file.write_text(content, encoding="utf-8")
|
|
113
|
+
return new_file
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def update_index(
|
|
117
|
+
index_file: Path,
|
|
118
|
+
developer: str,
|
|
119
|
+
session_num: int,
|
|
120
|
+
journal_file: str,
|
|
121
|
+
max_lines: int = MAX_JOURNAL_LINES,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""更新工作空间索引文件 index.md。"""
|
|
124
|
+
today = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
125
|
+
|
|
126
|
+
# 生成日志表格
|
|
127
|
+
dev_dir = index_file.parent
|
|
128
|
+
table_rows = []
|
|
129
|
+
for f in sorted(dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md")):
|
|
130
|
+
lines = count_lines(f)
|
|
131
|
+
status = "Active" if f.name == journal_file else "Archived"
|
|
132
|
+
table_rows.append(f"| `{f.name}` | ~{lines} | {status} |")
|
|
133
|
+
|
|
134
|
+
journals_table = "\n".join(table_rows)
|
|
135
|
+
|
|
136
|
+
index_content = f"""# Workspace Index - {developer}
|
|
137
|
+
|
|
138
|
+
> Journal tracking for AI development sessions.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Current Status
|
|
143
|
+
|
|
144
|
+
<!-- @@@auto:current-status -->
|
|
145
|
+
- **Active File**: `{journal_file}`
|
|
146
|
+
- **Total Sessions**: {session_num}
|
|
147
|
+
- **Last Active**: {today}
|
|
148
|
+
<!-- @@@/auto:current-status -->
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Journals
|
|
153
|
+
|
|
154
|
+
| File | Lines | Status |
|
|
155
|
+
|------|-------|--------|
|
|
156
|
+
<!-- @@@auto:journals-table -->
|
|
157
|
+
{journals_table}
|
|
158
|
+
<!-- @@@/auto:journals-table -->
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
*Auto-managed by QODER Pipeline*
|
|
163
|
+
"""
|
|
164
|
+
index_file.write_text(index_content, encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# =============================================================================
|
|
168
|
+
# Main
|
|
169
|
+
# =============================================================================
|
|
170
|
+
|
|
171
|
+
def main() -> int:
|
|
172
|
+
"""CLI 入口。"""
|
|
173
|
+
parser = argparse.ArgumentParser(description="Record AI session to journal")
|
|
174
|
+
parser.add_argument("--title", required=True, help="Session title")
|
|
175
|
+
parser.add_argument("--summary", default="", help="Session summary")
|
|
176
|
+
parser.add_argument("--commit", default="", help="Git commit hash")
|
|
177
|
+
parser.add_argument("--branch", default="", help="Git branch (auto-detected if omitted)")
|
|
178
|
+
args = parser.parse_args()
|
|
179
|
+
|
|
180
|
+
repo_root = get_repo_root()
|
|
181
|
+
developer = ensure_developer(repo_root)
|
|
182
|
+
|
|
183
|
+
workspace = get_workspace_dir(repo_root)
|
|
184
|
+
if not workspace:
|
|
185
|
+
print("Error: No workspace directory", file=sys.stderr)
|
|
186
|
+
return 1
|
|
187
|
+
|
|
188
|
+
workspace.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
|
|
190
|
+
# 获取 Git 分支
|
|
191
|
+
branch = args.branch
|
|
192
|
+
if not branch:
|
|
193
|
+
try:
|
|
194
|
+
result = subprocess.run(
|
|
195
|
+
["git", "branch", "--show-current"],
|
|
196
|
+
capture_output=True, text=True, cwd=str(repo_root)
|
|
197
|
+
)
|
|
198
|
+
branch = result.stdout.strip()
|
|
199
|
+
except Exception:
|
|
200
|
+
branch = "unknown"
|
|
201
|
+
|
|
202
|
+
# 获取最新日志文件
|
|
203
|
+
journal_file, journal_num, lines = get_latest_journal_info(workspace)
|
|
204
|
+
|
|
205
|
+
# 如果没有日志文件或已满, 创建新的
|
|
206
|
+
if not journal_file or lines >= MAX_JOURNAL_LINES:
|
|
207
|
+
journal_num = journal_num + 1 if journal_file else 1
|
|
208
|
+
journal_file = create_new_journal_file(workspace, journal_num, developer)
|
|
209
|
+
lines = 0
|
|
210
|
+
|
|
211
|
+
# 读取当前会话编号
|
|
212
|
+
index_file = workspace / "index.md"
|
|
213
|
+
session_num = get_current_session(index_file) + 1
|
|
214
|
+
|
|
215
|
+
# 追加会话记录
|
|
216
|
+
today = datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
217
|
+
commit_line = f"- **Commit**: `{args.commit}`\n" if args.commit else ""
|
|
218
|
+
branch_line = f"- **Branch**: `{branch}`\n" if branch else ""
|
|
219
|
+
|
|
220
|
+
entry = f"""### Session {session_num}: {args.title}
|
|
221
|
+
|
|
222
|
+
- **Date**: {today}
|
|
223
|
+
- **Developer**: {developer}
|
|
224
|
+
{commit_line}{branch_line}
|
|
225
|
+
**Summary**: {args.summary or 'No summary provided.'}
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
with open(journal_file, "a", encoding="utf-8") as f:
|
|
232
|
+
f.write(entry)
|
|
233
|
+
except (OSError, IOError) as e:
|
|
234
|
+
print(f"Error: Failed to append to journal: {e}", file=sys.stderr)
|
|
235
|
+
return 1
|
|
236
|
+
|
|
237
|
+
# 更新索引
|
|
238
|
+
update_index(index_file, developer, session_num, journal_file.name)
|
|
239
|
+
|
|
240
|
+
print(f"Session {session_num} recorded in {journal_file.name}")
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__":
|
|
245
|
+
sys.exit(main())
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
benchmark.py - QODER 流水线性能基准 (性能优化 A7)
|
|
5
|
+
|
|
6
|
+
测量关键操作的延迟和内存, 供优化前后对比。
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python benchmark.py # 跑全部基准
|
|
10
|
+
python benchmark.py --search # 只测搜索
|
|
11
|
+
python benchmark.py --build # 只测构建 (耗时)
|
|
12
|
+
python benchmark.py --json # 输出 JSON (供对比)
|
|
13
|
+
|
|
14
|
+
指标:
|
|
15
|
+
- search_cold: 冷启动搜索 (Python 启动 + JSON 加载 + 扫描)
|
|
16
|
+
- search_warm: 热搜索 (倒排索引缓存命中)
|
|
17
|
+
- index_load: keyword-index.json 加载时间
|
|
18
|
+
- inverted_build: 倒排索引构建时间
|
|
19
|
+
- inverted_cache_load: 缓存加载时间
|
|
20
|
+
- cnmap_lookup: CN_MAP 查询 (含 auto-expand)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import os
|
|
26
|
+
import sys
|
|
27
|
+
import time
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
32
|
+
except (AttributeError, TypeError, OSError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__)) # .qoder/scripts
|
|
36
|
+
sys.path.insert(0, THIS_DIR)
|
|
37
|
+
# BASE = repo root = scripts 的上两级 (.qoder/scripts -> .qoder -> repo)
|
|
38
|
+
BASE = os.path.dirname(os.path.dirname(THIS_DIR))
|
|
39
|
+
|
|
40
|
+
INDEX_DIR = os.path.join(BASE, 'data', 'index')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def measure(fn, repeat=1):
|
|
44
|
+
"""测函数执行时间, 返回 (avg_ms, min_ms, max_ms)。"""
|
|
45
|
+
times = []
|
|
46
|
+
for _ in range(repeat):
|
|
47
|
+
t0 = time.perf_counter()
|
|
48
|
+
fn()
|
|
49
|
+
times.append((time.perf_counter() - t0) * 1000)
|
|
50
|
+
return sum(times) / len(times), min(times), max(times)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def bench_search():
|
|
54
|
+
"""搜索基准: 冷启动 + 热搜索对比。"""
|
|
55
|
+
results = {}
|
|
56
|
+
|
|
57
|
+
# 1. JSON 加载 (冷)
|
|
58
|
+
ki_path = os.path.join(INDEX_DIR, 'keyword-index.json')
|
|
59
|
+
def load_ki():
|
|
60
|
+
with open(ki_path, encoding='utf-8') as f:
|
|
61
|
+
return json.load(f)
|
|
62
|
+
results['index_load_ms'] = measure(load_ki)[0]
|
|
63
|
+
ki = load_ki()
|
|
64
|
+
results['keyword_count'] = len(ki)
|
|
65
|
+
|
|
66
|
+
# 2. 全扫描 (旧逻辑)
|
|
67
|
+
from search_index import _match_keyword
|
|
68
|
+
queries = ['insurance', 'attendance', 'salary', 'vehicle', 'user']
|
|
69
|
+
def full_scan():
|
|
70
|
+
for q in queries:
|
|
71
|
+
for kw in ki:
|
|
72
|
+
if _match_keyword(q, kw):
|
|
73
|
+
pass
|
|
74
|
+
results['full_scan_5q_ms'] = measure(full_scan, repeat=5)[0] / 5
|
|
75
|
+
|
|
76
|
+
# 3. 倒排索引构建 (首次)
|
|
77
|
+
from common.search_engine import get_inverted_index, search_keywords_fast, build_inverted_index
|
|
78
|
+
# 清缓存测构建
|
|
79
|
+
cache_path = os.path.join(INDEX_DIR, '.inverted-cache.json')
|
|
80
|
+
if os.path.exists(cache_path):
|
|
81
|
+
os.remove(cache_path)
|
|
82
|
+
results['inverted_build_ms'] = measure(lambda: get_inverted_index(BASE))[0]
|
|
83
|
+
|
|
84
|
+
# 4. 缓存加载 (热)
|
|
85
|
+
results['inverted_cache_load_ms'] = measure(lambda: get_inverted_index(BASE), repeat=5)[0]
|
|
86
|
+
|
|
87
|
+
# 5. 倒排搜索 (热)
|
|
88
|
+
inverted = get_inverted_index(BASE)
|
|
89
|
+
def inverted_search():
|
|
90
|
+
for q in queries:
|
|
91
|
+
search_keywords_fast([q], inverted, ki)
|
|
92
|
+
results['inverted_scan_5q_ms'] = measure(inverted_search, repeat=5)[0] / 5
|
|
93
|
+
|
|
94
|
+
# 6. 提速比
|
|
95
|
+
if results['inverted_scan_5q_ms'] > 0:
|
|
96
|
+
results['speedup'] = round(results['full_scan_5q_ms'] / results['inverted_scan_5q_ms'], 1)
|
|
97
|
+
else:
|
|
98
|
+
results['speedup'] = float('inf')
|
|
99
|
+
|
|
100
|
+
return results
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def bench_cnmap():
|
|
104
|
+
"""CN_MAP 查询基准。"""
|
|
105
|
+
from common.terms import CN_MAP, BUSINESS_PATH_MAP, get_cn_map_with_auto
|
|
106
|
+
results = {}
|
|
107
|
+
results['cn_map_static_count'] = len(CN_MAP)
|
|
108
|
+
results['business_path_map_count'] = len(BUSINESS_PATH_MAP)
|
|
109
|
+
|
|
110
|
+
# auto-expand (含 PRD 扫描)
|
|
111
|
+
def auto_expand():
|
|
112
|
+
return get_cn_map_with_auto()
|
|
113
|
+
results['auto_expand_ms'] = measure(auto_expand)[0]
|
|
114
|
+
results['cn_map_total_count'] = len(get_cn_map_with_auto())
|
|
115
|
+
|
|
116
|
+
# 查询扩展
|
|
117
|
+
test_terms = ['考勤', '保险', '库存', '采购', '发票']
|
|
118
|
+
def expand_query():
|
|
119
|
+
out = []
|
|
120
|
+
for t in test_terms:
|
|
121
|
+
if t in CN_MAP:
|
|
122
|
+
out.extend(CN_MAP[t].split())
|
|
123
|
+
return out
|
|
124
|
+
results['query_expand_5q_ms'] = measure(expand_query, repeat=10)[0] / 10
|
|
125
|
+
|
|
126
|
+
return results
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def bench_index_sizes():
|
|
130
|
+
"""索引文件大小。"""
|
|
131
|
+
results = {}
|
|
132
|
+
for name in ['keyword-index.json', 'api-index.json', 'style-index.json',
|
|
133
|
+
'prd-index.json', 'module-map.json', 'prd-code-map.json']:
|
|
134
|
+
p = os.path.join(INDEX_DIR, name)
|
|
135
|
+
if os.path.isfile(p):
|
|
136
|
+
results[name] = os.path.getsize(p)
|
|
137
|
+
# 缓存文件
|
|
138
|
+
for name in ['.inverted-cache.json', '.file-keys.json']:
|
|
139
|
+
p = os.path.join(INDEX_DIR, name)
|
|
140
|
+
if os.path.isfile(p):
|
|
141
|
+
results[name] = os.path.getsize(p)
|
|
142
|
+
return results
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def main():
|
|
146
|
+
parser = argparse.ArgumentParser(description='QODER 性能基准')
|
|
147
|
+
parser.add_argument('--search', action='store_true', help='只测搜索')
|
|
148
|
+
parser.add_argument('--build', action='store_true', help='测构建 (耗时)')
|
|
149
|
+
parser.add_argument('--json', action='store_true', help='JSON 输出')
|
|
150
|
+
args = parser.parse_args()
|
|
151
|
+
|
|
152
|
+
report = {
|
|
153
|
+
'timestamp': datetime.now().isoformat(timespec='seconds'),
|
|
154
|
+
'python': sys.version.split()[0],
|
|
155
|
+
'platform': sys.platform,
|
|
156
|
+
'base': os.path.basename(BASE),
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if not args.build:
|
|
160
|
+
print('=' * 50)
|
|
161
|
+
print('QODER 性能基准')
|
|
162
|
+
print('=' * 50)
|
|
163
|
+
|
|
164
|
+
if not args.build or args.search:
|
|
165
|
+
if not args.json:
|
|
166
|
+
print('\n--- 搜索性能 ---')
|
|
167
|
+
sr = bench_search()
|
|
168
|
+
report['search'] = sr
|
|
169
|
+
if not args.json:
|
|
170
|
+
print(' keyword-index 加载: %.0f ms (%d keys)' % (sr['index_load_ms'], sr['keyword_count']))
|
|
171
|
+
print(' 全扫描 (5 query): %.2f ms' % sr['full_scan_5q_ms'])
|
|
172
|
+
print(' 倒排索引构建: %.0f ms (首次)' % sr['inverted_build_ms'])
|
|
173
|
+
print(' 缓存加载: %.2f ms (热)' % sr['inverted_cache_load_ms'])
|
|
174
|
+
print(' 倒排搜索 (5 query): %.2f ms' % sr['inverted_scan_5q_ms'])
|
|
175
|
+
print(' 提速: %.1fx' % sr['speedup'])
|
|
176
|
+
|
|
177
|
+
if not args.json:
|
|
178
|
+
print('\n--- CN_MAP ---')
|
|
179
|
+
cr = bench_cnmap()
|
|
180
|
+
report['cnmap'] = cr
|
|
181
|
+
if not args.json:
|
|
182
|
+
print(' 静态词: %d, 路径映射: %d' % (cr['cn_map_static_count'], cr['business_path_map_count']))
|
|
183
|
+
print(' auto-expand: %.0f ms, 总计 %d 词' % (cr['auto_expand_ms'], cr['cn_map_total_count']))
|
|
184
|
+
|
|
185
|
+
if not args.build:
|
|
186
|
+
if not args.json:
|
|
187
|
+
print('\n--- 索引大小 ---')
|
|
188
|
+
sz = bench_index_sizes()
|
|
189
|
+
report['index_sizes'] = sz
|
|
190
|
+
if not args.json:
|
|
191
|
+
for name, size in sorted(sz.items(), key=lambda x: -x[1]):
|
|
192
|
+
print(' %-30s %s' % (name, format_size(size)))
|
|
193
|
+
|
|
194
|
+
if args.json:
|
|
195
|
+
print(json.dumps(report, indent=2, ensure_ascii=False))
|
|
196
|
+
else:
|
|
197
|
+
print('\n' + '=' * 50)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def format_size(n):
|
|
201
|
+
for unit in ['B', 'KB', 'MB']:
|
|
202
|
+
if n < 1024:
|
|
203
|
+
return '%.1f %s' % (n, unit)
|
|
204
|
+
n /= 1024
|
|
205
|
+
return '%.1f GB' % n
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
if __name__ == '__main__':
|
|
209
|
+
main()
|