@hupan56/wlkj 2.7.12 → 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.
Files changed (152) hide show
  1. package/bin/cli.js +344 -78
  2. package/package.json +29 -29
  3. package/templates/.qoder/.runtime/ctx-cache-5660152f1d6dd819.md +23 -0
  4. package/templates/.qoder/.runtime/ctx-cache-afdce0dac06b25b0.md +23 -0
  5. package/templates/.qoder/.runtime/search-cache-eae7644e7b122f35.txt +1 -0
  6. package/templates/.qoder/learning/eval-history.jsonl +28 -0
  7. package/templates/data/index/wiki-index.json +8 -0
  8. package/templates/qoder/agents/insight-planning.md +1 -1
  9. package/templates/qoder/agents/insight-research.md +28 -15
  10. package/templates/qoder/commands/optional/wl-insight.md +159 -161
  11. package/templates/qoder/commands/optional/wl-report.md +4 -4
  12. package/templates/qoder/commands/optional/wl-status.md +2 -2
  13. package/templates/qoder/commands/wl-code.md +2 -2
  14. package/templates/qoder/commands/wl-design.md +2 -2
  15. package/templates/qoder/commands/wl-init.md +3 -3
  16. package/templates/qoder/commands/wl-prd.md +2 -2
  17. package/templates/qoder/commands/wl-req.md +43 -0
  18. package/templates/qoder/commands/wl-search.md +8 -8
  19. package/templates/qoder/commands/wl-task.md +17 -17
  20. package/templates/qoder/commands/wl-test.md +41 -15
  21. package/templates/qoder/config.yaml +17 -1
  22. package/templates/qoder/hooks/session-start.py +12 -22
  23. package/templates/qoder/nul +4 -0
  24. package/templates/qoder/rules/wl-pipeline.md +22 -48
  25. package/templates/qoder/scripts/README.md +139 -0
  26. package/templates/qoder/scripts/common/autotest_auth.py +109 -0
  27. package/templates/qoder/scripts/common/bootstrap.py +145 -0
  28. package/templates/qoder/scripts/common/check_publish.py +98 -0
  29. package/templates/qoder/scripts/common/cmd_registry.py +112 -0
  30. package/templates/qoder/scripts/common/config.py +187 -0
  31. package/templates/qoder/scripts/common/contract.py +317 -0
  32. package/templates/qoder/scripts/common/developer.py +2 -1
  33. package/templates/qoder/scripts/common/feishu.py +10 -9
  34. package/templates/qoder/scripts/common/guard.py +159 -0
  35. package/templates/qoder/scripts/common/identity.py +121 -2
  36. package/templates/qoder/scripts/common/kg_capabilities.py +182 -0
  37. package/templates/qoder/scripts/common/mcp_base.py +268 -0
  38. package/templates/qoder/scripts/common/paths.py +187 -1
  39. package/templates/qoder/scripts/common/result.py +223 -0
  40. package/templates/qoder/scripts/common/roles.py +60 -0
  41. package/templates/qoder/scripts/common/task_utils.py +21 -9
  42. package/templates/qoder/scripts/common/test_extract.py +115 -0
  43. package/templates/qoder/scripts/kg/__init__.py +11 -0
  44. package/templates/qoder/scripts/kg/build_entity_registry.py +196 -0
  45. package/templates/qoder/scripts/kg/build_relations.py +127 -0
  46. package/templates/qoder/scripts/{build_style_index.py → kg/build_style_index.py} +39 -10
  47. package/templates/qoder/scripts/kg/build_workflows.py +144 -0
  48. package/templates/qoder/scripts/{context_pack.py → kg/context_pack.py} +18 -11
  49. package/templates/qoder/scripts/{enrich_prompt.py → kg/enrich_prompt.py} +232 -226
  50. package/templates/qoder/scripts/{extract_api_params.py → kg/extract.py} +398 -246
  51. package/templates/qoder/scripts/{kg.py → kg/kg.py} +638 -708
  52. package/templates/qoder/scripts/{kg_build.py → kg/kg_build.py} +618 -612
  53. package/templates/qoder/scripts/{kg_build_db.py → kg/kg_build_db.py} +333 -327
  54. package/templates/qoder/scripts/{kg_duckdb.py → kg/kg_duckdb.py} +38 -37
  55. package/templates/qoder/scripts/{kg_incremental.py → kg/kg_incremental.py} +420 -393
  56. package/templates/qoder/scripts/{kg_link_db.py → kg/kg_link_db.py} +230 -224
  57. package/templates/qoder/scripts/{kg_semantic.py → kg/kg_semantic.py} +156 -150
  58. package/templates/qoder/scripts/kg/prefetch.py +359 -0
  59. package/templates/qoder/scripts/{search_index.py → kg/search_index.py} +70 -14
  60. package/templates/qoder/scripts/mcp/__init__.py +11 -0
  61. package/templates/qoder/scripts/{kg_mcp_server.py → mcp/kg_mcp_server.py} +77 -272
  62. package/templates/qoder/scripts/{lanhu_stdio_wrapper.py → mcp/lanhu_stdio_wrapper.py} +125 -119
  63. package/templates/qoder/scripts/{check_mcp.py → mcp/mcp_doctor.py} +515 -298
  64. package/templates/qoder/scripts/{mcp_launcher.py → mcp/mcp_launcher.py} +442 -414
  65. package/templates/qoder/scripts/{mysql_mcp_server.py → mcp/mysql_mcp_server.py} +347 -396
  66. package/templates/qoder/scripts/{zentao_mcp_server.py → mcp/zentao_mcp_server.py} +384 -424
  67. package/templates/qoder/scripts/report/__init__.py +11 -0
  68. package/templates/qoder/scripts/{add_session.py → report/add_session.py} +250 -244
  69. package/templates/qoder/scripts/{archive_prd.py → report/archive_prd.py} +383 -377
  70. package/templates/qoder/scripts/{eval_prd.py → report/eval_prd.py} +73 -11
  71. package/templates/qoder/scripts/{export.py → report/export.py} +63 -0
  72. package/templates/qoder/scripts/{fill_prototype.py → report/fill_prototype.py} +6 -0
  73. package/templates/qoder/scripts/{gen_design_doc.py → report/gen_design_doc.py} +400 -394
  74. package/templates/qoder/scripts/{learn.py → report/learn.py} +152 -146
  75. package/templates/qoder/scripts/{learn_aggregate.py → report/learn_aggregate.py} +207 -201
  76. package/templates/qoder/scripts/{report.py → report/report.py} +287 -281
  77. package/templates/qoder/scripts/report/req.py +222 -0
  78. package/templates/qoder/scripts/report/role.py +33 -0
  79. package/templates/qoder/scripts/{status.py → report/status.py} +634 -628
  80. package/templates/qoder/scripts/setup/__init__.py +11 -0
  81. package/templates/qoder/scripts/setup/carriers.py +662 -0
  82. package/templates/qoder/scripts/{init_doctor.py → setup/init_doctor.py} +63 -26
  83. package/templates/qoder/scripts/{install_qoderwork.py → setup/install_qoderwork.py} +36 -26
  84. package/templates/qoder/scripts/{platform_doctor.py → setup/platform_doctor.py} +265 -259
  85. package/templates/qoder/scripts/{repo_root.py → setup/repo_root.py} +112 -106
  86. package/templates/qoder/scripts/{setup.py → setup/setup.py} +113 -4
  87. package/templates/qoder/scripts/{setup_lanhu.py → setup/setup_lanhu.py} +973 -963
  88. package/templates/qoder/scripts/task/__init__.py +11 -0
  89. package/templates/qoder/scripts/{git_sync.py → task/git_sync.py} +52 -31
  90. package/templates/qoder/scripts/{syncgate.py → task/syncgate.py} +6 -0
  91. package/templates/qoder/scripts/task/task.py +221 -0
  92. package/templates/qoder/scripts/task/task_lifecycle.py +596 -0
  93. package/templates/qoder/scripts/task/task_query.py +161 -0
  94. package/templates/qoder/scripts/task/task_relations.py +424 -0
  95. package/templates/qoder/scripts/{team_sync.py → task/team_sync.py} +93 -20
  96. package/templates/qoder/scripts/test/__init__.py +11 -0
  97. package/templates/qoder/scripts/{autotest.py → test/autotest.py} +1174 -1751
  98. package/templates/qoder/scripts/{autotest_batch.py → test/autotest_batch.py} +242 -224
  99. package/templates/qoder/scripts/test/autotest_data.py +675 -0
  100. package/templates/qoder/scripts/{autotest_run.py → test/autotest_run.py} +309 -297
  101. package/templates/qoder/scripts/{benchmark.py → test/benchmark.py} +6 -0
  102. package/templates/qoder/scripts/{kg_auto_login.py → test/kg_auto_login.py} +202 -196
  103. package/templates/qoder/scripts/{kg_test_runner.py → test/kg_test_runner.py} +7 -1
  104. package/templates/qoder/scripts/{page_probe.py → test/page_probe.py} +465 -459
  105. package/templates/qoder/scripts/wlkj.py +116 -0
  106. package/templates/qoder/settings.json +1 -10
  107. package/templates/qoder/skills/design-import/SKILL.md +226 -226
  108. package/templates/qoder/skills/design-review/SKILL.md +82 -82
  109. package/templates/qoder/skills/prd-generator/SKILL.md +26 -16
  110. package/templates/qoder/skills/prd-review/SKILL.md +5 -5
  111. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -256
  112. package/templates/qoder/skills/spec-coder/SKILL.md +4 -4
  113. package/templates/qoder/skills/spec-generator/SKILL.md +4 -4
  114. package/templates/qoder/skills/test-generator/SKILL.md +5 -5
  115. package/templates/qoder/skills/wl-code/SKILL.md +4 -4
  116. package/templates/qoder/skills/wl-commit/SKILL.md +4 -4
  117. package/templates/qoder/skills/wl-design/SKILL.md +3 -3
  118. package/templates/qoder/skills/wl-init/SKILL.md +8 -8
  119. package/templates/qoder/skills/wl-insight/SKILL.md +5 -5
  120. package/templates/qoder/skills/wl-prd-full/SKILL.md +6 -6
  121. package/templates/qoder/skills/wl-prd-quick/SKILL.md +6 -6
  122. package/templates/qoder/skills/wl-prd-review/SKILL.md +4 -4
  123. package/templates/qoder/skills/wl-report/SKILL.md +7 -7
  124. package/templates/qoder/skills/wl-search/SKILL.md +13 -13
  125. package/templates/qoder/skills/wl-spec/SKILL.md +5 -5
  126. package/templates/qoder/skills/wl-status/SKILL.md +5 -5
  127. package/templates/qoder/skills/wl-task/SKILL.md +6 -6
  128. package/templates/qoder/skills/wl-test/SKILL.md +102 -39
  129. package/templates/root/AGENTS.md +39 -40
  130. package/templates/qoder/hooks/inject-workflow-state.py +0 -169
  131. package/templates/qoder/scripts/__pycache__/check_mcp_launch.cpython-39.pyc +0 -0
  132. package/templates/qoder/scripts/__pycache__/install_qoderwork.cpython-39.pyc +0 -0
  133. package/templates/qoder/scripts/__pycache__/mcp_launcher.cpython-39.pyc +0 -0
  134. package/templates/qoder/scripts/__pycache__/platform_doctor.cpython-39.pyc +0 -0
  135. package/templates/qoder/scripts/check_carriers.py +0 -238
  136. package/templates/qoder/scripts/check_mcp_launch.py +0 -183
  137. package/templates/qoder/scripts/check_qoderwork_consistency.py +0 -166
  138. package/templates/qoder/scripts/collect_prds.py +0 -31
  139. package/templates/qoder/scripts/common/mentions.py +0 -134
  140. package/templates/qoder/scripts/common/utf8.py +0 -38
  141. package/templates/qoder/scripts/extract_routes.py +0 -54
  142. package/templates/qoder/scripts/extract_routes_tree.py +0 -78
  143. package/templates/qoder/scripts/handoff.py +0 -22
  144. package/templates/qoder/scripts/init_developer.py +0 -76
  145. package/templates/qoder/scripts/parse_prds.py +0 -33
  146. package/templates/qoder/scripts/role.py +0 -51
  147. package/templates/qoder/scripts/sync_carriers.py +0 -259
  148. package/templates/qoder/scripts/task.py +0 -1261
  149. package/templates/qoder/scripts/workspace_init.py +0 -102
  150. package/templates/qoder/skills/prompt-enrich/SKILL.md +0 -90
  151. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  152. /package/templates/qoder/scripts/{secure-ls.js → test/secure-ls.js} +0 -0
@@ -0,0 +1,182 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ kg_capabilities.py - 知识图谱共享查询能力 (Strategy 模式, 消除 CLI↔MCP 重复)
4
+
5
+ v3.0 f3: kg.py (CLI) 和 kg_mcp_server.py (MCP) 有近字重复的查询逻辑
6
+ (workflow/hop/wiki/design_system 等)。本模块提取共享查询, 两边都调它,
7
+ 消除重复, 且保证 CLI 和 MCP 行为完全一致。
8
+
9
+ 核心 API (都接收已打开的 duckdb con, 由调用方管理连接):
10
+ workflow_query(con, q) -> str 业务流程链查询
11
+ # hop_query / wiki_query / design_system_query 后续添加
12
+
13
+ 设计: 查询逻辑 (数据清洗/SQL/格式化) 在此单一真源;
14
+ kg.py 和 kg_mcp_server 只负责"开连接 + 调本模块 + 返回"。
15
+ """
16
+
17
+ __all__ = ["workflow_query", "hop_query", "wiki_query"]
18
+
19
+
20
+ def workflow_query(con, q):
21
+ """workflows 表查询 + 脏数据清洗 + 中文兜底。
22
+
23
+ Args:
24
+ con: 已打开的 duckdb 连接 (read_only)
25
+ q: 模块名 (英文 assets/cost/hr 或中文触发兜底)
26
+
27
+ Returns:
28
+ 格式化的业务流程链文本
29
+ """
30
+ q = (q or '').strip()
31
+ # 1) 精确 + 模糊匹配 workflows.module(英文)
32
+ hits = con.execute(
33
+ "SELECT DISTINCT module FROM workflows WHERE module = ? OR module LIKE '%' || ? || '%' ORDER BY module",
34
+ [q.lower(), q.lower()]
35
+ ).fetchall()
36
+ if not hits:
37
+ # 2) 中文兜底: 查 entities.cn 返回英文候选
38
+ cn_hits = con.execute(
39
+ "SELECT canonical, cn FROM entities WHERE type IN ('FEATURE','MODULE') AND cn <> '' AND (cn LIKE '%' || ? OR cn LIKE '%' || ? || '%')",
40
+ [q, q]
41
+ ).fetchall()
42
+ if cn_hits:
43
+ cands = ', '.join('%s(%s)' % (c[0], c[1]) for c in cn_hits[:8])
44
+ return "未在 workflows 表找到模块 '%s'。但 entities 表有相近中文名,可换这些英文模块名重试:\n %s" % (q, cands)
45
+ all_mods = [r[0] for r in con.execute("SELECT DISTINCT module FROM workflows ORDER BY module").fetchall()]
46
+ return "未找到模块 '%s'。现有模块: %s" % (q, ', '.join(all_mods))
47
+
48
+ out = []
49
+ for (mod,) in hits:
50
+ # 清洗: 过滤空端点和模板泄漏按钮
51
+ rows = con.execute("""
52
+ SELECT state, seq, button, endpoint FROM workflows
53
+ WHERE module = ?
54
+ AND endpoint <> 'None' AND endpoint <> ''
55
+ AND button NOT LIKE '{{%'
56
+ ORDER BY seq, state
57
+ """, [mod]).fetchall()
58
+ if not rows:
59
+ continue
60
+ out.append('=== %s (%d 步, 已清洗脏数据) ===' % (mod, len(rows)))
61
+ cur_seq = -1
62
+ for st, sq, btn, ep in rows:
63
+ if sq != cur_seq:
64
+ out.append(' [%d] %s' % (sq, st))
65
+ cur_seq = sq
66
+ out.append(' %-16s -> %s' % (btn, ep))
67
+ out.append('')
68
+ return '\n'.join(out).strip() or "模块 '%s' 清洗后无有效操作链" % q
69
+
70
+
71
+ def hop_query(con, symbol, depth, multi_hop_fn=None):
72
+ """多跳遍历: 别名解析(三级兜底) + 递归CTE + 按depth分组展示。
73
+
74
+ Args:
75
+ con: duckdb 连接
76
+ symbol: 起始符号 (中文/端点/函数名, 自动别名解析)
77
+ depth: 最大跳数 1-5
78
+ multi_hop_fn: kg_duckdb.multi_hop 函数 (避免硬依赖)
79
+ Returns:
80
+ 格式化的多跳遍历文本
81
+ """
82
+ # 1) 别名解析(三级兜底)
83
+ start_id = symbol
84
+ resolved = con.execute(
85
+ "SELECT entity_id FROM aliases WHERE alias = ?", [symbol.lower()]
86
+ ).fetchone()
87
+ if resolved:
88
+ start_id = resolved[0]
89
+ else:
90
+ fuzzy = con.execute(
91
+ "SELECT id FROM entities WHERE id = ? OR id LIKE '%' || ? || '%' LIMIT 1",
92
+ [symbol, symbol]
93
+ ).fetchone()
94
+ if fuzzy:
95
+ start_id = fuzzy[0]
96
+ else:
97
+ edge_hit = con.execute(
98
+ "SELECT from_id FROM edges WHERE from_id LIKE '%' || ? || '%' LIMIT 1",
99
+ [symbol]
100
+ ).fetchone()
101
+ if edge_hit:
102
+ start_id = edge_hit[0]
103
+
104
+ # 2) 递归 CTE 遍历
105
+ if multi_hop_fn is None:
106
+ import kg_duckdb as K
107
+ multi_hop_fn = K.multi_hop
108
+ try:
109
+ hops = multi_hop_fn(con, start_id, max_depth=depth)
110
+ except Exception as e:
111
+ return "遍历出错 (起点=%s): %s" % (start_id, str(e))
112
+
113
+ if not hops or (len(hops) == 1 and hops[0][1] == 0):
114
+ return "从 '%s' (解析为 %s) 出发无可达节点。可能该符号不在图谱中,或无出边。" % (symbol, start_id)
115
+
116
+ # 3) 按 depth 分组展示
117
+ from collections import defaultdict
118
+ by_depth = defaultdict(list)
119
+ for eid, d in hops:
120
+ by_depth[d].append(eid)
121
+
122
+ out = ['=== 多跳遍历: %s (解析为 %s) ===' % (symbol, start_id)]
123
+ out.append('可达 %d 个实体, 最大 %d 跳:' % (len(hops), max(by_depth.keys())))
124
+ out.append('')
125
+ for d in sorted(by_depth.keys()):
126
+ eids = by_depth[d]
127
+ out.append('[第 %d 跳] %d 个实体:' % (d, len(eids)))
128
+ shown = 0
129
+ for eid in eids:
130
+ if shown >= 12:
131
+ out.append(' ... 还有 %d 个' % (len(eids) - 12))
132
+ break
133
+ info = con.execute(
134
+ "SELECT type, canonical, cn FROM entities WHERE id = ?", [eid]
135
+ ).fetchone()
136
+ if info:
137
+ t, c, cn = info
138
+ label = '%s / %s' % (c, cn) if cn and cn != c else c
139
+ out.append(' %-14s %-12s %s' % ('[%s]' % t, eid[:40], label))
140
+ else:
141
+ out.append(' %-14s %s' % ('[edge]', eid[:60]))
142
+ shown += 1
143
+ out.append('')
144
+ return '\n'.join(out).strip()
145
+
146
+ def wiki_query(kw):
147
+ """Repo Wiki 搜索 + 格式化 (CLI/MCP 共用)。
148
+
149
+ Args:
150
+ kw: 搜索关键词
151
+ Returns:
152
+ 格式化的 wiki 匹配结果文本
153
+ """
154
+ import os
155
+ from common.paths import DATA_INDEX_DIR as _idx, PROJECT_ROOT as _base
156
+ wiki_path = os.path.join(str(_idx), 'wiki-index.json')
157
+ if not os.path.isfile(wiki_path):
158
+ return ("wiki-index.json 不存在。请手动构建(一次性):\n"
159
+ " python .qoder/scripts/search_index.py wiki")
160
+ try:
161
+ from common.repowiki import search_wiki
162
+ from common.terms import get_cn_map_with_auto
163
+ except ImportError as e:
164
+ return "Wiki 搜索模块缺失: %s" % str(e)
165
+ code_dir = os.path.join(_base, 'data', 'code')
166
+ try:
167
+ cn_map = get_cn_map_with_auto()
168
+ results = search_wiki(kw, str(_idx), code_dir, cn_map)
169
+ except Exception as e:
170
+ return "Wiki 搜索出错: %s" % str(e)
171
+ if not results:
172
+ return "No wiki pages match: %s" % kw
173
+ out = ['Wiki pages matching: %s (%d found)' % (kw, len(results)), '']
174
+ for i, r in enumerate(results, 1):
175
+ out.append(' %d. [%s] %s' % (i, r.get('project', '?'), r.get('title', '')))
176
+ deps = r.get('dependent_files') or []
177
+ if deps:
178
+ out.append(' 依赖: %s%s' % (', '.join(deps[:2]),
179
+ (' (共 %d 个文件)' % len(deps)) if len(deps) > 2 else ''))
180
+ if r.get('md_path'):
181
+ out.append(' 文档: %s' % r['md_path'])
182
+ return '\n'.join(out)
@@ -0,0 +1,268 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ mcp_base.py - MCP Server 基类 (Template Method 模式)
4
+
5
+ 治"3个MCP server协议层逐行复制~255行"。三者(kg/mysql/zentao)的:
6
+ - _send (JSON-RPC写)
7
+ - main (readline循环 + 每请求新event_loop)
8
+ - _handle 五分支骨架 (initialize/notifications/initialized/tools·list/tools·call/ping/error)
9
+ - stdout重定向 (stdout→stderr, 避免污染stdio协议)
10
+ 全部逐字一致, 仅差异点:
11
+ - serverInfo.name/version
12
+ - initialize 是否调 check_intranet 塞 warning
13
+ - tools/call 的异常分类 (mysql 多 ValueError→只读拒绝)
14
+ - error 分支文案
15
+
16
+ 本基类把骨架固化为 final 方法, 子类填钩子:
17
+ - SERVER_NAME / SERVER_VERSION: 类属性
18
+ - TOOLS: 工具元数据列表
19
+ - handle_tool(name, args) -> str: 工具执行 (Strategy 钩子)
20
+ - on_initialize_hook(info) -> None: 塞 warning 等 (默认空)
21
+ - on_tool_error(e) -> (text, is_error): 自定义异常分类 (默认通用)
22
+
23
+ Usage (子类示例):
24
+ from common.mcp_base import BaseMCPServer
25
+
26
+ class ZentaoMCPServer(BaseMCPServer):
27
+ SERVER_NAME = "qoder-zentao"
28
+ SERVER_VERSION = "1.0.0"
29
+ TOOLS = [...] # 工具元数据
30
+ def handle_tool(self, name, args):
31
+ if name == 'list_stories': return self._list_stories(args)
32
+ return '未知工具: %s' % name
33
+ def on_initialize_hook(self, info):
34
+ ok, msg = check_intranet()
35
+ if not ok: info['warning'] = msg
36
+
37
+ if __name__ == '__main__':
38
+ ZentaoMCPServer().run()
39
+
40
+ 收益: 加第4个MCP server从复制~255行降到写~30行(只填tools+handle_tool)。
41
+
42
+ v3.0+ 增强: ToolRegistry + @tool 装饰器 (Command Registry 模式)。
43
+ 消除子类 _execute_tool 的长 if-elif 链 + TOOLS 元数据两处维护的漂移。
44
+ 用法见底部示例。
45
+ """
46
+
47
+ import asyncio
48
+ import json
49
+ import os
50
+ import sys
51
+ from typing import Any, Dict, List, Optional, Tuple
52
+
53
+ __all__ = ["BaseMCPServer", "tool", "ToolRegistry"]
54
+
55
+
56
+ # ============================================================
57
+ # ToolRegistry: @tool 装饰器收集工具定义 (Command Registry 模式)
58
+ # ============================================================
59
+
60
+ def tool(name: str, description: str = "", input_schema: Optional[Dict] = None):
61
+ """装饰器: 把一个方法注册为 MCP 工具。
62
+
63
+ 替代旧式的 "TOOLS 列表 + _execute_tool if 链 两处维护"。
64
+ 装饰后, BaseMCPServer.TOOLS 自动收集, handle_tool 自动分发。
65
+
66
+ Usage (子类内):
67
+ @tool("search_code", "搜代码", {"type":"object","properties":{...}})
68
+ def search_code(self, args):
69
+ return "结果文本"
70
+ """
71
+ def decorator(method):
72
+ method._tool_meta = {
73
+ "name": name,
74
+ "description": description or method.__doc__ or "",
75
+ "inputSchema": input_schema or {"type": "object", "properties": {}},
76
+ }
77
+ return method
78
+ return decorator
79
+
80
+
81
+ class _ToolRegistryMeta(type):
82
+ """元类: 子类定义完成后, 扫描 @tool 装饰的方法, 自动填充 TOOLS + _tool_map。
83
+
84
+ 这样子类只需用 @tool 装饰, 不用再手写 TOOLS 列表和 handle_tool if 链。
85
+ """
86
+ def __new__(mcs, name, bases, namespace):
87
+ cls = super().__new__(mcs, name, bases, namespace)
88
+ # 收集本类 + 父类的 @tool 方法
89
+ tool_map = {}
90
+ # 先继承父类的
91
+ for base in bases:
92
+ if hasattr(base, "_tool_map"):
93
+ tool_map.update(base._tool_map)
94
+ # 再收集本类的
95
+ for attr_name, attr_val in namespace.items():
96
+ meta = getattr(attr_val, "_tool_meta", None)
97
+ if meta:
98
+ tool_map[meta["name"]] = {
99
+ "meta": meta,
100
+ "handler": attr_name, # 方法名, 运行时 getattr 取
101
+ }
102
+ cls._tool_map = tool_map
103
+ # 如果子类没显式定义 TOOLS (用装饰器模式), 自动生成
104
+ if tool_map and not namespace.get("TOOLS"):
105
+ cls.TOOLS = [v["meta"] for v in tool_map.values()]
106
+ return cls
107
+
108
+
109
+ class BaseMCPServer(metaclass=_ToolRegistryMeta):
110
+ """MCP stdio server 基类 (Template Method + ToolRegistry)。
111
+
112
+ 两种用法 (二选一):
113
+ A) 装饰器模式 (推荐, 消除 if 链):
114
+ class MyServer(BaseMCPServer):
115
+ SERVER_NAME = "my"
116
+ @tool("foo", "做foo", {...})
117
+ def foo(self, args): return "结果"
118
+ # TOOLS 自动收集, handle_tool 自动分发, 无需覆写
119
+ B) 传统模式 (兼容旧代码):
120
+ class MyServer(BaseMCPServer):
121
+ SERVER_NAME = "my"
122
+ TOOLS = [...] # 手写
123
+ def handle_tool(self, name, args): ... # 手写 if 链
124
+ """
125
+
126
+ # ---- 子类必填类属性 ----
127
+ SERVER_NAME: str = "qoder-mcp"
128
+ SERVER_VERSION: str = "0.1.0"
129
+ TOOLS: List[Dict[str, Any]] = []
130
+ _tool_map: Dict[str, Dict] = {} # 由元类自动填充
131
+
132
+ # ---- 协议常量 (可覆写) ----
133
+ PROTOCOL_VERSION: str = "2024-11-05"
134
+
135
+ # ============================================================
136
+ # 内部: stdout 重定向 (MCP 用 stdout 传 JSON-RPC, 不能被 print 污染)
137
+ # ============================================================
138
+ _real_stdout = None
139
+
140
+ @classmethod
141
+ def _setup_streams(cls):
142
+ """把 sys.stdout 存起来, 重定向 sys.stdout→stderr (子进程的 print 进 stderr)。"""
143
+ if cls._real_stdout is None:
144
+ cls._real_stdout = sys.stdout
145
+ sys.stdout = sys.stderr
146
+
147
+ # ============================================================
148
+ # final: 协议发送
149
+ # ============================================================
150
+ @classmethod
151
+ def _send(cls, msg: Dict[str, Any]) -> None:
152
+ """发送 JSON-RPC 消息到真实 stdout (跨3个server逐字一致)。"""
153
+ cls._real_stdout.write(json.dumps(msg, ensure_ascii=False) + "\n")
154
+ cls._real_stdout.flush()
155
+
156
+ # ============================================================
157
+ # 默认 handle_tool: 从 _tool_map 自动分发 (装饰器模式)
158
+ # ============================================================
159
+ def handle_tool(self, name: str, args: Dict[str, Any]) -> str:
160
+ """[默认实现] 从 ToolRegistry 取 handler 执行。
161
+
162
+ - 若用了 @tool 装饰器: 自动分发, 子类不覆写
163
+ - 若传统模式 (子类覆写了 handle_tool): 走子类的 if 链
164
+ """
165
+ entry = self._tool_map.get(name)
166
+ if entry:
167
+ handler = getattr(self, entry["handler"])
168
+ return handler(args)
169
+ return "未知工具: %s" % name
170
+
171
+ def on_initialize_hook(self, info: Dict[str, Any]) -> None:
172
+ """[可选覆写] initialize 时往 serverInfo 塞额外信息 (如 warning)。
173
+
174
+ 默认空。mysql/zentao 在此调 check_intranet 塞 warning。
175
+ """
176
+ pass
177
+
178
+ def on_tool_error(self, e: Exception) -> Tuple[str, bool]:
179
+ """[可选覆写] tools/call 抛异常时的分类。
180
+
181
+ Returns:
182
+ (text, is_error): 返回给客户端的文本 + 是否标记为错误
183
+ 默认: 通用异常 → "[error] xxx" + is_error=True。
184
+ mysql 覆写此方法把 ValueError(只读拒绝)单独分类。
185
+ """
186
+ return ("[error] %s" % str(e)[:200], True)
187
+
188
+ # ============================================================
189
+ # final: 请求处理骨架 (Template Method)
190
+ # ============================================================
191
+ async def _handle(self, req: Dict[str, Any]) -> None:
192
+ """处理一个 JSON-RPC 请求 (五分支骨架, 子类不覆写)。"""
193
+ method = req.get("method", "")
194
+ req_id = req.get("id")
195
+ params = req.get("params", {}) or {}
196
+
197
+ if method == "initialize":
198
+ info = {"name": self.SERVER_NAME, "version": self.SERVER_VERSION}
199
+ self.on_initialize_hook(info) # 钩子: 子类塞 warning
200
+ self._send({
201
+ "jsonrpc": "2.0", "id": req_id,
202
+ "result": {
203
+ "protocolVersion": self.PROTOCOL_VERSION,
204
+ "capabilities": {"tools": {}},
205
+ "serverInfo": info,
206
+ },
207
+ })
208
+ elif method == "notifications/initialized":
209
+ pass
210
+ elif method == "tools/list":
211
+ self._send({"jsonrpc": "2.0", "id": req_id, "result": {"tools": self.TOOLS}})
212
+ elif method == "tools/call":
213
+ tool_name = params.get("name", "")
214
+ arguments = params.get("arguments", {}) or {}
215
+ try:
216
+ result_text = await asyncio.get_event_loop().run_in_executor(
217
+ None, self.handle_tool, tool_name, arguments
218
+ )
219
+ self._send({
220
+ "jsonrpc": "2.0", "id": req_id,
221
+ "result": {
222
+ "content": [{"type": "text", "text": result_text}],
223
+ "isError": False,
224
+ },
225
+ })
226
+ except Exception as e:
227
+ text, is_error = self.on_tool_error(e)
228
+ self._send({
229
+ "jsonrpc": "2.0", "id": req_id,
230
+ "result": {
231
+ "content": [{"type": "text", "text": text}],
232
+ "isError": is_error,
233
+ },
234
+ })
235
+ elif method == "ping":
236
+ self._send({"jsonrpc": "2.0", "id": req_id, "result": {}})
237
+ elif req_id is not None:
238
+ self._send({
239
+ "jsonrpc": "2.0", "id": req_id,
240
+ "error": {"code": -32601, "message": "Method not found: %s" % method},
241
+ })
242
+
243
+ # ============================================================
244
+ # final: 主循环
245
+ # ============================================================
246
+ def run(self) -> None:
247
+ """启动 stdio 主循环 (readline → 解析 → _handle)。子类不覆写。"""
248
+ self._setup_streams()
249
+ tag = "[%s]" % self.SERVER_NAME
250
+ while True:
251
+ try:
252
+ line = sys.stdin.buffer.readline()
253
+ except (EOFError, OSError):
254
+ break
255
+ if not line:
256
+ break
257
+ line = line.strip()
258
+ if not line:
259
+ continue
260
+ try:
261
+ req = json.loads(line.decode("utf-8"))
262
+ loop = asyncio.new_event_loop()
263
+ loop.run_until_complete(self._handle(req))
264
+ loop.close()
265
+ except json.JSONDecodeError:
266
+ pass
267
+ except Exception as e:
268
+ print("%s error: %s" % (tag, str(e)[:80]), file=sys.stderr)
@@ -17,7 +17,22 @@ from typing import Optional, Dict
17
17
 
18
18
  # Project root (auto-detect: find .qoder/ upward)
19
19
  def _find_repo_root() -> Path:
20
- """Find project root by searching for .qoder/ directory."""
20
+ """Find project root by searching for .qoder/ directory.
21
+
22
+ 优先看 cwd: AI/CLI 常在项目根调 `python .qoder/scripts/xxx.py`,
23
+ cwd 就是项目根。再用 __file__ 兜底 (脚本被 import 时 cwd 可能不对)。
24
+ """
25
+ cwd = Path.cwd()
26
+ if (cwd / ".qoder").is_dir():
27
+ return cwd
28
+ current = cwd
29
+ for _ in range(10):
30
+ if (current / ".qoder").is_dir():
31
+ return current
32
+ parent = current.parent
33
+ if parent == current:
34
+ break
35
+ current = parent
21
36
  current = Path(__file__).resolve().parent
22
37
  for _ in range(10):
23
38
  if (current / ".qoder").is_dir():
@@ -267,6 +282,177 @@ def get_developer_drafts_dir(developer_name: str) -> Path:
267
282
  return drafts_dir
268
283
 
269
284
 
285
+ # =============================================================================
286
+ # 私有凭证目录 (M3: 收拢散落的敏感文件到 .private/)
287
+ # =============================================================================
288
+ # 现状问题: member根目录散落5个凭证文件(.signing_key/.secrets/auth-state.json/
289
+ # autotest-data.yaml), 和产出目录混在一起, 没层次。
290
+ # 解法: 全收进 .private/ (个人私有, 全gitignore), 根目录只留结构化产出。
291
+ #
292
+ # 向后兼容: get_private_file() 先查 .private/新位置, 没有则回退老位置。
293
+
294
+ PRIVATE_DIR_NAME = ".private"
295
+
296
+
297
+ def get_private_dir(developer_name: str = None) -> Path:
298
+ """开发者的私有凭证目录 workspace/members/{dev}/.private/
299
+
300
+ 所有敏感数据(signing_key/secrets/auth-state/autotest-data)收进这。
301
+ 全 gitignore, 永不 push。
302
+ """
303
+ if developer_name is None:
304
+ developer_name = get_developer() or "unknown"
305
+ p = get_developer_dir(developer_name) / PRIVATE_DIR_NAME
306
+ p.mkdir(parents=True, exist_ok=True)
307
+ return p
308
+
309
+
310
+ def get_private_file(filename: str, developer_name: str = None) -> Path:
311
+ """取私有文件路径 (向后兼容: 先 .private/新位置, 没有回退老位置)。
312
+
313
+ Args:
314
+ filename: 文件名 (如 'signing_key', 'auth-state.json', 'autotest-data.yaml')
315
+ Returns:
316
+ .private/filename 路径 (如果老位置有但新位置没有, 返回老位置)
317
+ """
318
+ if developer_name is None:
319
+ developer_name = get_developer() or "unknown"
320
+ dev_dir = get_developer_dir(developer_name)
321
+ new_path = get_private_dir(developer_name) / filename
322
+ # 向后兼容: 老位置的文件名映射
323
+ old_name_map = {
324
+ "signing_key": ".signing_key", # 老的带点前缀
325
+ }
326
+ old_name = old_name_map.get(filename, filename)
327
+ old_path = dev_dir / old_name
328
+ # 新位置有 → 用新的; 否则老位置有 → 用老的(渐进迁移); 都没有 → 返回新位置(待创建)
329
+ if new_path.exists():
330
+ return new_path
331
+ if old_path.exists():
332
+ return old_path
333
+ return new_path # 默认新位置
334
+
335
+
336
+ def get_secrets_dir(developer_name: str = None) -> Path:
337
+ """开发者的 secrets 子目录 .private/secrets/ (蓝湖cookie等)。
338
+
339
+ 向后兼容: 老的 .secrets/ 有则用它, 否则用 .private/secrets/。
340
+ """
341
+ if developer_name is None:
342
+ developer_name = get_developer() or "unknown"
343
+ dev_dir = get_developer_dir(developer_name)
344
+ new_secrets = get_private_dir(developer_name) / "secrets"
345
+ old_secrets = dev_dir / ".secrets"
346
+ if old_secrets.exists() and not new_secrets.exists():
347
+ return old_secrets # 渐进迁移: 老的还在先用老的
348
+ new_secrets.mkdir(parents=True, exist_ok=True)
349
+ return new_secrets
350
+
351
+
352
+ # =============================================================================
353
+ # M3: 时间桶 + REQ-ID 容器 (workspace 二态重构)
354
+ # =============================================================================
355
+ # 理念: workspace = 个人草稿台. drafts(草稿,私有) / outputs(产出,进team_sync)
356
+ # 时间桶内建: drafts/2026/06/W4/REQ-ID/ (活区=本周桶, 历史=过去的桶, 无独立archive)
357
+ # 一个需求的所有产出(PRD/原型/任务/规格)聚合在 REQ-ID 容器里
358
+
359
+ import datetime as _dt
360
+
361
+
362
+ def get_time_bucket(when: Optional[_dt.date] = None) -> str:
363
+ """返回当前(或指定)日期的时间桶相对路径: YYYY/MM/Ww
364
+
365
+ 周定义: ISO 周 (周一为一周开始). W1-W53.
366
+ 例: 2026-06-23 (周二, 第26周) → '2026/06/W26'
367
+ """
368
+ d = when or _dt.date.today()
369
+ iso_year, iso_week, _ = d.isocalendar()
370
+ # 用 iso_year (跨年时可能 != d.year), 月份用 d.month (实际月份桶)
371
+ return "%d/%02d/W%d" % (d.year, d.month, iso_week)
372
+
373
+
374
+ def get_time_bucket_path(developer_name: str, area: str, when: Optional[_dt.date] = None) -> Path:
375
+ """某开发者某区(drafts/outputs)的某时间桶绝对路径。
376
+
377
+ Args:
378
+ developer_name: 开发者名
379
+ area: 'drafts' 或 'outputs'
380
+ when: 日期(默认今天), 决定哪个时间桶
381
+ Returns:
382
+ workspace/members/{dev}/{area}/{YYYY}/{MM}/{Ww}/
383
+ """
384
+ bucket = get_time_bucket(when)
385
+ p = get_developer_dir(developer_name) / area / bucket
386
+ p.mkdir(parents=True, exist_ok=True)
387
+ return p
388
+
389
+
390
+ def get_req_dir(req_id: str, developer_name: str = None, area: str = "drafts",
391
+ when: Optional[_dt.date] = None) -> Path:
392
+ """某需求的容器目录 (一个需求的所有产出聚合处)。
393
+
394
+ workspace/members/{dev}/{area}/{YYYY}/{MM}/{Ww}/{REQ-ID}/
395
+
396
+ Args:
397
+ req_id: 需求编号 (REQ-2026-001 或 REQ-2026-001-保险OCR)
398
+ developer_name: 开发者名(默认当前)
399
+ area: 'drafts' 或 'outputs'
400
+ when: 日期(默认今天)
401
+ """
402
+ if developer_name is None:
403
+ developer_name = get_developer() or "unknown"
404
+ # 规范化 req_id (只取 REQ-YYYY-NNN 部分, 去掉标题后缀)
405
+ import re
406
+ m = re.match(r"(REQ-\d{4}-\d{3,})", req_id)
407
+ clean = m.group(1) if m else req_id
408
+ bucket_path = get_time_bucket_path(developer_name, area, when)
409
+ req_path = bucket_path / clean
410
+ req_path.mkdir(parents=True, exist_ok=True)
411
+ return req_path
412
+
413
+
414
+ def get_outputs_dir(developer_name: str = None) -> Path:
415
+ """开发者的 outputs 区根目录 (产出, 进 team_sync)。"""
416
+ if developer_name is None:
417
+ developer_name = get_developer() or "unknown"
418
+ p = get_developer_dir(developer_name) / "outputs"
419
+ p.mkdir(parents=True, exist_ok=True)
420
+ return p
421
+
422
+
423
+ def find_req_dirs(req_id: str, developer_name: str = None,
424
+ repo_root: Optional[Path] = None) -> list:
425
+ """跨所有时间桶找某需求的所有容器 (drafts + outputs 都找)。
426
+
427
+ /wl-req show 的核心: 不管需求在哪个桶(drafts本周/outputs本周/历史桶),
428
+ 全找出来。
429
+
430
+ Returns:
431
+ [{area, path, bucket}, ...] 列表
432
+ """
433
+ if developer_name is None:
434
+ developer_name = get_developer(repo_root) or "unknown"
435
+ if repo_root is None:
436
+ repo_root = PROJECT_ROOT
437
+ import re
438
+ m = re.match(r"(REQ-\d{4}-\d{3,})", req_id)
439
+ clean = m.group(1) if m else req_id
440
+
441
+ dev_dir = MEMBERS_DIR / developer_name if (PROJECT_ROOT / "workspace" / "members").parent == PROJECT_ROOT / "workspace" else repo_root / "workspace" / "members" / developer_name
442
+ results = []
443
+ for area in ("drafts", "outputs"):
444
+ area_root = dev_dir / area
445
+ if not area_root.is_dir():
446
+ continue
447
+ # 递归找含 clean 的目录 (跨 YYYY/MM/Ww 桶)
448
+ for found in area_root.rglob(clean):
449
+ if found.is_dir():
450
+ # 桶 = found 相对 area_root 的父路径
451
+ rel = found.parent.relative_to(area_root)
452
+ results.append({"area": area, "path": found, "bucket": str(rel)})
453
+ return results
454
+
455
+
270
456
  def get_workspace_dir(repo_root: Optional[Path] = None) -> Optional[Path]:
271
457
  """Get developer workspace directory."""
272
458
  dev = get_developer(repo_root)