@hupan56/wlkj 2.7.11 → 3.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 +375 -78
- package/package.json +29 -29
- package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
- package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
- package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
- package/templates/.qoder/learning/eval-history.jsonl +28 -0
- package/templates/data/index/wiki-index.json +8 -0
- package/templates/qoder/agents/insight-planning.md +1 -1
- package/templates/qoder/agents/insight-research.md +28 -15
- package/templates/qoder/commands/optional/wl-insight.md +159 -161
- package/templates/qoder/commands/optional/wl-report.md +4 -4
- package/templates/qoder/commands/optional/wl-status.md +2 -2
- package/templates/qoder/commands/wl-code.md +2 -2
- package/templates/qoder/commands/wl-design.md +2 -2
- package/templates/qoder/commands/wl-init.md +3 -3
- package/templates/qoder/commands/wl-prd.md +2 -2
- package/templates/qoder/commands/wl-req.md +43 -0
- package/templates/qoder/commands/wl-search.md +8 -8
- package/templates/qoder/commands/wl-task.md +17 -17
- package/templates/qoder/commands/wl-test.md +41 -15
- package/templates/qoder/config.yaml +17 -1
- package/templates/qoder/hooks/session-start.py +12 -22
- package/templates/qoder/nul +4 -0
- package/templates/qoder/rules/wl-pipeline.md +22 -48
- package/templates/qoder/scripts/README.md +139 -0
- package/templates/qoder/scripts/common/autotest_auth.py +109 -0
- package/templates/qoder/scripts/common/bootstrap.py +145 -0
- package/templates/qoder/scripts/common/check_publish.py +98 -0
- package/templates/qoder/scripts/common/cmd_registry.py +112 -0
- package/templates/qoder/scripts/common/config.py +187 -0
- package/templates/qoder/scripts/common/contract.py +317 -0
- package/templates/qoder/scripts/common/developer.py +2 -1
- package/templates/qoder/scripts/common/feishu.py +10 -9
- package/templates/qoder/scripts/common/guard.py +159 -0
- package/templates/qoder/scripts/common/identity.py +121 -2
- package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
- package/templates/qoder/scripts/common/mcp_base.py +268 -0
- package/templates/qoder/scripts/common/paths.py +187 -1
- package/templates/qoder/scripts/common/result.py +223 -0
- package/templates/qoder/scripts/common/roles.py +60 -0
- package/templates/qoder/scripts/common/task_utils.py +21 -9
- package/templates/qoder/scripts/common/test_extract.py +115 -0
- package/templates/qoder/scripts/kg/__init__.py +11 -0
- package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
- package/templates/qoder/scripts/kg/build_relations.py +127 -0
- package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
- package/templates/qoder/scripts/kg/build_workflows.py +144 -0
- package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
- package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
- package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
- package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
- package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
- package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
- package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
- package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
- package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
- package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
- package/templates/qoder/scripts/kg/prefetch.py +359 -0
- package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
- package/templates/qoder/scripts/mcp/__init__.py +11 -0
- package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
- package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
- package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
- package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
- package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
- package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
- package/templates/qoder/scripts/report/__init__.py +11 -0
- package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
- package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
- package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
- package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
- package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
- package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
- package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
- package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
- package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
- package/templates/qoder/scripts/report/req.py +222 -0
- package/templates/qoder/scripts/report/role.py +33 -0
- package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
- package/templates/qoder/scripts/setup/__init__.py +11 -0
- package/templates/qoder/scripts/setup/carriers.py +662 -0
- package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
- package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
- package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
- package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
- package/templates/qoder/scripts/{setup.py → setup/setup.py} +147 -0
- package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
- package/templates/qoder/scripts/task/__init__.py +11 -0
- package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
- package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
- package/templates/qoder/scripts/task/task.py +221 -0
- package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
- package/templates/qoder/scripts/task/task_query.py +161 -0
- package/templates/qoder/scripts/task/task_relations.py +424 -0
- package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
- package/templates/qoder/scripts/test/__init__.py +11 -0
- package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
- package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
- package/templates/qoder/scripts/test/autotest_data.py +675 -0
- package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
- package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
- package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
- package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
- package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
- package/templates/qoder/scripts/wlkj.py +116 -0
- package/templates/qoder/settings.json +1 -10
- package/templates/qoder/skills/design-import/SKILL.md +226 -226
- package/templates/qoder/skills/design-review/SKILL.md +82 -82
- package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
- package/templates/qoder/skills/prd-review/SKILL.md +5 -5
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
- package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
- package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
- package/templates/qoder/skills/test-generator/SKILL.md +5 -5
- package/templates/qoder/skills/wl-code/SKILL.md +4 -4
- package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
- package/templates/qoder/skills/wl-design/SKILL.md +3 -3
- package/templates/qoder/skills/wl-init/SKILL.md +8 -8
- package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
- package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
- package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
- package/templates/qoder/skills/wl-report/SKILL.md +7 -7
- package/templates/qoder/skills/wl-search/SKILL.md +13 -13
- package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
- package/templates/qoder/skills/wl-status/SKILL.md +5 -5
- package/templates/qoder/skills/wl-task/SKILL.md +6 -6
- package/templates/qoder/skills/wl-test/SKILL.md +102 -39
- package/templates/root/AGENTS.md +39 -40
- package/templates/qoder/hooks/inject-workflow-state.py +0 -169
- package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/check_carriers.py +0 -238
- package/templates/qoder/scripts/check_mcp_launch.py +0 -183
- package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
- package/templates/qoder/scripts/collect_prds.py +0 -31
- package/templates/qoder/scripts/common/mentions.py +0 -134
- package/templates/qoder/scripts/common/utf8.py +0 -38
- package/templates/qoder/scripts/extract_routes.py +0 -54
- package/templates/qoder/scripts/extract_routes_tree.py +0 -78
- package/templates/qoder/scripts/handoff.py +0 -22
- package/templates/qoder/scripts/init_developer.py +0 -76
- package/templates/qoder/scripts/parse_prds.py +0 -33
- package/templates/qoder/scripts/role.py +0 -51
- package/templates/qoder/scripts/sync_carriers.py +0 -259
- package/templates/qoder/scripts/task.py +0 -1261
- package/templates/qoder/scripts/workspace_init.py +0 -102
- package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
result.py - 统一结果类型 + 优雅错误处理
|
|
4
|
+
|
|
5
|
+
治"容错率低"的底层基建。现状各脚本错误风格极不统一:
|
|
6
|
+
- 裸 except Exception: pass 吞异常 (build_style_index / eval_prd / team_sync)
|
|
7
|
+
- exit code 含义不一致 (eval_prd 1=不达标或文件不存在; team_sync 0/1/2/3 四档)
|
|
8
|
+
- 调用方只能 parse 中文文案, 无法程序化决策
|
|
9
|
+
|
|
10
|
+
本模块引入:
|
|
11
|
+
1. Result 数据类: ok / code / msg / data, 语义化 code
|
|
12
|
+
2. graceful_error 装饰器: 把 Result/异常翻译成"人类/AI 可读文本 + 一致 exit code"
|
|
13
|
+
3. 统一 exit code 表 (替代散落各处的魔法数字)
|
|
14
|
+
|
|
15
|
+
设计原则 (与方案一致):
|
|
16
|
+
- 失败不崩, 输出可读诊断 + 建议下一步
|
|
17
|
+
- 调用方能据 code 程序化决策, 不靠 parse 中文
|
|
18
|
+
- eval_prd 门禁从"硬阻塞"改"软门禁" (WARN 带警告发布)
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
from common.result import Result, OK, fail, warn, graceful_error
|
|
22
|
+
|
|
23
|
+
@graceful_error
|
|
24
|
+
def main():
|
|
25
|
+
r = do_something()
|
|
26
|
+
if not r.ok:
|
|
27
|
+
return r # 装饰器翻译成可读输出 + exit code
|
|
28
|
+
return Result.ok(data=r.data)
|
|
29
|
+
|
|
30
|
+
Exit code 约定 (全脚本统一, 替代散落魔法数字):
|
|
31
|
+
0 OK 成功
|
|
32
|
+
1 GENERIC 通用失败 (未分类)
|
|
33
|
+
2 BUSY 资源占用 (如 kg_db 被占)
|
|
34
|
+
3 CONFLICT 冲突需人工 (如 team_sync SYNC_CONFLICT)
|
|
35
|
+
4 AUTHZ 身份/权限拒绝 (与 identity.EXIT_AUTHZ 对齐)
|
|
36
|
+
5 NOT_FOUND 文件/资源不存在
|
|
37
|
+
6 INDEX_MISSING 索引缺失 (需先 init/doctor)
|
|
38
|
+
7 BELOW_THRESH 门禁未达标 (eval_prd, 软门禁可带警告继续)
|
|
39
|
+
8 NETWORK 网络/外部服务不可达
|
|
40
|
+
9 DEPS_MISSING 依赖未装 (pip 包缺失)
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
import functools
|
|
44
|
+
import sys
|
|
45
|
+
import traceback
|
|
46
|
+
from dataclasses import dataclass, field
|
|
47
|
+
from typing import Any, Optional
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"Result",
|
|
51
|
+
"OK",
|
|
52
|
+
"ResultError",
|
|
53
|
+
"ok",
|
|
54
|
+
"fail",
|
|
55
|
+
"warn",
|
|
56
|
+
"graceful_error",
|
|
57
|
+
"EXIT",
|
|
58
|
+
"code_to_exit",
|
|
59
|
+
"CODE",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ============================================================
|
|
64
|
+
# 语义化 code 常量 (调用方据此程序化决策)
|
|
65
|
+
# ============================================================
|
|
66
|
+
|
|
67
|
+
class CODE:
|
|
68
|
+
"""结果 code 常量。小写字符串, 语义化, 跨脚本一致。"""
|
|
69
|
+
OK = "ok"
|
|
70
|
+
GENERIC = "generic" # 通用未分类
|
|
71
|
+
BUSY = "busy" # 资源占用 (kg_db 锁)
|
|
72
|
+
CONFLICT = "conflict" # 冲突需人工
|
|
73
|
+
AUTHZ = "authz" # 身份/权限
|
|
74
|
+
NOT_FOUND = "not_found" # 文件/资源不存在
|
|
75
|
+
INDEX_MISSING = "index_missing" # 索引缺失
|
|
76
|
+
BELOW_THRESH = "below_thresh" # 门禁未达标
|
|
77
|
+
NETWORK = "network" # 网络不可达
|
|
78
|
+
DEPS_MISSING = "deps_missing" # 依赖未装
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ============================================================
|
|
82
|
+
# code → exit code 映射 (一处定义, 全脚本统一)
|
|
83
|
+
# ============================================================
|
|
84
|
+
|
|
85
|
+
EXIT = {
|
|
86
|
+
CODE.OK: 0,
|
|
87
|
+
CODE.GENERIC: 1,
|
|
88
|
+
CODE.BUSY: 2,
|
|
89
|
+
CODE.CONFLICT: 3,
|
|
90
|
+
CODE.AUTHZ: 4,
|
|
91
|
+
CODE.NOT_FOUND: 5,
|
|
92
|
+
CODE.INDEX_MISSING: 6,
|
|
93
|
+
CODE.BELOW_THRESH: 7,
|
|
94
|
+
CODE.NETWORK: 8,
|
|
95
|
+
CODE.DEPS_MISSING: 9,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def code_to_exit(code: str) -> int:
|
|
100
|
+
"""code → exit code, 未知 code 回退 1。"""
|
|
101
|
+
return EXIT.get(code, 1)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ============================================================
|
|
105
|
+
# Result 数据类
|
|
106
|
+
# ============================================================
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class Result:
|
|
110
|
+
"""统一结果。
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
ok: 是否成功
|
|
114
|
+
code: 语义化 code (见 CODE 常量), 调用方据此决策
|
|
115
|
+
msg: 人类可读消息
|
|
116
|
+
data: 成功时携带的数据 (任意)
|
|
117
|
+
hint: 失败时的"下一步建议" (可选, 输出给用户/AI)
|
|
118
|
+
"""
|
|
119
|
+
ok: bool
|
|
120
|
+
code: str = CODE.GENERIC
|
|
121
|
+
msg: str = ""
|
|
122
|
+
data: Any = None
|
|
123
|
+
hint: Optional[str] = None
|
|
124
|
+
|
|
125
|
+
# ---- 工厂方法 ----
|
|
126
|
+
@classmethod
|
|
127
|
+
def success(cls, data: Any = None, msg: str = "", code: str = CODE.OK) -> "Result":
|
|
128
|
+
return cls(ok=True, code=code, msg=msg, data=data)
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def failure(cls, msg: str, code: str = CODE.GENERIC,
|
|
132
|
+
hint: Optional[str] = None, data: Any = None) -> "Result":
|
|
133
|
+
return cls(ok=False, code=code, msg=msg, hint=hint, data=data)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def warning(cls, msg: str, code: str = CODE.BELOW_THRESH,
|
|
137
|
+
hint: Optional[str] = None, data: Any = None) -> "Result":
|
|
138
|
+
"""WARN: 软门禁/带警告继续。ok=True (可继续), 但带警告。"""
|
|
139
|
+
return cls(ok=True, code=code, msg=msg, hint=hint, data=data)
|
|
140
|
+
|
|
141
|
+
# ---- 便捷判断 ----
|
|
142
|
+
@property
|
|
143
|
+
def is_warn(self) -> bool:
|
|
144
|
+
"""是否为"成功但有警告"(如门禁 WARN 档)。"""
|
|
145
|
+
return self.ok and self.code != CODE.OK
|
|
146
|
+
|
|
147
|
+
def __bool__(self) -> bool:
|
|
148
|
+
return self.ok
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# 模块级便捷别名 (常见用法)
|
|
152
|
+
OK = Result.success()
|
|
153
|
+
ResultError = type("ResultError", (Exception,), {}) # 预留: 未来 raise Result(r)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ok(data: Any = None, msg: str = "") -> Result:
|
|
157
|
+
return Result.success(data=data, msg=msg)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def fail(msg: str, code: str = CODE.GENERIC, hint: Optional[str] = None) -> Result:
|
|
161
|
+
return Result.failure(msg=msg, code=code, hint=hint)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def warn(msg: str, code: str = CODE.BELOW_THRESH, hint: Optional[str] = None) -> Result:
|
|
165
|
+
return Result.warning(msg=msg, code=code, hint=hint)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ============================================================
|
|
169
|
+
# graceful_error 装饰器
|
|
170
|
+
# ============================================================
|
|
171
|
+
|
|
172
|
+
def _emit(r: Result, *, exit_on_fail: bool = True) -> int:
|
|
173
|
+
"""把 Result 翻译成可读输出 + 返回 exit code。"""
|
|
174
|
+
if r.ok and not r.is_warn:
|
|
175
|
+
if r.msg:
|
|
176
|
+
print(r.msg)
|
|
177
|
+
return 0
|
|
178
|
+
if r.is_warn:
|
|
179
|
+
# WARN: 打印警告但不当失败
|
|
180
|
+
print("[WARN] {}".format(r.msg))
|
|
181
|
+
if r.hint:
|
|
182
|
+
print(" -> {}".format(r.hint))
|
|
183
|
+
return code_to_exit(r.code) if False else 0 # WARN 视为成功 (exit 0)
|
|
184
|
+
# FAIL
|
|
185
|
+
tag = r.code.upper() if r.code != CODE.GENERIC else "ERROR"
|
|
186
|
+
print("[{}] {}".format(tag, r.msg))
|
|
187
|
+
if r.hint:
|
|
188
|
+
print(" -> {}".format(r.hint))
|
|
189
|
+
if exit_on_fail:
|
|
190
|
+
sys.exit(code_to_exit(r.code))
|
|
191
|
+
return code_to_exit(r.code)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def graceful_error(func):
|
|
195
|
+
"""装饰 main(): 统一把返回值 (Result/None/int) 翻译成可读输出 + exit code。
|
|
196
|
+
|
|
197
|
+
- 返回 Result: 按 ok/code 翻译
|
|
198
|
+
- 返回 int: 直接当 exit code
|
|
199
|
+
- 返回 None: 视为成功 (exit 0)
|
|
200
|
+
- 抛异常: 捕获, 输出诊断 + traceback (失败不崩 → 但 main 层崩溃要可见)
|
|
201
|
+
"""
|
|
202
|
+
@functools.wraps(func)
|
|
203
|
+
def wrapper(*args, **kwargs):
|
|
204
|
+
try:
|
|
205
|
+
ret = func(*args, **kwargs)
|
|
206
|
+
except SystemExit:
|
|
207
|
+
raise
|
|
208
|
+
except KeyboardInterrupt:
|
|
209
|
+
print("\n[中断] 用户取消")
|
|
210
|
+
sys.exit(130)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
# 未捕获异常: 输出诊断 + 简短 traceback, 不静默吞
|
|
213
|
+
print("[ERROR] 未预期异常: {}".format(e))
|
|
214
|
+
print(" -> 这是 bug, 请反馈。traceback:")
|
|
215
|
+
traceback.print_exc()
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
if isinstance(ret, Result):
|
|
218
|
+
return _emit(ret)
|
|
219
|
+
if isinstance(ret, int):
|
|
220
|
+
sys.exit(ret)
|
|
221
|
+
# None / 其他: 成功
|
|
222
|
+
return 0
|
|
223
|
+
return wrapper
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
roles.py - 角色章节过滤 (单一真源, 消除 kg.py↔kg_mcp_server.py 逐字重复)
|
|
4
|
+
|
|
5
|
+
现状: ROLE_SECTIONS 字典 + _filter_by_role 函数在 kg.py(:645-672) 和
|
|
6
|
+
kg_mcp_server.py(:249-276) 逐字重复。改一处忘改另一处 → CLI 和 MCP 行为分叉。
|
|
7
|
+
|
|
8
|
+
本模块作为单一真源, kg.py 和 kg_mcp_server.py 都从这里 import。
|
|
9
|
+
|
|
10
|
+
过滤规则: 按角色裁剪 context_pack 输出的章节 (## 1. / ## 2. / ## 2b. 等),
|
|
11
|
+
让不同角色只看到与其职责相关的段落, 减少噪音。
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from common.roles import filter_by_role, ROLE_SECTIONS
|
|
15
|
+
text = filter_by_role(context_pack_output, role='pm')
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
__all__ = ["ROLE_SECTIONS", "filter_by_role"]
|
|
21
|
+
|
|
22
|
+
# 各角色可见的 context_pack 章节号
|
|
23
|
+
# (与 kg.py ROLE_SECTIONS / kg_mcp_server.py ROLE_SECTIONS 对齐, 此处为唯一真源)
|
|
24
|
+
ROLE_SECTIONS = {
|
|
25
|
+
'pm': {'1', '2', '2b', '3', '4', '5', '7'}, # PM: 字段+PRD+API,无代码噪音
|
|
26
|
+
'design': {'2', '2b', '3', '6'}, # 设计: 页面+布局+字段+风格
|
|
27
|
+
'dev': {'1', '3', '5', '7'}, # 开发: 代码+字段+API+wiki
|
|
28
|
+
'test': {'1', '4', '5'}, # 测试: 代码+PRD+API
|
|
29
|
+
'admin': {'4', '5'}, # 管理: PRD+API 概览
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def filter_by_role(text, role):
|
|
34
|
+
"""按角色过滤 context_pack 输出的章节。
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
text: context_pack 的多段输出 (## 1. xxx / ## 2. xxx ...)
|
|
38
|
+
role: 角色名 (pm/design/dev/test/admin), 未知角色不过滤
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
过滤后的文本 (只保留该角色可见章节)
|
|
42
|
+
"""
|
|
43
|
+
visible = ROLE_SECTIONS.get((role or '').lower())
|
|
44
|
+
if not visible:
|
|
45
|
+
return text # 未知角色不过滤
|
|
46
|
+
# 按 "## 数字." 切段 (含 2b 这种带字母的)
|
|
47
|
+
sections = re.split(r'(\n## \d+[ab]?\.)', text)
|
|
48
|
+
result = sections[0]
|
|
49
|
+
i = 1
|
|
50
|
+
while i < len(sections):
|
|
51
|
+
header = sections[i]
|
|
52
|
+
num_m = re.match(r'\n## (\d+[ab]?)\.', header)
|
|
53
|
+
if num_m and num_m.group(1) in visible:
|
|
54
|
+
result += header
|
|
55
|
+
if i + 1 < len(sections):
|
|
56
|
+
result += sections[i + 1]
|
|
57
|
+
i += 2
|
|
58
|
+
else:
|
|
59
|
+
i += 2
|
|
60
|
+
return result.strip()
|
|
@@ -173,16 +173,28 @@ def modify_task_json(task_dir: Path, mutator, timeout: float = 15.0):
|
|
|
173
173
|
|
|
174
174
|
lock_path = task_dir / ".task.lock"
|
|
175
175
|
lock = FileLock(str(lock_path), timeout=timeout, stale_seconds=300)
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
176
|
+
|
|
177
|
+
# 锁竞争超时: 退避重试一次。绝不降级为无锁写 —— modify_task_json 存在的意义
|
|
178
|
+
# 就是防并发 read-modify-write 丢更新, 两个并发方都超时降级 = 必然丢一个的更新。
|
|
179
|
+
acquired = False
|
|
180
|
+
for attempt in range(2):
|
|
181
|
+
try:
|
|
182
|
+
lock.acquire()
|
|
183
|
+
acquired = True
|
|
184
|
+
break
|
|
185
|
+
except LockTimeoutError as e:
|
|
186
|
+
if attempt == 0:
|
|
187
|
+
# 第一次超时: 短暂退避后重试 (锁持有方通常很快释放)
|
|
188
|
+
import time as _time
|
|
189
|
+
_time.sleep(0.5)
|
|
190
|
+
continue
|
|
191
|
+
# 重试仍超时: 明确失败, 不写 (宁可这次改不了, 也不丢更新)
|
|
192
|
+
print(f"ERROR: task lock busy after retry ({task_dir.name}): {e}", file=sys.stderr)
|
|
193
|
+
print(f" 另一个进程正在修改此任务, 请稍后重试。改动未生效 (未丢数据)。", file=sys.stderr)
|
|
183
194
|
return False
|
|
184
|
-
|
|
185
|
-
return
|
|
195
|
+
if not acquired:
|
|
196
|
+
return False
|
|
197
|
+
|
|
186
198
|
try:
|
|
187
199
|
data = load_task_json(task_dir)
|
|
188
200
|
if data is None:
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
Test code extractor — E1: extract structured assertions from Test.java files.
|
|
4
|
+
|
|
5
|
+
These are the "second layer" of the knowledge graph: structured business
|
|
6
|
+
knowledge (Given-When-Then, assertion patterns, service dependencies) that
|
|
7
|
+
production code extraction can never reach. 39+ Test.java files in fywl-ics
|
|
8
|
+
contain real integration tests with @Autowired services and assertion logic.
|
|
9
|
+
|
|
10
|
+
JOIN keys to other layers:
|
|
11
|
+
- injected service class → code-callgraph (who this test exercises)
|
|
12
|
+
- package path → ui-modules / prd-index (which business module)
|
|
13
|
+
- @Test method name → code-keyword (searchable)
|
|
14
|
+
"""
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
|
|
18
|
+
# @Test annotation (Junit 5)
|
|
19
|
+
RE_TEST = re.compile(r'@Test\b', re.M)
|
|
20
|
+
# @DisplayName("...")
|
|
21
|
+
RE_DISPLAY = re.compile(r'@DisplayName\s*\(\s*["\']([^"\']+)["\']\s*\)')
|
|
22
|
+
# method signature: void methodName() or public void methodName()
|
|
23
|
+
RE_METHOD = re.compile(r'(?:public\s+|private\s+|protected\s+)?(?:static\s+)?(?:void|\w+(?:<[^>]*>)?)\s+(\w+)\s*\([^)]*\)\s*(?:throws\s+[\w.,\s]+)?\s*\{', re.M)
|
|
24
|
+
# @Autowired / @Resource field: @Autowired private SomeService svc;
|
|
25
|
+
RE_INJECT = re.compile(r'@(?:Autowired|Resource)\s+(?:private\s+|protected\s+)?(\w+(?:<[^>]*>)?)\s+(\w+)\s*;', re.M)
|
|
26
|
+
# @SpringBootTest, @RunWith, @ExtendWith — marks integration tests
|
|
27
|
+
RE_INTEGRATION = re.compile(r'@(?:SpringBootTest|RunWith|ExtendWith|WebMvcTest|DataJpaTest)', re.M)
|
|
28
|
+
# Assertions: assertThat(x).isEqualTo(y), Assertions.assertEquals(x,y), Assert.X
|
|
29
|
+
RE_ASSERT = re.compile(r'(?:assertThat|Assertions?|Assert)\.(\w+)\s*\(', re.M)
|
|
30
|
+
# package declaration
|
|
31
|
+
RE_PACKAGE = re.compile(r'package\s+([\w.]+)\s*;', re.M)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def extract_test_assertions(proj_path):
|
|
35
|
+
"""Scan a project for *Test.java files and extract structured test info.
|
|
36
|
+
|
|
37
|
+
Returns a list of dicts:
|
|
38
|
+
{file, class, package, method, display_name, injected_deps[],
|
|
39
|
+
assertions[], is_integration, comment}
|
|
40
|
+
"""
|
|
41
|
+
results = []
|
|
42
|
+
if not proj_path or not os.path.isdir(proj_path):
|
|
43
|
+
return results
|
|
44
|
+
|
|
45
|
+
for root, dirs, files in os.walk(proj_path):
|
|
46
|
+
dirs[:] = [d for d in dirs if d not in ('node_modules', '.git', 'dist', '__pycache__')]
|
|
47
|
+
for f in files:
|
|
48
|
+
if not f.endswith('Test.java'):
|
|
49
|
+
continue
|
|
50
|
+
fpath = os.path.join(root, f)
|
|
51
|
+
try:
|
|
52
|
+
content = open(fpath, encoding='utf-8', errors='ignore').read()
|
|
53
|
+
except Exception:
|
|
54
|
+
continue
|
|
55
|
+
if '@Test' not in content:
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
# Class-level info
|
|
59
|
+
pkg_m = RE_PACKAGE.search(content)
|
|
60
|
+
package = pkg_m.group(1) if pkg_m else ''
|
|
61
|
+
is_integration = bool(RE_INTEGRATION.search(content))
|
|
62
|
+
|
|
63
|
+
# Injected dependencies (services/mappers under test)
|
|
64
|
+
injected = []
|
|
65
|
+
for im in RE_INJECT.finditer(content):
|
|
66
|
+
dep_type = im.group(1).split('<')[0] # strip generics
|
|
67
|
+
injected.append({'type': dep_type, 'name': im.group(2)})
|
|
68
|
+
|
|
69
|
+
# Class name (from filename, strip .java)
|
|
70
|
+
class_name = f[:-5]
|
|
71
|
+
|
|
72
|
+
# Extract each @Test method
|
|
73
|
+
test_positions = [m.start() for m in RE_TEST.finditer(content)]
|
|
74
|
+
display_map = {}
|
|
75
|
+
for dm in RE_DISPLAY.finditer(content):
|
|
76
|
+
# Associate @DisplayName with the nearest @Test after it
|
|
77
|
+
display_map[dm.end()] = dm.group(1)
|
|
78
|
+
|
|
79
|
+
for tp in test_positions:
|
|
80
|
+
# Find display name (nearest before this @Test, within 100 chars)
|
|
81
|
+
display = ''
|
|
82
|
+
for pos, name in display_map.items():
|
|
83
|
+
if pos <= tp and tp - pos < 200:
|
|
84
|
+
display = name
|
|
85
|
+
|
|
86
|
+
# Find method name after @Test
|
|
87
|
+
search_region = content[tp:tp + 300]
|
|
88
|
+
method_m = RE_METHOD.search(search_region)
|
|
89
|
+
if not method_m:
|
|
90
|
+
continue
|
|
91
|
+
method_name = method_m.group(1)
|
|
92
|
+
|
|
93
|
+
# Extract method body for assertions
|
|
94
|
+
body_start = tp + method_m.end()
|
|
95
|
+
body = content[body_start:body_start + 600]
|
|
96
|
+
assertions = list(set(m.group(1) for m in RE_ASSERT.finditer(body)))
|
|
97
|
+
|
|
98
|
+
# Extract comment before @Test (/** ... */ or // ...)
|
|
99
|
+
before = content[max(0, tp - 200):tp]
|
|
100
|
+
comment_m = re.search(r'(?:/\*\*\s*\n\s*\*\s*([^*]+?)\s*\n)|(?://\s*(.+)$)', before, re.M)
|
|
101
|
+
comment = (comment_m.group(1) or comment_m.group(2)).strip() if comment_m else ''
|
|
102
|
+
|
|
103
|
+
results.append({
|
|
104
|
+
'file': fpath.replace('\\', '/'),
|
|
105
|
+
'class': class_name,
|
|
106
|
+
'package': package,
|
|
107
|
+
'method': method_name,
|
|
108
|
+
'display_name': display,
|
|
109
|
+
'comment': comment[:80],
|
|
110
|
+
'injected_deps': [d['type'] for d in injected[:5]],
|
|
111
|
+
'assertions': assertions[:5],
|
|
112
|
+
'is_integration': is_integration,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
return results
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 子包初始化: 注入 scripts/根 + 所有子包到 sys.path
|
|
2
|
+
# 让跨包 import (旧式不带前缀) 仍可用, 兼容现有代码
|
|
3
|
+
import os as _os, sys as _sys
|
|
4
|
+
_root = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
|
|
5
|
+
if _root not in _sys.path:
|
|
6
|
+
_sys.path.insert(0, _root)
|
|
7
|
+
# 注入所有子包目录 (兼容旧式 import xxx)
|
|
8
|
+
for _sub in ('kg', 'build', 'test', 'setup', 'io'):
|
|
9
|
+
_subdir = _os.path.join(_root, _sub)
|
|
10
|
+
if _os.path.isdir(_subdir) and _subdir not in _sys.path:
|
|
11
|
+
_sys.path.insert(0, _subdir)
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""build_entity_registry.py — 从 ui-modules + code-api 构建 entity-registry.json。
|
|
3
|
+
|
|
4
|
+
# v3.0 路径自举: 引导到 common/bootstrap, 统一 sys.path 逻辑
|
|
5
|
+
import os as _o, sys as _s
|
|
6
|
+
_cp = _o.path.join(_o.path.dirname(_o.path.dirname(_o.path.abspath(__file__))), 'common')
|
|
7
|
+
if _cp not in _s.path: _s.path.insert(0, _cp)
|
|
8
|
+
from bootstrap import setup; setup()
|
|
9
|
+
|
|
10
|
+
产出 data/index/_build_cache/entity-registry.json, 供 kg_duckdb 导入 entities/aliases 表,
|
|
11
|
+
让 kg.py feature <功能名> 能返回功能画像。
|
|
12
|
+
|
|
13
|
+
结构 (与 kg_duckdb.bulk_import_from_json 契约对齐):
|
|
14
|
+
{
|
|
15
|
+
"entities": {
|
|
16
|
+
"F:<dir>": {"type":"FEATURE","canonical":"<dir>","cn":"<中文名>","aliases":[...],"properties":{...}},
|
|
17
|
+
"E:<endpoint>": {"type":"ENDPOINT","canonical":"<endpoint>","cn":"","aliases":[],"properties":{}},
|
|
18
|
+
"C:<controller>": {"type":"CONTROLLER",...}
|
|
19
|
+
},
|
|
20
|
+
"feature_adjacency": {"F:<dir>": {"endpoints": ["E:<ep>", ...]}}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
数据源:
|
|
24
|
+
- ui-modules.json 的 modules (每个 module → FEATURE entity, cn=中文名)
|
|
25
|
+
- code-api.json 的 endpoints (每个 → ENDPOINT entity)
|
|
26
|
+
- trace-chain.json 的 traces (推导 FEATURE → ENDPOINT 邻接)
|
|
27
|
+
|
|
28
|
+
注意: 这是基础实现 (从模块结构推导)。更丰富的语义 (controller/test 关联、
|
|
29
|
+
业务流程) 由 build_relations.py/build_features.py/build_workflows.py 补充,
|
|
30
|
+
那些脚本未实现时不影响本脚本的 entity 基础层。
|
|
31
|
+
"""
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import sys
|
|
35
|
+
from datetime import datetime
|
|
36
|
+
|
|
37
|
+
# 路径自适应 (和兄弟脚本一致)
|
|
38
|
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
39
|
+
sys.path.insert(0, os.path.join(THIS_DIR, '..'))
|
|
40
|
+
from common.paths import DATA_INDEX_DIR, BUILD_CACHE_DIR
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load(name):
|
|
44
|
+
"""从 _build_cache 读索引 JSON (build_style_index 的产物位置)。"""
|
|
45
|
+
for d in (str(BUILD_CACHE_DIR), str(DATA_INDEX_DIR)):
|
|
46
|
+
p = os.path.join(d, name)
|
|
47
|
+
if os.path.isfile(p):
|
|
48
|
+
try:
|
|
49
|
+
return json.load(open(p, encoding='utf-8'))
|
|
50
|
+
except (json.JSONDecodeError, OSError):
|
|
51
|
+
pass
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def build():
|
|
56
|
+
entities = {}
|
|
57
|
+
aliases_rows = [] # (alias_lower, entity_id)
|
|
58
|
+
feature_adjacency = {}
|
|
59
|
+
|
|
60
|
+
# 1. FEATURE entities (from ui-modules.json)
|
|
61
|
+
mi = load('ui-modules.json')
|
|
62
|
+
feat_count = 0
|
|
63
|
+
for proj, mods in mi.get('projects', {}).items():
|
|
64
|
+
for m in mods:
|
|
65
|
+
if not isinstance(m, dict):
|
|
66
|
+
continue
|
|
67
|
+
mdir = m.get('dir', '').strip()
|
|
68
|
+
if not mdir:
|
|
69
|
+
continue
|
|
70
|
+
eid = 'F:%s' % mdir
|
|
71
|
+
cn = m.get('cn', '').strip()
|
|
72
|
+
# cn 为空或与 dir 相同时不算有中文名
|
|
73
|
+
if not cn or cn == mdir:
|
|
74
|
+
cn = ''
|
|
75
|
+
entities[eid] = {
|
|
76
|
+
'type': 'FEATURE',
|
|
77
|
+
'canonical': mdir,
|
|
78
|
+
'cn': cn,
|
|
79
|
+
'aliases': [mdir, mdir.lower()],
|
|
80
|
+
'properties': {'project': proj, 'path': m.get('path', '')},
|
|
81
|
+
}
|
|
82
|
+
for a in entities[eid]['aliases']:
|
|
83
|
+
aliases_rows.append((a.lower(), eid))
|
|
84
|
+
feature_adjacency[eid] = {'endpoints': [], 'buttons': []}
|
|
85
|
+
feat_count += 1
|
|
86
|
+
|
|
87
|
+
# 2. ENDPOINT entities (from code-api.json + trace-chain.json 补充)
|
|
88
|
+
# trace 的 endpoint 比 code-api 多且粒度不同, 两者都收录, 避免邻接匹配落空。
|
|
89
|
+
api = load('code-api.json')
|
|
90
|
+
tc = load('trace-chain.json')
|
|
91
|
+
ep_count = 0
|
|
92
|
+
for ep, ctrl_file in api.items():
|
|
93
|
+
eid = 'E:%s' % ep
|
|
94
|
+
entities[eid] = {
|
|
95
|
+
'type': 'ENDPOINT',
|
|
96
|
+
'canonical': ep,
|
|
97
|
+
'cn': '',
|
|
98
|
+
'aliases': [ep],
|
|
99
|
+
'properties': {'controller_file': ctrl_file},
|
|
100
|
+
}
|
|
101
|
+
aliases_rows.append((ep.lower(), eid))
|
|
102
|
+
ep_count += 1
|
|
103
|
+
# 补充 trace 里独有的 endpoint
|
|
104
|
+
for t in tc.get('traces', []):
|
|
105
|
+
ep = t.get('endpoint', '')
|
|
106
|
+
if not ep:
|
|
107
|
+
continue
|
|
108
|
+
eid = 'E:%s' % ep
|
|
109
|
+
if eid not in entities:
|
|
110
|
+
entities[eid] = {
|
|
111
|
+
'type': 'ENDPOINT',
|
|
112
|
+
'canonical': ep,
|
|
113
|
+
'cn': '',
|
|
114
|
+
'aliases': [ep],
|
|
115
|
+
'properties': {'source': 'trace', 'verb': t.get('verb', '')},
|
|
116
|
+
}
|
|
117
|
+
aliases_rows.append((ep.lower(), eid))
|
|
118
|
+
ep_count += 1
|
|
119
|
+
|
|
120
|
+
# 3. FEATURE -> ENDPOINT 邻接 (from trace-chain.json, tc 已在 step 2 加载)
|
|
121
|
+
# trace 里 button 的 vue_file 路径含 module dir, 可反推它属于哪个 FEATURE。
|
|
122
|
+
feat_canonicals = [(fid, ent['canonical']) for fid, ent in entities.items()
|
|
123
|
+
if ent['type'] == 'FEATURE' and ent.get('canonical')]
|
|
124
|
+
trace_matched = 0
|
|
125
|
+
for t in tc.get('traces', []):
|
|
126
|
+
vue = (t.get('vue_file') or '').replace('\\', '/')
|
|
127
|
+
endpoint = t.get('endpoint', '')
|
|
128
|
+
if not vue or not endpoint:
|
|
129
|
+
continue
|
|
130
|
+
# 从 vue 路径找匹配的 module dir (最长匹配优先, 避免 cost 误匹配 costDetail)
|
|
131
|
+
matched_feat = None
|
|
132
|
+
best_len = 0
|
|
133
|
+
for fid, d in feat_canonicals:
|
|
134
|
+
if ('/%s/' % d) in vue or vue.endswith('/%s' % d) or ('/%s.' % d) in vue:
|
|
135
|
+
if len(d) > best_len: # 最长匹配
|
|
136
|
+
matched_feat = fid
|
|
137
|
+
best_len = len(d)
|
|
138
|
+
if matched_feat:
|
|
139
|
+
trace_matched += 1
|
|
140
|
+
adj = feature_adjacency.setdefault(matched_feat, {})
|
|
141
|
+
adj.setdefault('endpoints', [])
|
|
142
|
+
adj.setdefault('buttons', [])
|
|
143
|
+
ep_eid = 'E:%s' % endpoint
|
|
144
|
+
if ep_eid in entities and ep_eid not in adj['endpoints']:
|
|
145
|
+
adj['endpoints'].append(ep_eid)
|
|
146
|
+
# button 邻接: vue_short:handler 归属此 feature
|
|
147
|
+
vue_short = vue.split('/views/')[-1] if '/views/' in vue else vue
|
|
148
|
+
handler = t.get('handler', '')
|
|
149
|
+
btn_eid = 'B:%s:%s' % (vue_short, handler)
|
|
150
|
+
if btn_eid not in adj['buttons']:
|
|
151
|
+
adj['buttons'].append(btn_eid)
|
|
152
|
+
|
|
153
|
+
# 4. 去重 aliases
|
|
154
|
+
seen_alias = set()
|
|
155
|
+
unique_aliases = []
|
|
156
|
+
for a, eid in aliases_rows:
|
|
157
|
+
key = (a, eid)
|
|
158
|
+
if key not in seen_alias:
|
|
159
|
+
seen_alias.add(key)
|
|
160
|
+
unique_aliases.append({'alias': a, 'entity_id': eid})
|
|
161
|
+
|
|
162
|
+
registry = {
|
|
163
|
+
'version': '1.0',
|
|
164
|
+
'updated': datetime.now().isoformat(),
|
|
165
|
+
'entities': entities,
|
|
166
|
+
'aliases': unique_aliases,
|
|
167
|
+
'feature_adjacency': feature_adjacency,
|
|
168
|
+
'stats': {
|
|
169
|
+
'features': feat_count,
|
|
170
|
+
'endpoints': ep_count,
|
|
171
|
+
'total_entities': len(entities),
|
|
172
|
+
'features_with_cn': sum(1 for e in entities.values()
|
|
173
|
+
if e['type'] == 'FEATURE' and e.get('cn')),
|
|
174
|
+
'trace_matched': trace_matched,
|
|
175
|
+
'features_with_endpoints': sum(1 for v in feature_adjacency.values() if v.get('endpoints')),
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
return registry
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main():
|
|
182
|
+
print('Building entity registry...')
|
|
183
|
+
registry = build()
|
|
184
|
+
out = os.path.join(str(BUILD_CACHE_DIR), 'entity-registry.json')
|
|
185
|
+
os.makedirs(os.path.dirname(out), exist_ok=True)
|
|
186
|
+
with open(out, 'w', encoding='utf-8') as f:
|
|
187
|
+
json.dump(registry, f, ensure_ascii=False, indent=2, sort_keys=True)
|
|
188
|
+
s = registry['stats']
|
|
189
|
+
print(' FEATURE entities: %d (%d 有中文名)' % (s['features'], s['features_with_cn']))
|
|
190
|
+
print(' ENDPOINT entities: %d' % s['endpoints'])
|
|
191
|
+
print(' aliases: %d' % len(registry['aliases']))
|
|
192
|
+
print(' Saved to: %s' % out)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
if __name__ == '__main__':
|
|
196
|
+
main()
|