@hupan56/wlkj 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +269 -11
- package/package.json +1 -1
- package/templates/qoder/agents/insight-planning.md +67 -0
- package/templates/qoder/agents/insight-research.md +61 -0
- package/templates/qoder/agents/prd-reference.md +8 -2
- package/templates/qoder/commands/optional/wl-insight.md +275 -0
- package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
- package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
- package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
- package/templates/qoder/commands/wl-code.md +10 -2
- package/templates/qoder/commands/wl-commit.md +1 -1
- package/templates/qoder/commands/wl-design-draw.md +78 -0
- package/templates/qoder/commands/wl-design-scan.md +108 -0
- package/templates/qoder/commands/wl-design-spec.md +154 -0
- package/templates/qoder/commands/wl-design.md +32 -0
- package/templates/qoder/commands/wl-init.md +24 -3
- package/templates/qoder/commands/wl-prd-full.md +226 -0
- package/templates/qoder/commands/wl-prd-quick.md +134 -0
- package/templates/qoder/commands/wl-prd-review.md +104 -0
- package/templates/qoder/commands/wl-prd.md +17 -311
- package/templates/qoder/commands/wl-search.md +66 -30
- package/templates/qoder/commands/wl-task.md +290 -59
- package/templates/qoder/commands/wl-test.md +92 -24
- package/templates/qoder/config.yaml +51 -15
- package/templates/qoder/hooks/inject-workflow-state.py +2 -2
- package/templates/qoder/hooks/session-start.py +82 -51
- package/templates/qoder/rules/wl-pipeline.md +216 -105
- package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/archive_prd.py +377 -0
- package/templates/qoder/scripts/autotest.py +1715 -0
- package/templates/qoder/scripts/autotest_batch.py +224 -0
- package/templates/qoder/scripts/autotest_run.py +297 -0
- package/templates/qoder/scripts/benchmark.py +210 -209
- package/templates/qoder/scripts/build_style_index.py +444 -4
- package/templates/qoder/scripts/check_carriers.py +238 -0
- package/templates/qoder/scripts/check_mcp.py +298 -0
- package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
- package/templates/qoder/scripts/common/events.py +46 -0
- package/templates/qoder/scripts/common/extract.py +419 -0
- package/templates/qoder/scripts/common/graph_traverse.py +533 -0
- package/templates/qoder/scripts/common/paths.py +89 -0
- package/templates/qoder/scripts/common/pip_install.py +144 -0
- package/templates/qoder/scripts/common/platform_guard.py +61 -0
- package/templates/qoder/scripts/common/search_engine.py +205 -205
- package/templates/qoder/scripts/common/terms.py +57 -0
- package/templates/qoder/scripts/common/ts_extract.py +536 -0
- package/templates/qoder/scripts/context_pack.py +73 -13
- package/templates/qoder/scripts/enrich_prompt.py +226 -0
- package/templates/qoder/scripts/eval_prd.py +318 -231
- package/templates/qoder/scripts/export.py +487 -487
- package/templates/qoder/scripts/extract_api_params.py +246 -0
- package/templates/qoder/scripts/extract_routes.py +54 -0
- package/templates/qoder/scripts/extract_routes_tree.py +78 -0
- package/templates/qoder/scripts/fill_prototype.py +707 -0
- package/templates/qoder/scripts/gen_design_doc.py +394 -0
- package/templates/qoder/scripts/git_sync.py +27 -15
- package/templates/qoder/scripts/init_doctor.py +292 -40
- package/templates/qoder/scripts/install_qoderwork.py +366 -9
- package/templates/qoder/scripts/kg.py +708 -0
- package/templates/qoder/scripts/kg_auto_login.py +196 -0
- package/templates/qoder/scripts/kg_build.py +612 -0
- package/templates/qoder/scripts/kg_build_db.py +327 -0
- package/templates/qoder/scripts/kg_duckdb.py +549 -0
- package/templates/qoder/scripts/kg_incremental.py +393 -0
- package/templates/qoder/scripts/kg_link_db.py +224 -0
- package/templates/qoder/scripts/kg_mcp_server.py +801 -0
- package/templates/qoder/scripts/kg_semantic.py +150 -0
- package/templates/qoder/scripts/kg_test_runner.py +241 -0
- package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
- package/templates/qoder/scripts/learn.py +118 -39
- package/templates/qoder/scripts/learn_aggregate.py +201 -0
- package/templates/qoder/scripts/mcp_launcher.py +359 -0
- package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
- package/templates/qoder/scripts/repo_root.py +106 -0
- package/templates/qoder/scripts/role.py +12 -0
- package/templates/qoder/scripts/run_weekly_update.bat +5 -0
- package/templates/qoder/scripts/run_weekly_update.sh +5 -0
- package/templates/qoder/scripts/search_index.py +307 -60
- package/templates/qoder/scripts/secure-ls.js +5640 -0
- package/templates/qoder/scripts/setup.py +706 -641
- package/templates/qoder/scripts/setup_lanhu.py +963 -0
- package/templates/qoder/scripts/status.py +250 -11
- package/templates/qoder/scripts/sync_carriers.py +259 -0
- package/templates/qoder/scripts/syncgate.py +2 -2
- package/templates/qoder/scripts/task.py +75 -0
- package/templates/qoder/scripts/team_sync.py +60 -4
- package/templates/qoder/scripts/workspace_init.py +1 -1
- package/templates/qoder/skills/design-import/SKILL.md +226 -0
- package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
- package/templates/qoder/skills/design-review/SKILL.md +82 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +184 -60
- package/templates/qoder/skills/prd-review/SKILL.md +18 -1
- package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
- package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
- package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
- package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
- package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
- package/templates/qoder/skills/test-generator/SKILL.md +15 -2
- package/templates/qoder/skills/wl-code/SKILL.md +55 -36
- package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
- package/templates/qoder/skills/wl-design/SKILL.md +55 -0
- package/templates/qoder/skills/wl-init/SKILL.md +76 -67
- package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
- package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
- package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
- package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
- package/templates/qoder/skills/wl-report/SKILL.md +131 -107
- package/templates/qoder/skills/wl-search/SKILL.md +141 -75
- package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
- package/templates/qoder/skills/wl-status/SKILL.md +83 -61
- package/templates/qoder/skills/wl-task/SKILL.md +132 -58
- package/templates/qoder/skills/wl-test/SKILL.md +406 -40
- package/templates/qoder/templates/prototype-app.html +13 -8
- package/templates/qoder/templates/prototype-web.html +376 -93
- package/templates/root/AGENTS.md +89 -34
- package/templates/root/requirements.txt +21 -0
- package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
- package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
- package/templates/qoder/agents/prd-planning.md +0 -57
- package/templates/qoder/agents/prd-research.md +0 -34
- package/templates/qoder/commands/wl-insight.md +0 -51
- package/templates/qoder/scripts/__pycache__/init_doctor.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/__pycache__/setup.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/common/__pycache__/developer.cpython-39.pyc +0 -0
- package/templates/qoder/skills/wl-prd/SKILL.md +0 -92
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
kg.py — QODER 知识图谱统一 CLI 入口(全系列通用)
|
|
4
|
+
|
|
5
|
+
把原本只在 QoderWork MCP (kg_mcp_server.py) 里的 13 个知识图谱能力,
|
|
6
|
+
统一成一套命令行子命令。Qoder IDE / Quest / CLI 直接跑;QoderWork 仍可
|
|
7
|
+
用 MCP,本脚本是新增的并列通道(功能等价)。
|
|
8
|
+
|
|
9
|
+
设计原则:
|
|
10
|
+
1. 不复制逻辑 — 调底层: search_index / context_pack / graph_traverse /
|
|
11
|
+
kg_duckdb / repowiki / fill_prototype / gen_design_doc
|
|
12
|
+
2. 全容错 — DuckDB/索引缺失优雅降级,给修复提示,绝不抛栈给用户
|
|
13
|
+
3. 参数统一 — 13 个子命令风格一致,对标 MCP 工具名
|
|
14
|
+
4. 纯文本输出 — UTF-8,无日志噪音,AI 直接读
|
|
15
|
+
|
|
16
|
+
用法:
|
|
17
|
+
python .qoder/scripts/kg.py <子命令> [参数]
|
|
18
|
+
|
|
19
|
+
python .qoder/scripts/kg.py search 考勤 --platform web
|
|
20
|
+
python .qoder/scripts/kg.py impact /asset
|
|
21
|
+
python .qoder/scripts/kg.py coverage
|
|
22
|
+
python .qoder/scripts/kg.py feature 资产管理
|
|
23
|
+
python .qoder/scripts/kg.py workflow assets
|
|
24
|
+
python .qoder/scripts/kg.py hop 资产管理 --depth 3
|
|
25
|
+
"""
|
|
26
|
+
import io
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
# UTF-8 输出(Windows 终端默认 GBK 会乱码)
|
|
32
|
+
try:
|
|
33
|
+
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
38
|
+
BASE_DIR = os.path.dirname(os.path.dirname(SCRIPTS_DIR)) # repo root
|
|
39
|
+
sys.path.insert(0, SCRIPTS_DIR)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ── 路径与索引新鲜度(与底层一致)──────────────────────────────────────
|
|
43
|
+
def _index_dir():
|
|
44
|
+
"""获取 data/index 目录(延迟导入,避免顶层崩)。"""
|
|
45
|
+
try:
|
|
46
|
+
from common.paths import DATA_INDEX_DIR
|
|
47
|
+
return str(DATA_INDEX_DIR)
|
|
48
|
+
except Exception:
|
|
49
|
+
# 兜底: 常规布局
|
|
50
|
+
return os.path.join(BASE_DIR, 'data', 'index')
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _kg_db_path():
|
|
54
|
+
return os.path.join(_index_dir(), 'kg.duckdb')
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _has_duckdb():
|
|
58
|
+
"""知识图谱 DuckDB 是否已构建。"""
|
|
59
|
+
return os.path.isfile(_kg_db_path())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _hint_rebuild(action_cn='知识图谱'):
|
|
63
|
+
"""DuckDB/索引缺失时的统一修复提示。"""
|
|
64
|
+
return (
|
|
65
|
+
"⚠️ %s未构建或缺失。\n"
|
|
66
|
+
"修复方法(任选其一):\n"
|
|
67
|
+
" 1. 对 AI 说「初始化」(/wl-init) — 自动刷新索引\n"
|
|
68
|
+
" 2. 手动构建: python .qoder/scripts/kg_build.py\n"
|
|
69
|
+
" 3. 自检修复: python .qoder/scripts/init_doctor.py --fix\n"
|
|
70
|
+
"本次查询无法继续,但其他子命令(不依赖图谱的)仍可用。"
|
|
71
|
+
% action_cn
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── 子命令实现 ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
def cmd_search(args):
|
|
78
|
+
"""搜代码: 按关键词找代码文件。"""
|
|
79
|
+
if not args:
|
|
80
|
+
return "用法: kg.py search <关键词> [--platform web|app]"
|
|
81
|
+
kw, plat = _split_kw_platform(args)
|
|
82
|
+
if not kw:
|
|
83
|
+
return "请提供搜索关键词"
|
|
84
|
+
import search_index as S
|
|
85
|
+
# search_keywords(query, platform=None) 自带中文扩展,不需外层 expand
|
|
86
|
+
return _capture(S.search_keywords, kw, plat) or "无匹配结果"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def cmd_api(args):
|
|
90
|
+
"""搜 API 端点。"""
|
|
91
|
+
if not args:
|
|
92
|
+
return "用法: kg.py api <关键词>"
|
|
93
|
+
import search_index as S
|
|
94
|
+
return _capture(S.search_api, args[0]) or "无匹配 API"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def cmd_prd(args):
|
|
98
|
+
"""搜 PRD(防重复造轮子)。"""
|
|
99
|
+
if not args:
|
|
100
|
+
return "用法: kg.py prd <关键词>"
|
|
101
|
+
import search_index as S
|
|
102
|
+
return _capture(S.search_prds, args[0]) or "无匹配 PRD"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cmd_impact(args):
|
|
106
|
+
"""影响分析: 改某接口影响哪些前端页面/按钮。"""
|
|
107
|
+
if not args:
|
|
108
|
+
return "用法: kg.py impact <端点或关键词> 例: /asset 或 export"
|
|
109
|
+
import search_index as S
|
|
110
|
+
return _capture(S.search_impact, args[0]) or "未找到匹配端点"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_context360(args):
|
|
114
|
+
"""符号 360 度视图: 端点/函数/处理器的全部关联。"""
|
|
115
|
+
if not args:
|
|
116
|
+
return "用法: kg.py context360 <符号> 例: handleExport 或 /system/role"
|
|
117
|
+
import search_index as S
|
|
118
|
+
return _capture(S.search_context360, args[0]) or "未找到匹配符号"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def cmd_context(args):
|
|
122
|
+
"""上下文打包: 一次返回关键词相关的全部上下文。支持 --role 角色裁剪。"""
|
|
123
|
+
if not args:
|
|
124
|
+
return ("用法: kg.py context <关键词> [--platform web|app] "
|
|
125
|
+
"[--role pm|design|dev|test|admin]")
|
|
126
|
+
rest = list(args)
|
|
127
|
+
role = ''
|
|
128
|
+
plat = ''
|
|
129
|
+
# 解析 --role / --platform
|
|
130
|
+
rest2 = []
|
|
131
|
+
i = 0
|
|
132
|
+
while i < len(rest):
|
|
133
|
+
a = rest[i]
|
|
134
|
+
if a == '--role' and i + 1 < len(rest):
|
|
135
|
+
role = rest[i + 1]; i += 2; continue
|
|
136
|
+
if a == '--platform' and i + 1 < len(rest):
|
|
137
|
+
plat = rest[i + 1]; i += 2; continue
|
|
138
|
+
rest2.append(a); i += 1
|
|
139
|
+
kw = rest2[0] if rest2 else ''
|
|
140
|
+
if not kw:
|
|
141
|
+
return "请提供业务关键词"
|
|
142
|
+
import context_pack as C
|
|
143
|
+
full = _capture(C._compute_and_print, kw, plat, '')
|
|
144
|
+
if not full:
|
|
145
|
+
return "无匹配上下文"
|
|
146
|
+
if role:
|
|
147
|
+
return _filter_by_role(full, role)
|
|
148
|
+
return full
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def cmd_coverage(args):
|
|
152
|
+
"""功能×测试覆盖矩阵: 哪些功能有测试,哪些是盲区。"""
|
|
153
|
+
from common.graph_traverse import CodeGraph
|
|
154
|
+
g = CodeGraph(_index_dir())
|
|
155
|
+
return g.coverage_matrix() or "无法生成覆盖矩阵(索引可能为空)"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def cmd_feature(args):
|
|
159
|
+
"""功能画像: 一个功能的完整画像(端点+按钮+测试)。
|
|
160
|
+
|
|
161
|
+
DuckDB 优先(DuckDB 数据最全);DuckDB 缺失时回退到 CodeGraph 的 JSON 路径。
|
|
162
|
+
"""
|
|
163
|
+
if not args:
|
|
164
|
+
return "用法: kg.py feature <功能名> 例: 资产管理 或 asset 或 system"
|
|
165
|
+
feat = args[0]
|
|
166
|
+
# DuckDB 优先(数据最全,coverage 已验证可用)
|
|
167
|
+
if _has_duckdb():
|
|
168
|
+
try:
|
|
169
|
+
import duckdb
|
|
170
|
+
except ImportError:
|
|
171
|
+
return "未安装 duckdb。请运行: pip install duckdb"
|
|
172
|
+
con = duckdb.connect(_kg_db_path(), read_only=True)
|
|
173
|
+
try:
|
|
174
|
+
result = _feature_duckdb(con, feat)
|
|
175
|
+
if result:
|
|
176
|
+
return result
|
|
177
|
+
finally:
|
|
178
|
+
try: con.close()
|
|
179
|
+
except Exception: pass
|
|
180
|
+
# 回退: CodeGraph 的 JSON 实现(entity-registry.json 通常不存在)
|
|
181
|
+
from common.graph_traverse import CodeGraph
|
|
182
|
+
g = CodeGraph(_index_dir())
|
|
183
|
+
return g.feature_overview(feat) or "未找到功能 '%s'" % feat
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _feature_duckdb(con, query):
|
|
187
|
+
"""从 DuckDB 查功能画像(feature → endpoints/buttons/tests)。
|
|
188
|
+
|
|
189
|
+
别名解析: aliases 精确 → canonical/cn 模糊 → entity_id 含关键词。
|
|
190
|
+
"""
|
|
191
|
+
q = (query or '').strip()
|
|
192
|
+
# 1) 解析为 FEATURE entity_id
|
|
193
|
+
target = None
|
|
194
|
+
resolved = con.execute(
|
|
195
|
+
"SELECT entity_id FROM aliases WHERE alias = ? "
|
|
196
|
+
"AND entity_id IN (SELECT id FROM entities WHERE type='FEATURE')",
|
|
197
|
+
[q.lower()]
|
|
198
|
+
).fetchone()
|
|
199
|
+
if resolved:
|
|
200
|
+
target = resolved[0]
|
|
201
|
+
else:
|
|
202
|
+
# canonical / cn 模糊
|
|
203
|
+
row = con.execute(
|
|
204
|
+
"SELECT id FROM entities WHERE type='FEATURE' "
|
|
205
|
+
"AND (LOWER(canonical)=? OR cn=? OR LOWER(canonical) LIKE '%'||?||'%' OR cn LIKE '%'||?||'%') "
|
|
206
|
+
"LIMIT 1",
|
|
207
|
+
[q.lower(), q, q.lower(), q]
|
|
208
|
+
).fetchone()
|
|
209
|
+
if row:
|
|
210
|
+
target = row[0]
|
|
211
|
+
if not target:
|
|
212
|
+
# 列出可用 feature 帮用户选
|
|
213
|
+
feats = con.execute(
|
|
214
|
+
"SELECT canonical, cn FROM entities WHERE type='FEATURE' AND cn<>'' ORDER BY cn LIMIT 30"
|
|
215
|
+
).fetchall()
|
|
216
|
+
cands = ', '.join('%s(%s)' % (c, cn) if cn and cn != c else c for c, cn in feats)
|
|
217
|
+
return "未找到功能 '%s'。可用功能: %s" % (q, cands)
|
|
218
|
+
|
|
219
|
+
# 2) 取 feature 基本信息
|
|
220
|
+
info = con.execute(
|
|
221
|
+
"SELECT canonical, cn FROM entities WHERE id=?", [target]
|
|
222
|
+
).fetchone()
|
|
223
|
+
canon, cn = info if info else (target, target)
|
|
224
|
+
|
|
225
|
+
# 3) 统计端点 / 按钮
|
|
226
|
+
endpoints = con.execute(
|
|
227
|
+
"SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_endpoint'", [target]
|
|
228
|
+
).fetchall()
|
|
229
|
+
buttons = con.execute(
|
|
230
|
+
"SELECT to_id FROM edges WHERE from_id=? AND edge_type='has_button'", [target]
|
|
231
|
+
).fetchall()
|
|
232
|
+
|
|
233
|
+
# 4) 关联测试: feature 的端点 -(handled_by)-> controller -(tested_by)-> test
|
|
234
|
+
try:
|
|
235
|
+
tests = con.execute("""
|
|
236
|
+
SELECT DISTINCT tb.to_id FROM edges he
|
|
237
|
+
JOIN edges tb ON tb.from_id = he.to_id AND tb.edge_type='tested_by'
|
|
238
|
+
WHERE he.from_id=? AND he.edge_type='handled_by'
|
|
239
|
+
""", [target]).fetchall()
|
|
240
|
+
except Exception:
|
|
241
|
+
tests = []
|
|
242
|
+
|
|
243
|
+
ep_ids = [e[0] for e in endpoints]
|
|
244
|
+
btn_ids = [b[0] for b in buttons]
|
|
245
|
+
test_ids = [t[0] for t in tests]
|
|
246
|
+
|
|
247
|
+
# 5) 组装画像
|
|
248
|
+
out = ['## 功能画像: %s (%s)' % (cn, canon)]
|
|
249
|
+
out.append(' 端点 %d 个 | 按钮操作 %d 个 | 关联测试 %d 个' % (
|
|
250
|
+
len(ep_ids), len(btn_ids), len(test_ids)))
|
|
251
|
+
out.append('')
|
|
252
|
+
|
|
253
|
+
out.append('### 端点 (%d)' % len(ep_ids))
|
|
254
|
+
for eid in ep_ids[:10]:
|
|
255
|
+
out.append(' %s' % eid)
|
|
256
|
+
if len(ep_ids) > 10:
|
|
257
|
+
out.append(' ... 还有 %d 个' % (len(ep_ids) - 10))
|
|
258
|
+
out.append('')
|
|
259
|
+
|
|
260
|
+
out.append('### 按钮操作 (%d)' % len(btn_ids))
|
|
261
|
+
# 提取按钮的操作动词(handleAdd/handleDel 等)
|
|
262
|
+
verbs = {}
|
|
263
|
+
for bid in btn_ids:
|
|
264
|
+
# bid 形如 B:path/to/file.vue:handleAdd
|
|
265
|
+
if ':' in bid:
|
|
266
|
+
handler = bid.rsplit(':', 1)[-1]
|
|
267
|
+
else:
|
|
268
|
+
handler = bid
|
|
269
|
+
verbs[handler] = verbs.get(handler, 0) + 1
|
|
270
|
+
top_verbs = sorted(verbs.items(), key=lambda x: -x[1])[:15]
|
|
271
|
+
out.append(' 操作分布: %s' % ', '.join('%s×%d' % (v, n) for v, n in top_verbs))
|
|
272
|
+
out.append('')
|
|
273
|
+
|
|
274
|
+
out.append('### 关联测试 (%d)' % len(test_ids))
|
|
275
|
+
shown = 0
|
|
276
|
+
for tid in test_ids:
|
|
277
|
+
if shown >= 6:
|
|
278
|
+
out.append(' ... 还有 %d 个' % (len(test_ids) - 6))
|
|
279
|
+
break
|
|
280
|
+
# tid 形如 T:ClassName.method,查 tests 表拿断言
|
|
281
|
+
if tid.startswith('T:'):
|
|
282
|
+
ref = tid[2:]
|
|
283
|
+
cls = method = ''
|
|
284
|
+
if '.' in ref:
|
|
285
|
+
cls, method = ref.rsplit('.', 1)
|
|
286
|
+
trow = con.execute(
|
|
287
|
+
"SELECT display_name, assertions FROM tests WHERE class=? AND method=? LIMIT 1",
|
|
288
|
+
[cls, method]
|
|
289
|
+
).fetchone()
|
|
290
|
+
if trow:
|
|
291
|
+
dn, asserts = trow
|
|
292
|
+
import json as _json
|
|
293
|
+
try:
|
|
294
|
+
a_list = _json.loads(asserts) if isinstance(asserts, str) else asserts
|
|
295
|
+
a_str = ', '.join(a_list[:3]) if a_list else ''
|
|
296
|
+
except Exception:
|
|
297
|
+
a_str = ''
|
|
298
|
+
out.append(' %s — %s%s' % (ref, dn or '', (' [断言: %s]' % a_str) if a_str else ''))
|
|
299
|
+
else:
|
|
300
|
+
out.append(' %s' % ref)
|
|
301
|
+
else:
|
|
302
|
+
out.append(' %s' % tid)
|
|
303
|
+
shown += 1
|
|
304
|
+
|
|
305
|
+
if len(test_ids) == 0:
|
|
306
|
+
out.append(' ⚠️ 该功能无关联测试,是回归测试盲区')
|
|
307
|
+
|
|
308
|
+
return '\n'.join(out)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def cmd_workflow(args):
|
|
312
|
+
"""业务流程链: 一个模块的完整操作链(query→create→audit→...)。
|
|
313
|
+
|
|
314
|
+
优先匹配英文模块名(assets/cost/hr/...),中文输入返回候选。
|
|
315
|
+
数据源: DuckDB workflows 表。
|
|
316
|
+
"""
|
|
317
|
+
if not args:
|
|
318
|
+
return ("用法: kg.py workflow <模块名> 例: assets / cost / hr\n"
|
|
319
|
+
"可选模块: assets/cost/hr/maintenance/operation/"
|
|
320
|
+
"qualityAndSafety/veh/settings/dashboard/flow/systems/monitor/quasafe")
|
|
321
|
+
module = args[0]
|
|
322
|
+
if not _has_duckdb():
|
|
323
|
+
return _hint_rebuild('业务流程数据(DuckDB)')
|
|
324
|
+
try:
|
|
325
|
+
import duckdb
|
|
326
|
+
except ImportError:
|
|
327
|
+
return ("未安装 duckdb。请运行: pip install duckdb")
|
|
328
|
+
con = duckdb.connect(_kg_db_path(), read_only=True)
|
|
329
|
+
try:
|
|
330
|
+
return _workflow_query(con, module)
|
|
331
|
+
finally:
|
|
332
|
+
try: con.close()
|
|
333
|
+
except Exception: pass
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _workflow_query(con, q):
|
|
337
|
+
"""workflows 表查询 + 脏数据清洗 + 中文兜底(从 kg_mcp_server 平移)。"""
|
|
338
|
+
q = (q or '').strip()
|
|
339
|
+
# 1) 精确 + 模糊匹配 workflows.module(英文)
|
|
340
|
+
hits = con.execute(
|
|
341
|
+
"SELECT DISTINCT module FROM workflows WHERE module = ? OR module LIKE '%' || ? || '%' ORDER BY module",
|
|
342
|
+
[q.lower(), q.lower()]
|
|
343
|
+
).fetchall()
|
|
344
|
+
if not hits:
|
|
345
|
+
# 2) 中文兜底: 查 entities.cn 返回英文候选
|
|
346
|
+
cn_hits = con.execute(
|
|
347
|
+
"SELECT canonical, cn FROM entities WHERE type IN ('FEATURE','MODULE') AND cn <> '' AND (cn LIKE '%' || ? OR cn LIKE '%' || ? || '%')",
|
|
348
|
+
[q, q]
|
|
349
|
+
).fetchall()
|
|
350
|
+
if cn_hits:
|
|
351
|
+
cands = ', '.join('%s(%s)' % (c[0], c[1]) for c in cn_hits[:8])
|
|
352
|
+
return "未在 workflows 表找到模块 '%s'。但 entities 表有相近中文名,可换这些英文模块名重试:\n %s" % (q, cands)
|
|
353
|
+
all_mods = [r[0] for r in con.execute("SELECT DISTINCT module FROM workflows ORDER BY module").fetchall()]
|
|
354
|
+
return "未找到模块 '%s'。现有模块: %s" % (q, ', '.join(all_mods))
|
|
355
|
+
|
|
356
|
+
out = []
|
|
357
|
+
for (mod,) in hits:
|
|
358
|
+
# 清洗: 过滤空端点和模板泄漏按钮
|
|
359
|
+
rows = con.execute("""
|
|
360
|
+
SELECT state, seq, button, endpoint FROM workflows
|
|
361
|
+
WHERE module = ?
|
|
362
|
+
AND endpoint <> 'None' AND endpoint <> ''
|
|
363
|
+
AND button NOT LIKE '{{%'
|
|
364
|
+
ORDER BY seq, state
|
|
365
|
+
""", [mod]).fetchall()
|
|
366
|
+
if not rows:
|
|
367
|
+
continue
|
|
368
|
+
out.append('=== %s (%d 步, 已清洗脏数据) ===' % (mod, len(rows)))
|
|
369
|
+
cur_seq = -1
|
|
370
|
+
for st, sq, btn, ep in rows:
|
|
371
|
+
if sq != cur_seq:
|
|
372
|
+
out.append(' [%d] %s' % (sq, st))
|
|
373
|
+
cur_seq = sq
|
|
374
|
+
out.append(' %-16s -> %s' % (btn, ep))
|
|
375
|
+
out.append('')
|
|
376
|
+
return '\n'.join(out).strip() or "模块 '%s' 清洗后无有效操作链" % q
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def cmd_hop(args):
|
|
380
|
+
"""多跳遍历: 从一个符号出发,递归 N 跳找全部可达实体。
|
|
381
|
+
|
|
382
|
+
知识图谱核心能力。支持别名解析(传中文/端点/函数名自动转 entity_id)。
|
|
383
|
+
"""
|
|
384
|
+
if not args:
|
|
385
|
+
return "用法: kg.py hop <符号> [--depth 1-5] 例: 资产管理 或 /asset/list"
|
|
386
|
+
symbol, depth = _split_symbol_depth(args)
|
|
387
|
+
if not _has_duckdb():
|
|
388
|
+
return _hint_rebuild('知识图谱(DuckDB)')
|
|
389
|
+
try:
|
|
390
|
+
import duckdb
|
|
391
|
+
except ImportError:
|
|
392
|
+
return "未安装 duckdb。请运行: pip install duckdb"
|
|
393
|
+
import kg_duckdb as K
|
|
394
|
+
con = duckdb.connect(_kg_db_path(), read_only=True)
|
|
395
|
+
try:
|
|
396
|
+
# 1) 别名解析(三级兜底)
|
|
397
|
+
start_id = symbol
|
|
398
|
+
resolved = con.execute(
|
|
399
|
+
"SELECT entity_id FROM aliases WHERE alias = ?", [symbol.lower()]
|
|
400
|
+
).fetchone()
|
|
401
|
+
if resolved:
|
|
402
|
+
start_id = resolved[0]
|
|
403
|
+
else:
|
|
404
|
+
fuzzy = con.execute(
|
|
405
|
+
"SELECT id FROM entities WHERE id = ? OR id LIKE '%' || ? || '%' LIMIT 1",
|
|
406
|
+
[symbol, symbol]
|
|
407
|
+
).fetchone()
|
|
408
|
+
if fuzzy:
|
|
409
|
+
start_id = fuzzy[0]
|
|
410
|
+
else:
|
|
411
|
+
edge_hit = con.execute(
|
|
412
|
+
"SELECT from_id FROM edges WHERE from_id LIKE '%' || ? || '%' LIMIT 1",
|
|
413
|
+
[symbol]
|
|
414
|
+
).fetchone()
|
|
415
|
+
if edge_hit:
|
|
416
|
+
start_id = edge_hit[0]
|
|
417
|
+
|
|
418
|
+
# 2) 递归 CTE 遍历
|
|
419
|
+
try:
|
|
420
|
+
hops = K.multi_hop(con, start_id, max_depth=depth)
|
|
421
|
+
except Exception as e:
|
|
422
|
+
return "遍历出错 (起点=%s): %s" % (start_id, str(e))
|
|
423
|
+
|
|
424
|
+
if not hops or (len(hops) == 1 and hops[0][1] == 0):
|
|
425
|
+
return "从 '%s' (解析为 %s) 出发无可达节点。可能该符号不在图谱中,或无出边。" % (symbol, start_id)
|
|
426
|
+
|
|
427
|
+
# 3) 按 depth 分组展示
|
|
428
|
+
from collections import defaultdict
|
|
429
|
+
by_depth = defaultdict(list)
|
|
430
|
+
for eid, d in hops:
|
|
431
|
+
by_depth[d].append(eid)
|
|
432
|
+
|
|
433
|
+
out = ['=== 多跳遍历: %s (解析为 %s) ===' % (symbol, start_id)]
|
|
434
|
+
out.append('可达 %d 个实体, 最大 %d 跳:' % (len(hops), max(by_depth.keys())))
|
|
435
|
+
out.append('')
|
|
436
|
+
for d in sorted(by_depth.keys()):
|
|
437
|
+
eids = by_depth[d]
|
|
438
|
+
out.append('[第 %d 跳] %d 个实体:' % (d, len(eids)))
|
|
439
|
+
shown = 0
|
|
440
|
+
for eid in eids:
|
|
441
|
+
if shown >= 12:
|
|
442
|
+
out.append(' ... 还有 %d 个' % (len(eids) - 12))
|
|
443
|
+
break
|
|
444
|
+
info = con.execute(
|
|
445
|
+
"SELECT type, canonical, cn FROM entities WHERE id = ?", [eid]
|
|
446
|
+
).fetchone()
|
|
447
|
+
if info:
|
|
448
|
+
t, c, cn = info
|
|
449
|
+
label = '%s / %s' % (c, cn) if cn and cn != c else c
|
|
450
|
+
out.append(' %-14s %-12s %s' % ('[%s]' % t, eid[:40], label))
|
|
451
|
+
else:
|
|
452
|
+
out.append(' %-14s %s' % ('[edge]', eid[:60]))
|
|
453
|
+
shown += 1
|
|
454
|
+
out.append('')
|
|
455
|
+
return '\n'.join(out).strip()
|
|
456
|
+
finally:
|
|
457
|
+
try: con.close()
|
|
458
|
+
except Exception: pass
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def cmd_wiki(args):
|
|
462
|
+
"""Repo Wiki 搜索: 查模块级语义文档(比读源码省 token)。
|
|
463
|
+
|
|
464
|
+
只读 wiki-index.json,不触发自动构建(慢操作)。
|
|
465
|
+
"""
|
|
466
|
+
if not args:
|
|
467
|
+
return "用法: kg.py wiki <关键词>"
|
|
468
|
+
kw = args[0]
|
|
469
|
+
from common.paths import DATA_INDEX_DIR as _idx, PROJECT_ROOT as _base
|
|
470
|
+
wiki_path = os.path.join(str(_idx), 'wiki-index.json')
|
|
471
|
+
if not os.path.isfile(wiki_path):
|
|
472
|
+
return ("wiki-index.json 不存在。请在终端手动构建(一次性):\n"
|
|
473
|
+
" python .qoder/scripts/search_index.py wiki\n"
|
|
474
|
+
"(需要先在 Qoder IDE 对项目生成 Repo Wiki)")
|
|
475
|
+
try:
|
|
476
|
+
from common.repowiki import search_wiki
|
|
477
|
+
from common.terms import get_cn_map_with_auto
|
|
478
|
+
except ImportError as e:
|
|
479
|
+
return "Wiki 搜索模块缺失: %s" % str(e)
|
|
480
|
+
code_dir = os.path.join(_base, 'data', 'code')
|
|
481
|
+
try:
|
|
482
|
+
cn_map = get_cn_map_with_auto()
|
|
483
|
+
results = search_wiki(kw, str(_idx), code_dir, cn_map)
|
|
484
|
+
except Exception as e:
|
|
485
|
+
return "Wiki 搜索出错: %s" % str(e)
|
|
486
|
+
if not results:
|
|
487
|
+
return "No wiki pages match: %s" % kw
|
|
488
|
+
out = ['Wiki pages matching: %s (%d found)' % (kw, len(results)), '']
|
|
489
|
+
for i, r in enumerate(results, 1):
|
|
490
|
+
out.append(' %d. [%s] %s' % (i, r.get('project', '?'), r.get('title', '')))
|
|
491
|
+
deps = r.get('dependent_files') or []
|
|
492
|
+
if deps:
|
|
493
|
+
out.append(' 依赖: %s%s' % (', '.join(deps[:2]),
|
|
494
|
+
(' (共 %d 个文件)' % len(deps)) if len(deps) > 2 else ''))
|
|
495
|
+
if r.get('md_path'):
|
|
496
|
+
try:
|
|
497
|
+
rel = os.path.relpath(r['md_path'], _base)
|
|
498
|
+
except ValueError:
|
|
499
|
+
rel = r['md_path']
|
|
500
|
+
out.append(' 文档: %s' % rel)
|
|
501
|
+
out.append('')
|
|
502
|
+
out.append(' 提示: 读 wiki 文档(带行号引用)比读源码更省 token')
|
|
503
|
+
return '\n'.join(out)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def cmd_fill_prototype(args):
|
|
507
|
+
"""原型预填: 用真实代码数据自动填模板,输出 80% 完成的 HTML 草稿。"""
|
|
508
|
+
if not args:
|
|
509
|
+
return "用法: kg.py fill-prototype <关键词> [--platform web|app]"
|
|
510
|
+
kw, plat = _split_kw_platform(args)
|
|
511
|
+
plat = plat or 'web'
|
|
512
|
+
import subprocess as _sp
|
|
513
|
+
script = os.path.join(SCRIPTS_DIR, 'fill_prototype.py')
|
|
514
|
+
if not os.path.isfile(script):
|
|
515
|
+
return "fill_prototype.py 不存在"
|
|
516
|
+
try:
|
|
517
|
+
r = _sp.run([sys.executable, script, kw, '--platform', plat],
|
|
518
|
+
capture_output=True, text=True, encoding='utf-8',
|
|
519
|
+
errors='replace', timeout=30, cwd=SCRIPTS_DIR)
|
|
520
|
+
return r.stdout or "fill_prototype 无输出(可能索引缺失)"
|
|
521
|
+
except Exception as e:
|
|
522
|
+
return "[error] fill_prototype: %s" % str(e)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def cmd_design_system(args):
|
|
526
|
+
"""设计系统查询: 返回项目完整设计规范(布局指纹/颜色token/组件表/图标)。"""
|
|
527
|
+
plat = ''
|
|
528
|
+
if args and args[0] in ('--platform', '-p') and len(args) > 1:
|
|
529
|
+
plat = args[1]
|
|
530
|
+
import subprocess as _sp
|
|
531
|
+
design_md = os.path.join(BASE_DIR, 'data', 'design', 'DESIGN.md')
|
|
532
|
+
if not os.path.isfile(design_md):
|
|
533
|
+
gen_script = os.path.join(SCRIPTS_DIR, 'gen_design_doc.py')
|
|
534
|
+
if os.path.isfile(gen_script):
|
|
535
|
+
_sp.run([sys.executable, gen_script], capture_output=True, timeout=30)
|
|
536
|
+
if os.path.isfile(design_md):
|
|
537
|
+
content = open(design_md, encoding='utf-8').read()
|
|
538
|
+
if plat:
|
|
539
|
+
lines = content.split('\n')
|
|
540
|
+
result = []
|
|
541
|
+
in_section = False
|
|
542
|
+
for line in lines:
|
|
543
|
+
if line.startswith('### ') and plat.lower() in line.lower():
|
|
544
|
+
in_section = True
|
|
545
|
+
elif line.startswith('### ') and in_section:
|
|
546
|
+
break
|
|
547
|
+
if in_section:
|
|
548
|
+
result.append(line)
|
|
549
|
+
return '\n'.join(result) if result else content
|
|
550
|
+
return content
|
|
551
|
+
return "DESIGN.md 不存在且生成失败。请手动跑: python .qoder/scripts/gen_design_doc.py"
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# ── 工具函数 ──────────────────────────────────────────────────────────
|
|
555
|
+
|
|
556
|
+
def _capture(func, *args, **kwargs):
|
|
557
|
+
"""运行函数,捕获其 stdout 输出(底层脚本用 print 输出)。"""
|
|
558
|
+
old = sys.stdout
|
|
559
|
+
buf = io.StringIO()
|
|
560
|
+
sys.stdout = buf
|
|
561
|
+
try:
|
|
562
|
+
func(*args, **kwargs)
|
|
563
|
+
except Exception as e:
|
|
564
|
+
buf.write('\n[error] %s' % str(e))
|
|
565
|
+
finally:
|
|
566
|
+
sys.stdout = old
|
|
567
|
+
return buf.getvalue().strip()
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def _split_kw_platform(args):
|
|
571
|
+
"""从参数里拆出 keyword 和 --platform 值。"""
|
|
572
|
+
rest = list(args)
|
|
573
|
+
plat = ''
|
|
574
|
+
out = []
|
|
575
|
+
i = 0
|
|
576
|
+
while i < len(rest):
|
|
577
|
+
a = rest[i]
|
|
578
|
+
if a == '--platform' and i + 1 < len(rest):
|
|
579
|
+
plat = rest[i + 1]; i += 2; continue
|
|
580
|
+
out.append(a); i += 1
|
|
581
|
+
kw = out[0] if out else ''
|
|
582
|
+
return kw, plat
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _split_symbol_depth(args):
|
|
586
|
+
"""从参数里拆出 symbol 和 --depth 值(默认 3)。"""
|
|
587
|
+
rest = list(args)
|
|
588
|
+
depth = 3
|
|
589
|
+
out = []
|
|
590
|
+
i = 0
|
|
591
|
+
while i < len(rest):
|
|
592
|
+
a = rest[i]
|
|
593
|
+
if a == '--depth' and i + 1 < len(rest):
|
|
594
|
+
try: depth = int(rest[i + 1])
|
|
595
|
+
except (ValueError, TypeError): depth = 3
|
|
596
|
+
i += 2; continue
|
|
597
|
+
out.append(a); i += 1
|
|
598
|
+
return (out[0] if out else ''), max(1, min(5, depth))
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# Role-based section filtering for context_pack output(与 MCP 一致)
|
|
602
|
+
ROLE_SECTIONS = {
|
|
603
|
+
'pm': {'1', '2', '2b', '3', '4', '5', '7'}, # PM: 字段+PRD+API,无代码噪音
|
|
604
|
+
'design': {'2', '2b', '3', '6'}, # 设计: 页面+布局+字段+风格
|
|
605
|
+
'dev': {'1', '3', '5', '7'}, # 开发: 代码+字段+API+wiki
|
|
606
|
+
'test': {'1', '4', '5'}, # 测试: 代码+PRD+API
|
|
607
|
+
'admin': {'4', '5'}, # 管理: PRD+API 概览
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
def _filter_by_role(text, role):
|
|
612
|
+
"""按角色过滤 context_pack 输出的章节。"""
|
|
613
|
+
visible = ROLE_SECTIONS.get((role or '').lower())
|
|
614
|
+
if not visible:
|
|
615
|
+
return text # 未知角色不过滤
|
|
616
|
+
sections = re.split(r'(\n## \d+[ab]?\.)', text)
|
|
617
|
+
result = sections[0]
|
|
618
|
+
i = 1
|
|
619
|
+
while i < len(sections):
|
|
620
|
+
header = sections[i]
|
|
621
|
+
num_m = re.match(r'\n## (\d+[ab]?)\.', header)
|
|
622
|
+
if num_m and num_m.group(1) in visible:
|
|
623
|
+
result += header
|
|
624
|
+
if i + 1 < len(sections):
|
|
625
|
+
result += sections[i + 1]
|
|
626
|
+
i += 2
|
|
627
|
+
else:
|
|
628
|
+
i += 2
|
|
629
|
+
return result.strip()
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
# ── 子命令注册表 ──────────────────────────────────────────────────────
|
|
633
|
+
# (子命令名, 处理函数, 中文说明)
|
|
634
|
+
COMMANDS = [
|
|
635
|
+
('search', cmd_search, '搜代码: 按关键词找代码文件'),
|
|
636
|
+
('api', cmd_api, '搜 API 端点'),
|
|
637
|
+
('prd', cmd_prd, '搜 PRD(防重复造轮子)'),
|
|
638
|
+
('impact', cmd_impact, '影响分析: 改某接口影响哪些页面/按钮'),
|
|
639
|
+
('context360', cmd_context360, '符号 360 度视图(端点/函数/处理器)'),
|
|
640
|
+
('context', cmd_context, '上下文打包(代码+页面+字段+PRD+API,可 --role 角色裁剪)'),
|
|
641
|
+
('coverage', cmd_coverage, '功能×测试覆盖矩阵(标出回归盲区)'),
|
|
642
|
+
('feature', cmd_feature, '功能画像(端点+按钮+测试+页面)'),
|
|
643
|
+
('workflow', cmd_workflow, '业务流程链(query→create→audit→...)'),
|
|
644
|
+
('hop', cmd_hop, '多跳遍历(知识图谱核心,从一符号递归 N 跳)'),
|
|
645
|
+
('wiki', cmd_wiki, 'Repo Wiki 搜索(模块语义文档)'),
|
|
646
|
+
('fill-prototype', cmd_fill_prototype, '原型预填(真实数据填模板,输出 80% HTML)'),
|
|
647
|
+
('design-system', cmd_design_system, '设计系统查询(布局指纹/颜色token/组件表)'),
|
|
648
|
+
]
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _usage():
|
|
652
|
+
"""打印用法清单。"""
|
|
653
|
+
lines = [
|
|
654
|
+
'kg.py — QODER 知识图谱统一 CLI(全系列通用)',
|
|
655
|
+
'',
|
|
656
|
+
'用法: python .qoder/scripts/kg.py <子命令> [参数]',
|
|
657
|
+
'',
|
|
658
|
+
'13 个子命令:',
|
|
659
|
+
]
|
|
660
|
+
for name, _, desc in COMMANDS:
|
|
661
|
+
lines.append(' %-16s %s' % (name, desc))
|
|
662
|
+
lines += [
|
|
663
|
+
'',
|
|
664
|
+
'示例:',
|
|
665
|
+
' kg.py search 考勤 --platform web',
|
|
666
|
+
' kg.py impact /asset # 改 /asset 影响谁',
|
|
667
|
+
' kg.py coverage # 哪些功能没测试',
|
|
668
|
+
' kg.py feature 资产管理 # 资产管理功能画像',
|
|
669
|
+
' kg.py workflow assets # 资产模块业务流程',
|
|
670
|
+
' kg.py hop 资产管理 --depth 3 # 从资产管理出发 3 跳遍历',
|
|
671
|
+
' kg.py context 车辆 --role pm # PM 视角的上下文打包',
|
|
672
|
+
'',
|
|
673
|
+
'QoderWork 环境: AI 也会直接调 MCP 工具(mcp__qoder-knowledge-graph__*),功能等价。',
|
|
674
|
+
]
|
|
675
|
+
return '\n'.join(lines)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def main(argv=None):
|
|
679
|
+
argv = argv if argv is not None else sys.argv[1:]
|
|
680
|
+
if not argv or argv[0] in ('-h', '--help', 'help'):
|
|
681
|
+
print(_usage())
|
|
682
|
+
return 0
|
|
683
|
+
cmd = argv[0]
|
|
684
|
+
rest = argv[1:]
|
|
685
|
+
# 查找子命令
|
|
686
|
+
handler = None
|
|
687
|
+
for name, fn, _ in COMMANDS:
|
|
688
|
+
if name == cmd:
|
|
689
|
+
handler = fn
|
|
690
|
+
break
|
|
691
|
+
if handler is None:
|
|
692
|
+
print("未知子命令: %s" % cmd)
|
|
693
|
+
print(_usage())
|
|
694
|
+
return 1
|
|
695
|
+
# 执行(全容错,绝不抛栈)
|
|
696
|
+
try:
|
|
697
|
+
result = handler(rest)
|
|
698
|
+
if result:
|
|
699
|
+
print(result)
|
|
700
|
+
except Exception as e:
|
|
701
|
+
print("[error] %s 执行失败: %s" % (cmd, str(e)))
|
|
702
|
+
print("如果是索引/图谱问题,尝试: python .qoder/scripts/init_doctor.py --fix")
|
|
703
|
+
return 1
|
|
704
|
+
return 0
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
if __name__ == '__main__':
|
|
708
|
+
sys.exit(main())
|