@hupan56/wlkj 3.3.13 → 3.3.15
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 +0 -7
- package/package.json +29 -29
- package/templates/qoder/commands/optional/wl-spec.md +0 -8
- package/templates/qoder/commands/optional/wl-status.md +0 -8
- package/templates/qoder/commands/wl-code.md +0 -8
- package/templates/qoder/commands/wl-commit.md +0 -8
- package/templates/qoder/commands/wl-init.md +129 -129
- package/templates/qoder/commands/wl-search.md +0 -8
- package/templates/qoder/commands/wl-task.md +0 -8
- package/templates/qoder/commands/wl-test.md +0 -8
- package/templates/qoder/config.yaml +2 -3
- package/templates/qoder/contracts/insight.md +55 -55
- package/templates/qoder/hooks/pre-tool-use-commit.py +124 -124
- package/templates/qoder/hooks/session-start.py +384 -384
- package/templates/qoder/hooks/stop-eval.py +383 -383
- package/templates/qoder/scripts/capability/adapters/mcp.py +322 -322
- package/templates/qoder/scripts/capability/registry.py +245 -245
- package/templates/qoder/scripts/deployment/setup/install_qoderwork.py +14 -0
- package/templates/qoder/scripts/deployment/setup/setup.py +0 -11
- package/templates/qoder/scripts/domain/task/wlkj_panel.py +1503 -1503
- package/templates/qoder/scripts/foundation/bootstrap.py +145 -156
- package/templates/qoder/scripts/foundation/io/context_cache.py +94 -94
- package/templates/qoder/scripts/orchestration/wlkj.py +295 -306
- package/templates/qoder/scripts/tool_guide.md +70 -70
- package/templates/qoder/scripts/validation/eval/alignment_matrix.py +176 -176
- package/templates/qoder/scripts/validation/eval/bf2_content_fidelity.py +110 -110
- package/templates/qoder/scripts/validation/eval/bf2_llmjudge.py +104 -104
- package/templates/qoder/scripts/validation/eval/bf_score.py +218 -218
- package/templates/qoder/scripts/validation/eval/code_flywheel.py +150 -150
- package/templates/qoder/scripts/validation/eval/dispatcher_ab.py +156 -156
- package/templates/qoder/scripts/validation/eval/dispatcher_ab_2026-07-21.json +23 -23
- package/templates/qoder/scripts/validation/eval/feature_fidelity_flywheel.py +143 -143
- package/templates/qoder/scripts/validation/eval/gradient_matrix.py +261 -261
- package/templates/qoder/scripts/validation/eval/gradient_matrix_baseline_2026-07-21.json +33 -33
- package/templates/qoder/scripts/validation/eval/metrics_dashboard.py +105 -105
- package/templates/qoder/scripts/validation/eval/multi_turn_flywheel.py +118 -118
- package/templates/qoder/scripts/validation/eval/prd_fidelity_flywheel.py +128 -128
- package/templates/qoder/scripts/validation/eval/prd_flywheel.py +148 -148
- package/templates/qoder/scripts/validation/eval/prototype_fidelity_flywheel.py +166 -166
- package/templates/qoder/scripts/validation/eval/recall_flywheel.py +148 -148
- package/templates/qoder/scripts/validation/eval/robustness_flywheel.py +139 -139
- package/templates/qoder/scripts/validation/eval/speed_accuracy_flywheel.py +188 -188
- package/templates/qoder/scripts/validation/eval/task_flywheel.py +124 -124
- package/templates/qoder/scripts/validation/eval/token_flywheel.py +88 -88
- package/templates/qoder/scripts/validation/metrics/eval_code_ac.py +177 -177
- package/templates/qoder/scripts/validation/metrics/lint_cases.py +170 -170
- package/templates/qoder/scripts/validation/test/test_context_cache.py +78 -78
- package/templates/qoder/scripts/validation/test/test_lint_cases.py +60 -60
- package/templates/qoder/scripts/validation/test/test_pre_tool_use_commit.py +70 -70
- package/templates/qoder/commands/wl-data.md +0 -46
- package/templates/qoder/commands/wl-fix.md +0 -47
- package/templates/qoder/commands/wl-knowledge.md +0 -41
- package/templates/qoder/commands/wl-review.md +0 -49
- package/templates/workspace/specs/REQ-TEST-CTX/.context-cache.json +0 -1
|
@@ -1,156 +1,145 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
parent
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
rc(encoding="utf-8")
|
|
147
|
-
except Exception:
|
|
148
|
-
pass
|
|
149
|
-
|
|
150
|
-
repo_root = os.path.dirname(os.path.dirname(scripts_dir))
|
|
151
|
-
return scripts_dir, repo_root
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
# 模块级常量
|
|
155
|
-
SCRIPTS_DIR = find_scripts_root()
|
|
156
|
-
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))))
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
bootstrap.py - 统一 CLI 启动样板 (DRY) + 子目录化路径自举 (v3.0)
|
|
4
|
+
|
|
5
|
+
治"70处 sys.path + 50处 reconfigure" + 子目录化后的路径定位。
|
|
6
|
+
|
|
7
|
+
子目录化后, 脚本在 scripts/domain/kg/kg.py, 它的 dirname(__file__)=scripts/domain/kg/,
|
|
8
|
+
直接 sys.path.insert(scripts/domain/kg/) 后 `from foundation.x` 会断 (foundation 在 scripts/foundation/)。
|
|
9
|
+
|
|
10
|
+
本模块的 setup() 解决: 无论脚本在 scripts/ 根还是 scripts/某子包/,
|
|
11
|
+
都能正确定位 scripts/ 根并注入, 让 `from foundation.x` 和跨包 import 都能用。
|
|
12
|
+
|
|
13
|
+
核心: _find_scripts_root() 向上找含 'foundation/' 子目录的那个目录 = scripts/ 根。
|
|
14
|
+
|
|
15
|
+
Usage (两种姿势):
|
|
16
|
+
# 姿势1: 子包脚本开头 (推荐)
|
|
17
|
+
import os, sys
|
|
18
|
+
_S = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # 子包→scripts
|
|
19
|
+
if _S not in sys.path: sys.path.insert(0, _S)
|
|
20
|
+
from bootstrap import setup; setup()
|
|
21
|
+
|
|
22
|
+
# 姿势2: scripts/ 根脚本 (老姿势仍可用)
|
|
23
|
+
from bootstrap import setup; setup()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import os
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
__all__ = ["setup", "SCRIPTS_DIR", "REPO_ROOT", "find_scripts_root", "ensure_scripts_path"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def find_scripts_root(start=None):
|
|
33
|
+
"""定位 .qoder/scripts/ 根目录。
|
|
34
|
+
|
|
35
|
+
策略: 从 start (默认 __file__ 的目录) 向上找, 直到某目录含 'common/' 子目录
|
|
36
|
+
且该 common/ 下有 bootstrap.py (即本文件) → 那就是 scripts/ 根。
|
|
37
|
+
兜底: 找含 '.qoder' 的目录 + '/scripts'。
|
|
38
|
+
"""
|
|
39
|
+
start = start or os.path.dirname(os.path.abspath(__file__))
|
|
40
|
+
cur = start
|
|
41
|
+
for _ in range(10):
|
|
42
|
+
# scripts/ 根的标志: 含 common/bootstrap.py
|
|
43
|
+
if os.path.isfile(os.path.join(cur, "foundation", "bootstrap.py")):
|
|
44
|
+
return cur
|
|
45
|
+
parent = os.path.dirname(cur)
|
|
46
|
+
if parent == cur:
|
|
47
|
+
break
|
|
48
|
+
cur = parent
|
|
49
|
+
# 兜底: cwd 向上找 .qoder/scripts
|
|
50
|
+
cwd = os.getcwd()
|
|
51
|
+
for _ in range(10):
|
|
52
|
+
cand = os.path.join(cwd, ".qoder", "scripts")
|
|
53
|
+
if os.path.isdir(cand):
|
|
54
|
+
return cand
|
|
55
|
+
parent = os.path.dirname(cwd)
|
|
56
|
+
if parent == cwd:
|
|
57
|
+
break
|
|
58
|
+
cwd = parent
|
|
59
|
+
return start
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ensure_scripts_path(start=None):
|
|
63
|
+
"""确保 scripts/ 根在 sys.path 最前。幂等。
|
|
64
|
+
|
|
65
|
+
子包脚本调用此函数后, `from foundation.x` 即可用。
|
|
66
|
+
"""
|
|
67
|
+
root = find_scripts_root(start)
|
|
68
|
+
if root not in sys.path:
|
|
69
|
+
sys.path.insert(0, root)
|
|
70
|
+
return root
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _find_scripts_dir() -> str:
|
|
74
|
+
"""[内部] 定位 scripts/ 根 (兼容旧名)。"""
|
|
75
|
+
return find_scripts_root()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def ensure_utf8_stdio():
|
|
79
|
+
"""UTF-8 stdio 配置 (v3.0: 从 utf8.py 并入)。
|
|
80
|
+
|
|
81
|
+
把 stdout/stderr 切到 UTF-8, 失败则忽略 (不崩溃)。
|
|
82
|
+
幂等: 多次调用无害。
|
|
83
|
+
"""
|
|
84
|
+
for stream_name in ("stdout", "stderr"):
|
|
85
|
+
stream = getattr(sys, stream_name, None)
|
|
86
|
+
if stream is None:
|
|
87
|
+
continue
|
|
88
|
+
reconfigure = getattr(stream, "reconfigure", None)
|
|
89
|
+
if reconfigure is None:
|
|
90
|
+
continue
|
|
91
|
+
try:
|
|
92
|
+
reconfigure(encoding="utf-8", errors="replace")
|
|
93
|
+
except (TypeError, ValueError, OSError, IOError):
|
|
94
|
+
try:
|
|
95
|
+
reconfigure(encoding="utf-8")
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def setup():
|
|
101
|
+
"""统一 CLI 启动初始化。幂等。
|
|
102
|
+
|
|
103
|
+
做: ① ensure_utf8_stdio ② scripts/ 根注入 sys.path ③ scripts/common/ 注入
|
|
104
|
+
Returns: (SCRIPTS_DIR, REPO_ROOT)
|
|
105
|
+
"""
|
|
106
|
+
# 先确保 scripts/ 根在 path (本文件可能在 common/ 或被引用)
|
|
107
|
+
scripts_dir = ensure_scripts_root = find_scripts_root()
|
|
108
|
+
if scripts_dir not in sys.path:
|
|
109
|
+
sys.path.insert(0, scripts_dir)
|
|
110
|
+
common_dir = os.path.join(scripts_dir, "foundation")
|
|
111
|
+
if os.path.isdir(common_dir) and common_dir not in sys.path:
|
|
112
|
+
sys.path.insert(0, common_dir)
|
|
113
|
+
# 注入所有子包目录 (kg/build/mcp/test/setup/carriers/task/search/report)
|
|
114
|
+
# 让跨包 import (旧式不带前缀, 如 import search_index) 仍可用
|
|
115
|
+
try:
|
|
116
|
+
for _name in os.listdir(scripts_dir):
|
|
117
|
+
_subdir = os.path.join(scripts_dir, _name)
|
|
118
|
+
if os.path.isdir(_subdir) and _subdir not in sys.path:
|
|
119
|
+
sys.path.insert(0, _subdir)
|
|
120
|
+
except OSError:
|
|
121
|
+
pass
|
|
122
|
+
|
|
123
|
+
# 1. UTF-8 stdio (v3.0: utf8.py 已并入 bootstrap, 内联实现)
|
|
124
|
+
try:
|
|
125
|
+
ensure_utf8_stdio()
|
|
126
|
+
except Exception:
|
|
127
|
+
for sname in ("stdout", "stderr"):
|
|
128
|
+
s = getattr(sys, sname, None)
|
|
129
|
+
rc = getattr(s, "reconfigure", None) if s else None
|
|
130
|
+
if rc:
|
|
131
|
+
try:
|
|
132
|
+
rc(encoding="utf-8", errors="replace")
|
|
133
|
+
except Exception:
|
|
134
|
+
try:
|
|
135
|
+
rc(encoding="utf-8")
|
|
136
|
+
except Exception:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
repo_root = os.path.dirname(os.path.dirname(scripts_dir))
|
|
140
|
+
return scripts_dir, repo_root
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# 模块级常量
|
|
144
|
+
SCRIPTS_DIR = find_scripts_root()
|
|
145
|
+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(SCRIPTS_DIR))))
|
|
@@ -1,94 +1,94 @@
|
|
|
1
|
-
"""context_cache.py — 按 REQ-id 缓存 context_pack/rag_search 结果(T1.3 跨命令上下文缓存)。
|
|
2
|
-
|
|
3
|
-
PRD→spec→code 链路里同一 REQ 反复拉同一份上下文(3 次 KG/MCP 往返)→ 缓存复用,省 token + 减反复搜。
|
|
4
|
-
|
|
5
|
-
存储: workspace/specs/{REQ}/.context-cache.json
|
|
6
|
-
结构: {query_hash: {"query":..., "text":..., "ts":...}}
|
|
7
|
-
TTL 默认 24h(PRD/spec 改动后用 invalidate(req) 显式失效)。
|
|
8
|
-
|
|
9
|
-
纯模块,无外部依赖;任何异常 fail-open(缓存坏了不影响主流程,调用方降级直拉)。
|
|
10
|
-
"""
|
|
11
|
-
import os
|
|
12
|
-
import json
|
|
13
|
-
import time
|
|
14
|
-
import hashlib
|
|
15
|
-
|
|
16
|
-
DEFAULT_TTL = 24 * 3600 # 24h
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _repo_root():
|
|
20
|
-
"""从本文件向上找含 workspace/ 或 .qoder/ 的目录作仓库根。"""
|
|
21
|
-
d = os.path.dirname(os.path.abspath(__file__))
|
|
22
|
-
for _ in range(7):
|
|
23
|
-
if os.path.isdir(os.path.join(d, "workspace")) or os.path.isdir(os.path.join(d, ".qoder")):
|
|
24
|
-
return d
|
|
25
|
-
parent = os.path.dirname(d)
|
|
26
|
-
if parent == d:
|
|
27
|
-
break
|
|
28
|
-
d = parent
|
|
29
|
-
return os.getcwd()
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _sanitize(req_id):
|
|
33
|
-
return "".join(c for c in str(req_id) if c.isalnum() or c in "-_") or "unknown"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def cache_path(req_id, base=None):
|
|
37
|
-
base = base or _repo_root()
|
|
38
|
-
return os.path.join(base, "workspace", "specs", _sanitize(req_id), ".context-cache.json")
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def _load(path):
|
|
42
|
-
try:
|
|
43
|
-
with open(path, encoding="utf-8") as f:
|
|
44
|
-
data = json.load(f)
|
|
45
|
-
return data if isinstance(data, dict) else {}
|
|
46
|
-
except Exception:
|
|
47
|
-
return {}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def _save(path, data):
|
|
51
|
-
try:
|
|
52
|
-
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
53
|
-
tmp = path + ".tmp"
|
|
54
|
-
with open(tmp, "w", encoding="utf-8") as f:
|
|
55
|
-
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
56
|
-
os.replace(tmp, path) # 原子写
|
|
57
|
-
return True
|
|
58
|
-
except Exception:
|
|
59
|
-
return False
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _qhash(query):
|
|
63
|
-
return hashlib.md5(query.strip().lower().encode("utf-8")).hexdigest()[:12]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def get(req_id, query, ttl=DEFAULT_TTL, base=None):
|
|
67
|
-
"""命中且未过期 → 返回 text;否则 None。"""
|
|
68
|
-
data = _load(cache_path(req_id, base))
|
|
69
|
-
entry = data.get(_qhash(query))
|
|
70
|
-
if not entry:
|
|
71
|
-
return None
|
|
72
|
-
if ttl and (time.time() - float(entry.get("ts", 0))) > ttl:
|
|
73
|
-
return None
|
|
74
|
-
return entry.get("text")
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def put(req_id, query, text, base=None):
|
|
78
|
-
"""写一条缓存(覆盖同 query 旧值)。"""
|
|
79
|
-
path = cache_path(req_id, base)
|
|
80
|
-
data = _load(path)
|
|
81
|
-
data[_qhash(query)] = {"query": query, "text": text, "ts": time.time()}
|
|
82
|
-
return _save(path, data)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def invalidate(req_id, base=None):
|
|
86
|
-
"""清整个 REQ 的缓存(PRD/spec 改动后调)。"""
|
|
87
|
-
return _save(cache_path(req_id, base), {})
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def stats(req_id, base=None):
|
|
91
|
-
"""缓存条数 + query 摘要(调试/观测命中率用)。"""
|
|
92
|
-
data = _load(cache_path(req_id, base))
|
|
93
|
-
return {"entries": len(data),
|
|
94
|
-
"queries": [str(e.get("query", ""))[:40] for e in data.values()]}
|
|
1
|
+
"""context_cache.py — 按 REQ-id 缓存 context_pack/rag_search 结果(T1.3 跨命令上下文缓存)。
|
|
2
|
+
|
|
3
|
+
PRD→spec→code 链路里同一 REQ 反复拉同一份上下文(3 次 KG/MCP 往返)→ 缓存复用,省 token + 减反复搜。
|
|
4
|
+
|
|
5
|
+
存储: workspace/specs/{REQ}/.context-cache.json
|
|
6
|
+
结构: {query_hash: {"query":..., "text":..., "ts":...}}
|
|
7
|
+
TTL 默认 24h(PRD/spec 改动后用 invalidate(req) 显式失效)。
|
|
8
|
+
|
|
9
|
+
纯模块,无外部依赖;任何异常 fail-open(缓存坏了不影响主流程,调用方降级直拉)。
|
|
10
|
+
"""
|
|
11
|
+
import os
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
import hashlib
|
|
15
|
+
|
|
16
|
+
DEFAULT_TTL = 24 * 3600 # 24h
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _repo_root():
|
|
20
|
+
"""从本文件向上找含 workspace/ 或 .qoder/ 的目录作仓库根。"""
|
|
21
|
+
d = os.path.dirname(os.path.abspath(__file__))
|
|
22
|
+
for _ in range(7):
|
|
23
|
+
if os.path.isdir(os.path.join(d, "workspace")) or os.path.isdir(os.path.join(d, ".qoder")):
|
|
24
|
+
return d
|
|
25
|
+
parent = os.path.dirname(d)
|
|
26
|
+
if parent == d:
|
|
27
|
+
break
|
|
28
|
+
d = parent
|
|
29
|
+
return os.getcwd()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _sanitize(req_id):
|
|
33
|
+
return "".join(c for c in str(req_id) if c.isalnum() or c in "-_") or "unknown"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def cache_path(req_id, base=None):
|
|
37
|
+
base = base or _repo_root()
|
|
38
|
+
return os.path.join(base, "workspace", "specs", _sanitize(req_id), ".context-cache.json")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load(path):
|
|
42
|
+
try:
|
|
43
|
+
with open(path, encoding="utf-8") as f:
|
|
44
|
+
data = json.load(f)
|
|
45
|
+
return data if isinstance(data, dict) else {}
|
|
46
|
+
except Exception:
|
|
47
|
+
return {}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _save(path, data):
|
|
51
|
+
try:
|
|
52
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
53
|
+
tmp = path + ".tmp"
|
|
54
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
55
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
56
|
+
os.replace(tmp, path) # 原子写
|
|
57
|
+
return True
|
|
58
|
+
except Exception:
|
|
59
|
+
return False
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _qhash(query):
|
|
63
|
+
return hashlib.md5(query.strip().lower().encode("utf-8")).hexdigest()[:12]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get(req_id, query, ttl=DEFAULT_TTL, base=None):
|
|
67
|
+
"""命中且未过期 → 返回 text;否则 None。"""
|
|
68
|
+
data = _load(cache_path(req_id, base))
|
|
69
|
+
entry = data.get(_qhash(query))
|
|
70
|
+
if not entry:
|
|
71
|
+
return None
|
|
72
|
+
if ttl and (time.time() - float(entry.get("ts", 0))) > ttl:
|
|
73
|
+
return None
|
|
74
|
+
return entry.get("text")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def put(req_id, query, text, base=None):
|
|
78
|
+
"""写一条缓存(覆盖同 query 旧值)。"""
|
|
79
|
+
path = cache_path(req_id, base)
|
|
80
|
+
data = _load(path)
|
|
81
|
+
data[_qhash(query)] = {"query": query, "text": text, "ts": time.time()}
|
|
82
|
+
return _save(path, data)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def invalidate(req_id, base=None):
|
|
86
|
+
"""清整个 REQ 的缓存(PRD/spec 改动后调)。"""
|
|
87
|
+
return _save(cache_path(req_id, base), {})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def stats(req_id, base=None):
|
|
91
|
+
"""缓存条数 + query 摘要(调试/观测命中率用)。"""
|
|
92
|
+
data = _load(cache_path(req_id, base))
|
|
93
|
+
return {"entries": len(data),
|
|
94
|
+
"queries": [str(e.get("query", ""))[:40] for e in data.values()]}
|