@hupan56/wlkj 2.5.0 → 2.7.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 (127) hide show
  1. package/bin/cli.js +289 -12
  2. package/package.json +1 -1
  3. package/templates/qoder/agents/insight-planning.md +67 -0
  4. package/templates/qoder/agents/insight-research.md +61 -0
  5. package/templates/qoder/agents/prd-quick.md +1 -0
  6. package/templates/qoder/agents/prd-reference.md +10 -2
  7. package/templates/qoder/commands/optional/wl-insight.md +275 -0
  8. package/templates/qoder/commands/{wl-report.md → optional/wl-report.md} +13 -5
  9. package/templates/qoder/commands/{wl-spec.md → optional/wl-spec.md} +1 -1
  10. package/templates/qoder/commands/{wl-status.md → optional/wl-status.md} +28 -2
  11. package/templates/qoder/commands/wl-code.md +10 -2
  12. package/templates/qoder/commands/wl-commit.md +1 -1
  13. package/templates/qoder/commands/wl-design-draw.md +78 -0
  14. package/templates/qoder/commands/wl-design-scan.md +108 -0
  15. package/templates/qoder/commands/wl-design-spec.md +154 -0
  16. package/templates/qoder/commands/wl-design.md +32 -0
  17. package/templates/qoder/commands/wl-init.md +24 -3
  18. package/templates/qoder/commands/wl-prd-full.md +226 -0
  19. package/templates/qoder/commands/wl-prd-quick.md +134 -0
  20. package/templates/qoder/commands/wl-prd-review.md +104 -0
  21. package/templates/qoder/commands/wl-prd.md +17 -288
  22. package/templates/qoder/commands/wl-search.md +66 -30
  23. package/templates/qoder/commands/wl-task.md +290 -59
  24. package/templates/qoder/commands/wl-test.md +92 -24
  25. package/templates/qoder/config.yaml +59 -15
  26. package/templates/qoder/hooks/inject-workflow-state.py +35 -9
  27. package/templates/qoder/hooks/session-start.py +144 -62
  28. package/templates/qoder/rules/wl-pipeline.md +216 -105
  29. package/templates/qoder/scripts/__pycache__/search_index.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/archive_prd.py +377 -0
  31. package/templates/qoder/scripts/autotest.py +1715 -0
  32. package/templates/qoder/scripts/autotest_batch.py +224 -0
  33. package/templates/qoder/scripts/autotest_run.py +297 -0
  34. package/templates/qoder/scripts/benchmark.py +210 -209
  35. package/templates/qoder/scripts/build_style_index.py +444 -4
  36. package/templates/qoder/scripts/check_carriers.py +238 -0
  37. package/templates/qoder/scripts/check_mcp.py +298 -0
  38. package/templates/qoder/scripts/check_qoderwork_consistency.py +166 -0
  39. package/templates/qoder/scripts/common/developer.py +26 -19
  40. package/templates/qoder/scripts/common/events.py +46 -0
  41. package/templates/qoder/scripts/common/extract.py +419 -0
  42. package/templates/qoder/scripts/common/graph_traverse.py +533 -0
  43. package/templates/qoder/scripts/common/identity.py +6 -1
  44. package/templates/qoder/scripts/common/paths.py +89 -0
  45. package/templates/qoder/scripts/common/pip_install.py +144 -0
  46. package/templates/qoder/scripts/common/platform_guard.py +61 -0
  47. package/templates/qoder/scripts/common/search_engine.py +205 -205
  48. package/templates/qoder/scripts/common/terms.py +57 -0
  49. package/templates/qoder/scripts/common/ts_extract.py +536 -0
  50. package/templates/qoder/scripts/context_pack.py +73 -13
  51. package/templates/qoder/scripts/enrich_prompt.py +226 -0
  52. package/templates/qoder/scripts/eval_prd.py +318 -225
  53. package/templates/qoder/scripts/export.py +487 -487
  54. package/templates/qoder/scripts/extract_api_params.py +246 -0
  55. package/templates/qoder/scripts/extract_routes.py +54 -0
  56. package/templates/qoder/scripts/extract_routes_tree.py +78 -0
  57. package/templates/qoder/scripts/fill_prototype.py +707 -0
  58. package/templates/qoder/scripts/gen_design_doc.py +394 -0
  59. package/templates/qoder/scripts/git_sync.py +27 -15
  60. package/templates/qoder/scripts/init_doctor.py +306 -41
  61. package/templates/qoder/scripts/install_qoderwork.py +366 -9
  62. package/templates/qoder/scripts/kg.py +708 -0
  63. package/templates/qoder/scripts/kg_auto_login.py +196 -0
  64. package/templates/qoder/scripts/kg_build.py +612 -0
  65. package/templates/qoder/scripts/kg_build_db.py +327 -0
  66. package/templates/qoder/scripts/kg_duckdb.py +549 -0
  67. package/templates/qoder/scripts/kg_incremental.py +393 -0
  68. package/templates/qoder/scripts/kg_link_db.py +224 -0
  69. package/templates/qoder/scripts/kg_mcp_server.py +801 -0
  70. package/templates/qoder/scripts/kg_semantic.py +150 -0
  71. package/templates/qoder/scripts/kg_test_runner.py +241 -0
  72. package/templates/qoder/scripts/lanhu_stdio_wrapper.py +119 -0
  73. package/templates/qoder/scripts/learn.py +118 -39
  74. package/templates/qoder/scripts/learn_aggregate.py +201 -0
  75. package/templates/qoder/scripts/mcp_launcher.py +359 -0
  76. package/templates/qoder/scripts/mysql_mcp_server.py +396 -0
  77. package/templates/qoder/scripts/repo_root.py +106 -0
  78. package/templates/qoder/scripts/role.py +12 -0
  79. package/templates/qoder/scripts/run_weekly_update.bat +5 -0
  80. package/templates/qoder/scripts/run_weekly_update.sh +5 -0
  81. package/templates/qoder/scripts/search_index.py +307 -60
  82. package/templates/qoder/scripts/secure-ls.js +5640 -0
  83. package/templates/qoder/scripts/setup.py +706 -453
  84. package/templates/qoder/scripts/setup_lanhu.py +963 -0
  85. package/templates/qoder/scripts/status.py +250 -11
  86. package/templates/qoder/scripts/sync_carriers.py +259 -0
  87. package/templates/qoder/scripts/syncgate.py +5 -4
  88. package/templates/qoder/scripts/task.py +75 -0
  89. package/templates/qoder/scripts/team_sync.py +60 -4
  90. package/templates/qoder/scripts/workspace_init.py +1 -1
  91. package/templates/qoder/skills/design-import/SKILL.md +226 -0
  92. package/templates/qoder/skills/design-import/figma-workflow.md +81 -0
  93. package/templates/qoder/skills/design-review/SKILL.md +82 -25
  94. package/templates/qoder/skills/prd-generator/SKILL.md +185 -58
  95. package/templates/qoder/skills/prd-review/SKILL.md +18 -1
  96. package/templates/qoder/skills/prompt-enrich/SKILL.md +90 -0
  97. package/templates/qoder/skills/prototype-generator/SKILL.md +256 -141
  98. package/templates/qoder/skills/prototype-generator/SKILL.md.zcode-79180-2af4721f-f9a6-412c-88db-c0af680d211b.tmp +0 -0
  99. package/templates/qoder/skills/spec-coder/SKILL.md +18 -1
  100. package/templates/qoder/skills/spec-generator/SKILL.md +18 -1
  101. package/templates/qoder/skills/test-generator/SKILL.md +15 -2
  102. package/templates/qoder/skills/wl-code/SKILL.md +55 -36
  103. package/templates/qoder/skills/wl-commit/SKILL.md +89 -76
  104. package/templates/qoder/skills/wl-design/SKILL.md +55 -0
  105. package/templates/qoder/skills/wl-init/SKILL.md +76 -67
  106. package/templates/qoder/skills/wl-insight/SKILL.md +201 -81
  107. package/templates/qoder/skills/wl-prd-full/SKILL.md +69 -0
  108. package/templates/qoder/skills/wl-prd-quick/SKILL.md +49 -0
  109. package/templates/qoder/skills/wl-prd-review/SKILL.md +34 -0
  110. package/templates/qoder/skills/wl-report/SKILL.md +131 -107
  111. package/templates/qoder/skills/wl-search/SKILL.md +141 -75
  112. package/templates/qoder/skills/wl-spec/SKILL.md +49 -39
  113. package/templates/qoder/skills/wl-status/SKILL.md +83 -61
  114. package/templates/qoder/skills/wl-task/SKILL.md +132 -58
  115. package/templates/qoder/skills/wl-test/SKILL.md +406 -40
  116. package/templates/qoder/templates/prd-full-template.md +2 -0
  117. package/templates/qoder/templates/prd-quick-template.md +1 -0
  118. package/templates/qoder/templates/prototype-app.html +13 -8
  119. package/templates/qoder/templates/prototype-web.html +376 -93
  120. package/templates/root/AGENTS.md +89 -34
  121. package/templates/root/requirements.txt +21 -0
  122. package/templates/root//344/275/277/347/224/250/350/257/264/346/230/216.md +259 -259
  123. package/templates/root//346/226/260/346/211/213/346/214/207/345/215/227.md +186 -186
  124. package/templates/qoder/agents/prd-planning.md +0 -56
  125. package/templates/qoder/agents/prd-research.md +0 -33
  126. package/templates/qoder/commands/wl-insight.md +0 -51
  127. package/templates/qoder/skills/wl-prd/SKILL.md +0 -89
@@ -0,0 +1,150 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ LLM semantic labeling layer for the knowledge graph.
4
+
5
+ After kg_build.py extracts structural data (AST-level), this module:
6
+ 1. Finds entities that lack semantic labels (cn == dir, no business_domain)
7
+ 2. Outputs a labeling prompt for the AI (QoderWork) to fill in
8
+ 3. Provides a write-back function to store labels in DuckDB
9
+
10
+ The AI (QoderWork's LLM) sees the prompt during session-start or when
11
+ kg_build reports new modules, and labels them — zero human trigger.
12
+
13
+ Usage (called by AI automatically):
14
+ python kg_semantic.py --check # check what needs labeling
15
+ python kg_semantic.py --label '{"assets": {"cn": "资产管理", "domain": "固定资产"}}'
16
+ """
17
+ import json
18
+ import os
19
+ import sys
20
+
21
+ SCRIPTS = os.path.dirname(os.path.abspath(__file__))
22
+ sys.path.insert(0, SCRIPTS)
23
+ from common.paths import DATA_INDEX_DIR
24
+ from kg_duckdb import get_db
25
+
26
+ DB_PATH = str(DATA_INDEX_DIR / 'kg.duckdb')
27
+
28
+
29
+ def check_unlabeled():
30
+ """Find modules that need semantic labeling (cn == dir or cn is empty)."""
31
+ con = get_db()
32
+ # Find FEATURE entities where cn == dir (unlabeled) or cn is empty
33
+ rows = con.execute("""
34
+ SELECT id, canonical, cn FROM entities
35
+ WHERE type = 'FEATURE'
36
+ AND (cn = canonical OR cn = '' OR cn IS NULL)
37
+ ORDER BY canonical
38
+ """).fetchall()
39
+
40
+ if not rows:
41
+ return None
42
+
43
+ # Build labeling prompt
44
+ modules = []
45
+ for eid, dirname, cn in rows:
46
+ # Gather context: what endpoints/pages does this module have?
47
+ ep_count = con.execute(
48
+ "SELECT COUNT(*) FROM edges WHERE from_id = ? AND edge_type = 'has_endpoint'",
49
+ [eid]
50
+ ).fetchone()[0]
51
+ btn_count = con.execute(
52
+ "SELECT COUNT(*) FROM edges WHERE from_id = ? AND edge_type = 'has_button'",
53
+ [eid]
54
+ ).fetchone()[0]
55
+ # Sample endpoints for context
56
+ sample_eps = con.execute("""
57
+ SELECT e.to_id FROM edges e
58
+ WHERE e.from_id = ? AND e.edge_type = 'has_endpoint' LIMIT 5
59
+ """, [eid]).fetchall()
60
+
61
+ modules.append({
62
+ 'dir': dirname,
63
+ 'endpoints': ep_count,
64
+ 'buttons': btn_count,
65
+ 'sample_apis': [r[0].replace('E:', '') for r in sample_eps],
66
+ })
67
+
68
+ con.close()
69
+
70
+ prompt = {
71
+ 'action': 'semantic_labeling_needed',
72
+ 'description': '以下模块需要语义标注(中文名+业务领域)。请根据目录名和API端点推断。',
73
+ 'modules': modules,
74
+ 'instructions': (
75
+ '对每个模块返回: {"目录名": {"cn": "中文名", "domain": "业务领域"}}\n'
76
+ '示例: {"assets": {"cn": "资产管理", "domain": "固定资产管理"}}\n'
77
+ '然后用 kg_semantic.py --label 写回'
78
+ ),
79
+ }
80
+ return prompt
81
+
82
+
83
+ def apply_labels(labels_json):
84
+ """Write semantic labels back to DuckDB.
85
+
86
+ labels_json: {"assets": {"cn": "资产管理", "domain": "固定资产"}, ...}
87
+ """
88
+ labels = json.loads(labels_json) if isinstance(labels_json, str) else labels_json
89
+ con = get_db()
90
+ updated = 0
91
+
92
+ for dirname, info in labels.items():
93
+ cn = info.get('cn', dirname)
94
+ domain = info.get('domain', '')
95
+
96
+ # Find the FEATURE entity by canonical name (dir)
97
+ rows = con.execute(
98
+ "SELECT id FROM entities WHERE type='FEATURE' AND canonical = ?",
99
+ [dirname]
100
+ ).fetchall()
101
+
102
+ for row in rows:
103
+ eid = row[0]
104
+ # Update cn
105
+ con.execute(
106
+ "UPDATE entities SET cn = ? WHERE id = ?",
107
+ [cn, eid]
108
+ )
109
+ # Update props with domain
110
+ props_row = con.execute(
111
+ "SELECT props FROM entities WHERE id = ?", [eid]
112
+ ).fetchone()
113
+ if props_row:
114
+ try:
115
+ props = json.loads(props_row[0]) if props_row[0] else {}
116
+ except:
117
+ props = {}
118
+ props['business_domain'] = domain
119
+ props['labeled'] = True
120
+ con.execute(
121
+ "UPDATE entities SET props = ? WHERE id = ?",
122
+ [json.dumps(props, ensure_ascii=False), eid]
123
+ )
124
+ # Update alias
125
+ con.execute(
126
+ "INSERT OR REPLACE INTO aliases VALUES (?, ?)",
127
+ [cn.lower(), eid]
128
+ )
129
+ updated += 1
130
+
131
+ con.close()
132
+ return updated
133
+
134
+
135
+ if __name__ == '__main__':
136
+ if len(sys.argv) > 1 and sys.argv[1] == '--check':
137
+ result = check_unlabeled()
138
+ if result:
139
+ print(json.dumps(result, ensure_ascii=False, indent=2))
140
+ else:
141
+ print('{"status": "all_labeled", "message": "所有模块已有语义标注"}')
142
+
143
+ elif len(sys.argv) > 2 and sys.argv[1] == '--label':
144
+ count = apply_labels(sys.argv[2])
145
+ print('Updated %d entities with semantic labels' % count)
146
+
147
+ else:
148
+ print('Usage:')
149
+ print(' python kg_semantic.py --check # 查看待标注模块')
150
+ print(' python kg_semantic.py --label \'{"assets":{"cn":"资产管理","domain":"固定资产"}}\'')
@@ -0,0 +1,241 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Automated test runner using Playwright (headless) + knowledge graph.
4
+
5
+ Reads trace-chain.json to know "which button triggers which API", then:
6
+ 1. Opens the target page in headless Chromium
7
+ 2. Logs in using credentials from autotest-data.yaml
8
+ 3. Clicks each button found in trace-chain for that page
9
+ 4. Intercepts network requests to verify the expected API was called
10
+ 5. Records pass/fail per button
11
+
12
+ Output: JSON test results + human-readable summary.
13
+
14
+ Usage:
15
+ python kg_test_runner.py --page assets/base/sapAssets/index.vue
16
+ python kg_test_runner.py --module assets
17
+ python kg_test_runner.py --endpoint /asset/list
18
+ """
19
+ import argparse
20
+ import json
21
+ import os
22
+ import sys
23
+ import time
24
+ import yaml
25
+
26
+ SCRIPTS = os.path.dirname(os.path.abspath(__file__))
27
+ sys.path.insert(0, SCRIPTS)
28
+ from common.paths import DATA_INDEX_DIR, PROJECT_ROOT
29
+
30
+ INDEX_DIR = str(DATA_INDEX_DIR)
31
+ CODE_DIR = str(PROJECT_ROOT / 'data' / 'code')
32
+
33
+
34
+ def load_test_config():
35
+ """Load test environment URL + credentials."""
36
+ config_path = os.path.join(SCRIPTS, '..', 'config.yaml')
37
+ data_path = os.path.join(SCRIPTS, '..', '..', 'workspace', 'members',
38
+ os.environ.get('QODER_DEV', 'hupan'), 'autotest-data.yaml')
39
+
40
+ config = yaml.safe_load(open(config_path, encoding='utf-8'))
41
+ autotest = config.get('autotest', {})
42
+ env = autotest.get('default_env', 'test')
43
+ base_url = autotest.get('environments', {}).get(env, {}).get('web', {}).get('base_url', '')
44
+
45
+ creds = {}
46
+ if os.path.isfile(data_path):
47
+ creds = yaml.safe_load(open(data_path, encoding='utf-8')) or {}
48
+
49
+ return {
50
+ 'base_url': base_url,
51
+ 'env': env,
52
+ 'username': str(creds.get('test', {}).get('登录账号', creds.get('登录账号', 'admin'))),
53
+ 'password': str(creds.get('test', {}).get('登录密码', creds.get('登录密码', 'admin123'))),
54
+ }
55
+
56
+
57
+ def load_traces_for_page(page_filter):
58
+ """Load trace-chain entries matching a page/module/endpoint filter."""
59
+ trace_path = os.path.join(BUILD_CACHE_DIR, 'trace-chain.json')
60
+ if not os.path.isfile(trace_path):
61
+ return []
62
+ tc = json.load(open(trace_path, encoding='utf-8'))
63
+ results = []
64
+ for t in tc.get('traces', []):
65
+ vue = (t.get('vue_file') or '').replace('\\', '/')
66
+ ep = t.get('endpoint') or ''
67
+ # Match by page path, module, or endpoint
68
+ if page_filter.lower() in vue.lower() or page_filter.lower() in ep.lower():
69
+ if t.get('confidence') in ('high', 'medium'):
70
+ results.append(t)
71
+ return results
72
+
73
+
74
+ def run_tests(page_filter, headless=True, max_tests=10):
75
+ """Run automated tests for matching traces."""
76
+ from playwright.sync_api import sync_playwright
77
+
78
+ config = load_test_config()
79
+ traces = load_traces_for_page(page_filter)[:max_tests]
80
+
81
+ if not traces:
82
+ return {'error': 'No traces found for "%s"' % page_filter,
83
+ 'hint': 'Try: --page assets or --module cost or --endpoint /asset'}
84
+
85
+ results = {
86
+ 'page_filter': page_filter,
87
+ 'base_url': config['base_url'],
88
+ 'total': len(traces),
89
+ 'passed': 0,
90
+ 'failed': 0,
91
+ 'skipped': 0,
92
+ 'details': [],
93
+ }
94
+
95
+ with sync_playwright() as p:
96
+ browser = p.chromium.launch(headless=headless)
97
+ context = browser.new_context()
98
+ page = context.new_page()
99
+
100
+ # Login first
101
+ login_url = config['base_url'].rstrip('/') + '/auth/login'
102
+ page.goto(login_url, wait_until='networkidle')
103
+
104
+ # Fill login form
105
+ inputs = page.query_selector_all('input')
106
+ if len(inputs) >= 2:
107
+ inputs[0].fill(config['username'])
108
+ inputs[1].fill(config['password'])
109
+
110
+ # Click login button
111
+ buttons = page.query_selector_all('button')
112
+ for btn in buttons:
113
+ text = btn.inner_text().strip()
114
+ if '登录' in text and '账号' not in text:
115
+ btn.click()
116
+ break
117
+
118
+ page.wait_for_timeout(5000) # Wait for login
119
+
120
+ # Check if logged in
121
+ if '/auth/login' in page.url:
122
+ results['login_failed'] = True
123
+ results['details'].append({'test': 'login', 'status': 'FAIL',
124
+ 'reason': 'Still on login page'})
125
+ browser.close()
126
+ return results
127
+
128
+ results['login_failed'] = False
129
+
130
+ # Run each trace as a test
131
+ for trace in traces:
132
+ vue = trace.get('vue_file', '').replace('\\', '/')
133
+ button = trace.get('button', '')
134
+ handler = trace.get('handler', '')
135
+ endpoint = trace.get('endpoint', '')
136
+ confidence = trace.get('confidence', '')
137
+
138
+ test_name = '[%s] %s.%s → %s' % (button, vue.split('/views/')[-1] if '/views/' in vue else vue, handler, endpoint)
139
+
140
+ # Navigate to the page
141
+ # ICS routes are backend-driven, we need the correct URL
142
+ # For now, skip navigation (assume already on the right page from SPA routing)
143
+ # A real implementation would map vue_file → route path
144
+
145
+ # Set up network interception
146
+ captured = []
147
+ def on_request(req):
148
+ if endpoint and endpoint in req.url:
149
+ captured.append({'url': req.url, 'method': req.method})
150
+ page.on('request', on_request)
151
+
152
+ # Try to click the button by text
153
+ clicked = False
154
+ try:
155
+ btn_elements = page.query_selector_all('button')
156
+ for el in btn_elements:
157
+ btn_text = el.inner_text().strip()
158
+ if button and button in btn_text:
159
+ el.click(timeout=3000)
160
+ clicked = True
161
+ break
162
+ except Exception:
163
+ pass
164
+
165
+ page.wait_for_timeout(2000)
166
+
167
+ # Verify: was the expected API called?
168
+ if clicked and captured:
169
+ status = 'PASS'
170
+ results['passed'] += 1
171
+ elif clicked and not captured:
172
+ status = 'FAIL'
173
+ results['failed'] += 1
174
+ else:
175
+ status = 'SKIP'
176
+ results['skipped'] += 1
177
+
178
+ results['details'].append({
179
+ 'test': test_name[:80],
180
+ 'status': status,
181
+ 'button': button,
182
+ 'endpoint': endpoint,
183
+ 'api_called': len(captured) > 0,
184
+ 'confidence': confidence,
185
+ })
186
+
187
+ # Remove listener
188
+ page.remove_listener('request', on_request)
189
+
190
+ browser.close()
191
+
192
+ return results
193
+
194
+
195
+ def format_results(results):
196
+ """Format test results as human-readable text."""
197
+ if 'error' in results:
198
+ return results['error']
199
+
200
+ lines = ['## 自动测试结果', '']
201
+ lines.append('环境: %s' % results.get('base_url', '?'))
202
+ lines.append('过滤: %s' % results.get('page_filter', '?'))
203
+ lines.append('登录: %s' % ('失败' if results.get('login_failed') else '成功'))
204
+ lines.append('')
205
+ lines.append('总计: %d | 通过: %d | 失败: %d | 跳过: %d' % (
206
+ results['total'], results['passed'], results['failed'], results['skipped']))
207
+ lines.append('')
208
+
209
+ for d in results.get('details', []):
210
+ icon = {'PASS': '✅', 'FAIL': '❌', 'SKIP': '⏭️'}.get(d['status'], '?')
211
+ lines.append('%s %s' % (icon, d['test'][:70]))
212
+ if d['status'] == 'FAIL':
213
+ lines.append(' 按钮已点击但未拦截到 %s' % d.get('endpoint', ''))
214
+ elif d['status'] == 'SKIP':
215
+ lines.append(' 未找到按钮"%s"' % d.get('button', ''))
216
+
217
+ return '\n'.join(lines)
218
+
219
+
220
+ if __name__ == '__main__':
221
+ parser = argparse.ArgumentParser(description='Knowledge-graph-driven test runner')
222
+ parser.add_argument('--page', default='', help='页面路径过滤 (如 assets/index)')
223
+ parser.add_argument('--module', default='', help='模块名过滤 (如 assets)')
224
+ parser.add_argument('--endpoint', default='', help='端点过滤 (如 /asset/list)')
225
+ parser.add_argument('--headed', action='store_true', help='显示浏览器窗口(调试用)')
226
+ parser.add_argument('--max', type=int, default=10, help='最大测试数')
227
+ args = parser.parse_args()
228
+
229
+ page_filter = args.page or args.module or args.endpoint
230
+ if not page_filter:
231
+ print('请指定 --page / --module / --endpoint')
232
+ sys.exit(1)
233
+
234
+ results = run_tests(page_filter, headless=not args.headed, max_tests=args.max)
235
+ print(format_results(results))
236
+
237
+ # Save JSON results
238
+ results_path = os.path.join(INDEX_DIR, 'test-results.json')
239
+ with open(results_path, 'w', encoding='utf-8') as f:
240
+ json.dump(results, f, ensure_ascii=False, indent=2)
241
+ print('\n详细结果: %s' % results_path)
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ lanhu_stdio_wrapper.py - 蓝湖 MCP STDIO 启动包装(角色隔离 + QoderWork 自动开关)
5
+
6
+ 被谁调用:
7
+ QoderWork 按 ~/.qoderwork/mcp.json 的 lanhu.command 拉起本脚本。
8
+ QoderWork 管本进程的生死 (开 QoderWork 自动拉起, 关 QoderWork 自动杀)。
9
+
10
+ 做什么:
11
+ 1. 定位仓库根
12
+ 2. 读 .qoder/.developer → 当前角色 (UI设计师/PM/开发...)
13
+ 3. 读 workspace/members/{角色}/.secrets/lanhu.env → 该角色的蓝湖 cookie
14
+ 4. 注入环境变量 (LANHU_COOKIE + MCP_TRANSPORT=stdio + LANHU_USER_NAME)
15
+ 5. os.execv() 拉起 lanhu_mcp_server.py (用 venv 的 3.10+ python)
16
+
17
+ 为什么用 wrapper 而不是直接在 mcp.json 写 cookie:
18
+ 蓝湖权限按角色分 (UI 能改稿, PM/开发只读)。
19
+ cookie 必须按角色隔离, 换 .developer 时跟着换。
20
+ wrapper 每次启动读当前角色 → 用对应 cookie → 权限正确。
21
+ cookie 不进 mcp.json (那是机器级全局, 不按人), 仍按角色存 .secrets/。
22
+
23
+ 为什么用 os.execv 而非 subprocess:
24
+ execv 让 lanhu server 直接替换 wrapper 进程。
25
+ QoderWork 杀的就是 lanhu server 本身, 不留孤儿 wrapper 进程。
26
+
27
+ 跨平台:
28
+ 纯 Python, os.execv + venv python 路径跨平台 (win Scripts/python.exe, mac bin/python)。
29
+ """
30
+
31
+ import os
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ try:
36
+ sys.stderr.reconfigure(encoding='utf-8', errors='replace')
37
+ except (AttributeError, TypeError, OSError):
38
+ pass
39
+
40
+ # 定位仓库根: 本文件在 <repo>/.qoder/scripts/ → 上三级
41
+ SCRIPT_DIR = Path(__file__).resolve().parent
42
+ BASE = SCRIPT_DIR.parent.parent # repo root
43
+
44
+
45
+ def _find_repo_root():
46
+ """定位仓库根。本文件在 .qoder/scripts/, 上三级即根。
47
+ 兜底: 上溯找 .qoder/ 目录。"""
48
+ if (BASE / '.qoder').is_dir():
49
+ return BASE
50
+ # 兜底: 上溯 10 级找 .qoder/
51
+ cur = SCRIPT_DIR
52
+ for _ in range(10):
53
+ if (cur / '.qoder').is_dir():
54
+ return cur
55
+ parent = cur.parent
56
+ if parent == cur:
57
+ break
58
+ cur = parent
59
+ return BASE # 找不到回退默认
60
+
61
+
62
+ def main():
63
+ repo = _find_repo_root()
64
+
65
+ # 复用 setup_lanhu.py 的角色探测 + cookie 读取 (单一事实源)
66
+ sys.path.insert(0, str(repo / '.qoder' / 'scripts'))
67
+ try:
68
+ from setup_lanhu import (_current_developer, lanhu_env_file,
69
+ _read_env_cookie, _venv_python,
70
+ LANHU_VENV, LANHU_SERVER_SCRIPT, COOKIE_PLACEHOLDER)
71
+ except ImportError as e:
72
+ sys.stderr.write(f'[lanhu-wrapper] 无法导入 setup_lanhu: {e}\n')
73
+ sys.stderr.write(f'[lanhu-wrapper] 检查 {repo}/.qoder/scripts/setup_lanhu.py 是否存在\n')
74
+ sys.exit(1)
75
+
76
+ # 1. 当前角色
77
+ dev = _current_developer()
78
+ if not dev:
79
+ sys.stderr.write('[lanhu-wrapper] 未初始化开发者 (.qoder/.developer 不存在)。\n')
80
+ sys.stderr.write(' 跑 /wl-init 或 python .qoder/scripts/setup.py 初始化。\n')
81
+ sys.exit(1)
82
+
83
+ # 2. 该角色的 cookie
84
+ cookie = _read_env_cookie(dev)
85
+ if not cookie or cookie == COOKIE_PLACEHOLDER:
86
+ env_file = lanhu_env_file(dev)
87
+ sys.stderr.write(f'[lanhu-wrapper] 角色 {dev} 的蓝湖 cookie 未配置 (占位/缺失)。\n')
88
+ sys.stderr.write(f' 编辑 {env_file}, 把 {COOKIE_PLACEHOLDER} 换成真 cookie。\n')
89
+ sys.stderr.write(' 获取: lanhuapp.com 登录 → F12 → Network → Cookie 头整行复制\n')
90
+ sys.stderr.write(' (蓝湖工具将不可用, 但工作流其他功能不受影响)\n')
91
+ sys.exit(1)
92
+
93
+ # 3. venv python (lanhu 依赖要求 3.10+)
94
+ venv_py = _venv_python(LANHU_VENV)
95
+ if not venv_py.is_file():
96
+ sys.stderr.write(f'[lanhu-wrapper] 蓝湖 venv 不存在: {venv_py}\n')
97
+ sys.stderr.write(' 跑 python .qoder/scripts/setup_lanhu.py 安装蓝湖依赖\n')
98
+ sys.exit(1)
99
+
100
+ if not LANHU_SERVER_SCRIPT.is_file():
101
+ sys.stderr.write(f'[lanhu-wrapper] 蓝湖服务脚本不存在: {LANHU_SERVER_SCRIPT}\n')
102
+ sys.stderr.write(' 跑 python .qoder/scripts/setup_lanhu.py clone 蓝湖源码\n')
103
+ sys.exit(1)
104
+
105
+ # 4. 注入环境变量 (角色隔离的核心: cookie + 用户名都是当前角色的)
106
+ os.environ['LANHU_COOKIE'] = cookie
107
+ os.environ['MCP_TRANSPORT'] = 'stdio'
108
+ os.environ['LANHU_USER_NAME'] = dev
109
+ os.environ['LANHU_USER_ROLE'] = 'Developer'
110
+
111
+ # 调试日志打到 stderr (MCP stdio 协议用 stdout, 不能污染)
112
+ sys.stderr.write(f'[lanhu-wrapper] 启动蓝湖 MCP (STDIO, 角色: {dev})\n')
113
+
114
+ # 5. execv 拉起 lanhu server (接管进程, QoderWork 直接管它生死)
115
+ os.execv(str(venv_py), [str(venv_py), str(LANHU_SERVER_SCRIPT)])
116
+
117
+
118
+ if __name__ == '__main__':
119
+ main()