@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,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()
|