@hupan56/wlkj 2.2.3 → 2.2.5
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 +532 -532
- package/package.json +28 -28
- package/templates/qoder/hooks/inject-workflow-state.py +117 -117
- package/templates/qoder/hooks/session-start.py +204 -204
- package/templates/qoder/scripts/common/developer.py +231 -161
- package/templates/qoder/scripts/common/paths.py +310 -310
- package/templates/qoder/scripts/common/task_utils.py +392 -387
- package/templates/qoder/scripts/init_developer.py +75 -75
- package/templates/qoder/scripts/install_qoderwork.py +367 -367
- package/templates/qoder/scripts/role.py +39 -39
- package/templates/qoder/scripts/syncgate.py +333 -333
- package/templates/qoder/scripts/team_sync.py +439 -439
- package/templates/qoder/skills/design-review/SKILL.md +25 -25
- package/templates/qoder/skills/prd-generator/SKILL.md +180 -180
- package/templates/qoder/skills/prd-review/SKILL.md +36 -36
- package/templates/qoder/skills/prototype-generator/SKILL.md +141 -141
- package/templates/qoder/skills/spec-coder/SKILL.md +68 -68
- package/templates/qoder/skills/spec-generator/SKILL.md +66 -66
- package/templates/qoder/skills/test-generator/SKILL.md +71 -71
- package/templates/root/AGENTS.md +182 -182
|
@@ -1,204 +1,204 @@
|
|
|
1
|
-
# session-start.py - Inject project context on session start
|
|
2
|
-
import os, json, sys
|
|
3
|
-
|
|
4
|
-
if sys.platform == 'win32':
|
|
5
|
-
try:
|
|
6
|
-
sys.stdout.reconfigure(encoding='utf-8')
|
|
7
|
-
except Exception:
|
|
8
|
-
pass
|
|
9
|
-
|
|
10
|
-
NL = chr(10)
|
|
11
|
-
BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
12
|
-
|
|
13
|
-
# Injected as REAL OUTPUT below (a comment here would never reach the AI)
|
|
14
|
-
CRITICAL_RULE = NL.join([
|
|
15
|
-
'## CRITICAL RULE',
|
|
16
|
-
'When user asks for /wl-prd, ALWAYS ask platform FIRST:',
|
|
17
|
-
'"这个需求是针对哪个平台?',
|
|
18
|
-
' 1. Web 管理端 (fywl-ui) - Ant Design Vue + VxeGrid 风格',
|
|
19
|
-
' 2. APP 移动端 (Carmg-H5) - Vant 风格',
|
|
20
|
-
' 3. 两端都要',
|
|
21
|
-
' 请选择 (1/2/3):"',
|
|
22
|
-
'DO NOT skip this question. DO NOT auto-detect. ALWAYS ASK.',
|
|
23
|
-
])
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def load_json_safe(path):
|
|
27
|
-
try:
|
|
28
|
-
with open(path, encoding='utf-8') as f:
|
|
29
|
-
return json.load(f)
|
|
30
|
-
except Exception:
|
|
31
|
-
return None
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def get_developer():
|
|
35
|
-
df = os.path.join(BASE, '.qoder', '.developer')
|
|
36
|
-
if not os.path.isfile(df):
|
|
37
|
-
return None
|
|
38
|
-
try:
|
|
39
|
-
with open(df, encoding='utf-8') as f:
|
|
40
|
-
content = f.read()
|
|
41
|
-
except Exception:
|
|
42
|
-
return None
|
|
43
|
-
info = {}
|
|
44
|
-
for line in content.strip().splitlines():
|
|
45
|
-
for sep in ('=', ':'):
|
|
46
|
-
if sep in line:
|
|
47
|
-
k, v = line.split(sep, 1)
|
|
48
|
-
info[k.strip()] = v.strip()
|
|
49
|
-
break
|
|
50
|
-
return info or None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def get_active_tasks():
|
|
54
|
-
td = os.path.join(BASE, 'workspace', 'tasks')
|
|
55
|
-
if not os.path.isdir(td):
|
|
56
|
-
return []
|
|
57
|
-
tasks = []
|
|
58
|
-
for d in sorted(os.listdir(td)):
|
|
59
|
-
data = load_json_safe(os.path.join(td, d, 'task.json'))
|
|
60
|
-
if data is None:
|
|
61
|
-
continue
|
|
62
|
-
tasks.append({
|
|
63
|
-
'name': d,
|
|
64
|
-
'title': data.get('title', d),
|
|
65
|
-
'status': data.get('status', 'unknown')
|
|
66
|
-
})
|
|
67
|
-
return tasks
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def get_team_size():
|
|
71
|
-
md = os.path.join(BASE, 'workspace', 'members')
|
|
72
|
-
if not os.path.isdir(md):
|
|
73
|
-
return 0
|
|
74
|
-
return len([d for d in os.listdir(md) if os.path.isdir(os.path.join(md, d))])
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def get_index_info():
|
|
78
|
-
lines = []
|
|
79
|
-
lines.append('## Knowledge Index')
|
|
80
|
-
lines.append('Use search script (DO NOT read large JSON files directly):')
|
|
81
|
-
lines.append(' python .qoder/scripts/search_index.py <keyword> [--platform web|app]')
|
|
82
|
-
lines.append(' python .qoder/scripts/search_index.py --prd <keyword> # search PRDs')
|
|
83
|
-
lines.append(' python .qoder/scripts/search_index.py --style <table|form> [--platform p]')
|
|
84
|
-
lines.append(' python .qoder/scripts/search_index.py --field <name> # field usage')
|
|
85
|
-
lines.append(' python .qoder/scripts/search_index.py --api <keyword> # API endpoints')
|
|
86
|
-
lines.append(' python .qoder/scripts/search_index.py --components # UI components')
|
|
87
|
-
lines.append(' python .qoder/scripts/search_index.py --modules # project overview')
|
|
88
|
-
lines.append(' python .qoder/scripts/search_index.py --list # top keywords')
|
|
89
|
-
|
|
90
|
-
meta = load_json_safe(os.path.join(BASE, 'data', 'index', '.index-meta.json'))
|
|
91
|
-
if meta:
|
|
92
|
-
projects = meta.get('projects', {})
|
|
93
|
-
if isinstance(projects, dict):
|
|
94
|
-
for proj, info in projects.items():
|
|
95
|
-
if isinstance(info, dict):
|
|
96
|
-
files = info.get('files', 0)
|
|
97
|
-
else:
|
|
98
|
-
files = info # new meta format: {project: file_count}
|
|
99
|
-
lines.append(' ' + str(proj) + ': ' + str(files) + ' files')
|
|
100
|
-
elif isinstance(projects, list):
|
|
101
|
-
for proj in projects:
|
|
102
|
-
lines.append(' ' + str(proj))
|
|
103
|
-
lines.append('Last sync: ' + str(meta.get('last_sync', 'unknown')))
|
|
104
|
-
# Staleness warning (审计发现: cron 从未注册, 索引可能静默过期)
|
|
105
|
-
last_sync = meta.get('last_sync')
|
|
106
|
-
if last_sync:
|
|
107
|
-
try:
|
|
108
|
-
from datetime import datetime
|
|
109
|
-
# 兼容多种格式: "2026-06-12 16:49" / ISO
|
|
110
|
-
ts = str(last_sync).replace('T', ' ').split('.')[0].strip()
|
|
111
|
-
for fmt in ('%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S'):
|
|
112
|
-
try:
|
|
113
|
-
sync_dt = datetime.strptime(ts, fmt)
|
|
114
|
-
break
|
|
115
|
-
except ValueError:
|
|
116
|
-
continue
|
|
117
|
-
else:
|
|
118
|
-
sync_dt = None
|
|
119
|
-
if sync_dt:
|
|
120
|
-
age_days = (datetime.now() - sync_dt).days
|
|
121
|
-
if age_days > 30:
|
|
122
|
-
lines.append('[CRITICAL] 索引已 {} 天未更新! 搜索结果严重过期. 跑: python .qoder/scripts/git_sync.py'.format(age_days))
|
|
123
|
-
elif age_days > 7:
|
|
124
|
-
lines.append('[WARN] 索引已 {} 天未更新, 建议刷新: python .qoder/scripts/git_sync.py'.format(age_days))
|
|
125
|
-
except Exception:
|
|
126
|
-
pass # 日期解析失败不阻塞
|
|
127
|
-
if meta.get('failures'):
|
|
128
|
-
lines.append('WARNING: last sync had errors - see data/index/.index-meta.json')
|
|
129
|
-
|
|
130
|
-
return NL.join(lines)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def get_style_info():
|
|
134
|
-
"""Read style metadata for quick reference"""
|
|
135
|
-
meta = load_json_safe(os.path.join(BASE, 'data', 'index', 'style-meta.json'))
|
|
136
|
-
if not meta:
|
|
137
|
-
return None
|
|
138
|
-
|
|
139
|
-
lines = []
|
|
140
|
-
lines.append('## UI Style Index')
|
|
141
|
-
lines.append('Style search: python .qoder/scripts/search_index.py --style <table|form|modal>')
|
|
142
|
-
lines.append('Field search: python .qoder/scripts/search_index.py --field <name>')
|
|
143
|
-
|
|
144
|
-
for proj, d in meta.get('projects', {}).items():
|
|
145
|
-
vue = d.get('vue_files', 0)
|
|
146
|
-
if vue > 0:
|
|
147
|
-
pt = d.get('page_types', {})
|
|
148
|
-
types_str = ', '.join([k + ':' + str(v) for k, v in pt.items() if v > 0])
|
|
149
|
-
lines.append(' ' + proj + ': ' + str(vue) + ' Vue (' + types_str + ')')
|
|
150
|
-
|
|
151
|
-
lines.append('Top components: ' + ', '.join(list(meta.get('common_components', {}).keys())[:8]))
|
|
152
|
-
lines.append('Field map: ' + str(meta.get('field_count', 0)) + ' entries')
|
|
153
|
-
|
|
154
|
-
if os.path.isdir(os.path.join(BASE, 'data', 'style')):
|
|
155
|
-
lines.append('Style PDFs: data/style/ (design specs, code takes priority)')
|
|
156
|
-
|
|
157
|
-
return NL.join(lines)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def main():
|
|
161
|
-
parts = []
|
|
162
|
-
parts.append('<qoder-context>')
|
|
163
|
-
dev = get_developer()
|
|
164
|
-
if dev:
|
|
165
|
-
name = dev.get('name', 'unknown')
|
|
166
|
-
role = dev.get('role', 'unknown')
|
|
167
|
-
parts.append('Developer: ' + name + ' (' + role + ')')
|
|
168
|
-
parts.append('Workspace: workspace/members/' + name + '/')
|
|
169
|
-
else:
|
|
170
|
-
parts.append('Developer: NOT SET. Run /wl-init to register.')
|
|
171
|
-
team = get_team_size()
|
|
172
|
-
if team > 0:
|
|
173
|
-
parts.append('Team members: ' + str(team))
|
|
174
|
-
tasks = get_active_tasks()
|
|
175
|
-
if tasks:
|
|
176
|
-
parts.append('Active tasks:')
|
|
177
|
-
for t in tasks:
|
|
178
|
-
parts.append(' [' + t['status'] + '] ' + t['name'] + ' - ' + t['title'])
|
|
179
|
-
|
|
180
|
-
parts.append('')
|
|
181
|
-
parts.append(CRITICAL_RULE)
|
|
182
|
-
|
|
183
|
-
idx_info = get_index_info()
|
|
184
|
-
if idx_info:
|
|
185
|
-
parts.append('')
|
|
186
|
-
parts.append(idx_info)
|
|
187
|
-
|
|
188
|
-
style_info = get_style_info()
|
|
189
|
-
if style_info:
|
|
190
|
-
parts.append('')
|
|
191
|
-
parts.append(style_info)
|
|
192
|
-
|
|
193
|
-
parts.append('</qoder-context>')
|
|
194
|
-
print(NL.join(parts))
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if __name__ == '__main__':
|
|
198
|
-
try:
|
|
199
|
-
main()
|
|
200
|
-
except Exception as e:
|
|
201
|
-
# A broken hook must never kill the session - emit minimal context
|
|
202
|
-
print('<qoder-context>')
|
|
203
|
-
print('session-start hook error: ' + str(e)[:200])
|
|
204
|
-
print('</qoder-context>')
|
|
1
|
+
# session-start.py - Inject project context on session start
|
|
2
|
+
import os, json, sys
|
|
3
|
+
|
|
4
|
+
if sys.platform == 'win32':
|
|
5
|
+
try:
|
|
6
|
+
sys.stdout.reconfigure(encoding='utf-8')
|
|
7
|
+
except Exception:
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
NL = chr(10)
|
|
11
|
+
BASE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
12
|
+
|
|
13
|
+
# Injected as REAL OUTPUT below (a comment here would never reach the AI)
|
|
14
|
+
CRITICAL_RULE = NL.join([
|
|
15
|
+
'## CRITICAL RULE',
|
|
16
|
+
'When user asks for /wl-prd, ALWAYS ask platform FIRST:',
|
|
17
|
+
'"这个需求是针对哪个平台?',
|
|
18
|
+
' 1. Web 管理端 (fywl-ui) - Ant Design Vue + VxeGrid 风格',
|
|
19
|
+
' 2. APP 移动端 (Carmg-H5) - Vant 风格',
|
|
20
|
+
' 3. 两端都要',
|
|
21
|
+
' 请选择 (1/2/3):"',
|
|
22
|
+
'DO NOT skip this question. DO NOT auto-detect. ALWAYS ASK.',
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_json_safe(path):
|
|
27
|
+
try:
|
|
28
|
+
with open(path, encoding='utf-8') as f:
|
|
29
|
+
return json.load(f)
|
|
30
|
+
except Exception:
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_developer():
|
|
35
|
+
df = os.path.join(BASE, '.qoder', '.developer')
|
|
36
|
+
if not os.path.isfile(df):
|
|
37
|
+
return None
|
|
38
|
+
try:
|
|
39
|
+
with open(df, encoding='utf-8') as f:
|
|
40
|
+
content = f.read()
|
|
41
|
+
except Exception:
|
|
42
|
+
return None
|
|
43
|
+
info = {}
|
|
44
|
+
for line in content.strip().splitlines():
|
|
45
|
+
for sep in ('=', ':'):
|
|
46
|
+
if sep in line:
|
|
47
|
+
k, v = line.split(sep, 1)
|
|
48
|
+
info[k.strip()] = v.strip()
|
|
49
|
+
break
|
|
50
|
+
return info or None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_active_tasks():
|
|
54
|
+
td = os.path.join(BASE, 'workspace', 'tasks')
|
|
55
|
+
if not os.path.isdir(td):
|
|
56
|
+
return []
|
|
57
|
+
tasks = []
|
|
58
|
+
for d in sorted(os.listdir(td)):
|
|
59
|
+
data = load_json_safe(os.path.join(td, d, 'task.json'))
|
|
60
|
+
if data is None:
|
|
61
|
+
continue
|
|
62
|
+
tasks.append({
|
|
63
|
+
'name': d,
|
|
64
|
+
'title': data.get('title', d),
|
|
65
|
+
'status': data.get('status', 'unknown')
|
|
66
|
+
})
|
|
67
|
+
return tasks
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_team_size():
|
|
71
|
+
md = os.path.join(BASE, 'workspace', 'members')
|
|
72
|
+
if not os.path.isdir(md):
|
|
73
|
+
return 0
|
|
74
|
+
return len([d for d in os.listdir(md) if os.path.isdir(os.path.join(md, d))])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_index_info():
|
|
78
|
+
lines = []
|
|
79
|
+
lines.append('## Knowledge Index')
|
|
80
|
+
lines.append('Use search script (DO NOT read large JSON files directly):')
|
|
81
|
+
lines.append(' python .qoder/scripts/search_index.py <keyword> [--platform web|app]')
|
|
82
|
+
lines.append(' python .qoder/scripts/search_index.py --prd <keyword> # search PRDs')
|
|
83
|
+
lines.append(' python .qoder/scripts/search_index.py --style <table|form> [--platform p]')
|
|
84
|
+
lines.append(' python .qoder/scripts/search_index.py --field <name> # field usage')
|
|
85
|
+
lines.append(' python .qoder/scripts/search_index.py --api <keyword> # API endpoints')
|
|
86
|
+
lines.append(' python .qoder/scripts/search_index.py --components # UI components')
|
|
87
|
+
lines.append(' python .qoder/scripts/search_index.py --modules # project overview')
|
|
88
|
+
lines.append(' python .qoder/scripts/search_index.py --list # top keywords')
|
|
89
|
+
|
|
90
|
+
meta = load_json_safe(os.path.join(BASE, 'data', 'index', '.index-meta.json'))
|
|
91
|
+
if meta:
|
|
92
|
+
projects = meta.get('projects', {})
|
|
93
|
+
if isinstance(projects, dict):
|
|
94
|
+
for proj, info in projects.items():
|
|
95
|
+
if isinstance(info, dict):
|
|
96
|
+
files = info.get('files', 0)
|
|
97
|
+
else:
|
|
98
|
+
files = info # new meta format: {project: file_count}
|
|
99
|
+
lines.append(' ' + str(proj) + ': ' + str(files) + ' files')
|
|
100
|
+
elif isinstance(projects, list):
|
|
101
|
+
for proj in projects:
|
|
102
|
+
lines.append(' ' + str(proj))
|
|
103
|
+
lines.append('Last sync: ' + str(meta.get('last_sync', 'unknown')))
|
|
104
|
+
# Staleness warning (审计发现: cron 从未注册, 索引可能静默过期)
|
|
105
|
+
last_sync = meta.get('last_sync')
|
|
106
|
+
if last_sync:
|
|
107
|
+
try:
|
|
108
|
+
from datetime import datetime
|
|
109
|
+
# 兼容多种格式: "2026-06-12 16:49" / ISO
|
|
110
|
+
ts = str(last_sync).replace('T', ' ').split('.')[0].strip()
|
|
111
|
+
for fmt in ('%Y-%m-%d %H:%M', '%Y-%m-%d %H:%M:%S', '%Y-%m-%dT%H:%M:%S'):
|
|
112
|
+
try:
|
|
113
|
+
sync_dt = datetime.strptime(ts, fmt)
|
|
114
|
+
break
|
|
115
|
+
except ValueError:
|
|
116
|
+
continue
|
|
117
|
+
else:
|
|
118
|
+
sync_dt = None
|
|
119
|
+
if sync_dt:
|
|
120
|
+
age_days = (datetime.now() - sync_dt).days
|
|
121
|
+
if age_days > 30:
|
|
122
|
+
lines.append('[CRITICAL] 索引已 {} 天未更新! 搜索结果严重过期. 跑: python .qoder/scripts/git_sync.py'.format(age_days))
|
|
123
|
+
elif age_days > 7:
|
|
124
|
+
lines.append('[WARN] 索引已 {} 天未更新, 建议刷新: python .qoder/scripts/git_sync.py'.format(age_days))
|
|
125
|
+
except Exception:
|
|
126
|
+
pass # 日期解析失败不阻塞
|
|
127
|
+
if meta.get('failures'):
|
|
128
|
+
lines.append('WARNING: last sync had errors - see data/index/.index-meta.json')
|
|
129
|
+
|
|
130
|
+
return NL.join(lines)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_style_info():
|
|
134
|
+
"""Read style metadata for quick reference"""
|
|
135
|
+
meta = load_json_safe(os.path.join(BASE, 'data', 'index', 'style-meta.json'))
|
|
136
|
+
if not meta:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
lines = []
|
|
140
|
+
lines.append('## UI Style Index')
|
|
141
|
+
lines.append('Style search: python .qoder/scripts/search_index.py --style <table|form|modal>')
|
|
142
|
+
lines.append('Field search: python .qoder/scripts/search_index.py --field <name>')
|
|
143
|
+
|
|
144
|
+
for proj, d in meta.get('projects', {}).items():
|
|
145
|
+
vue = d.get('vue_files', 0)
|
|
146
|
+
if vue > 0:
|
|
147
|
+
pt = d.get('page_types', {})
|
|
148
|
+
types_str = ', '.join([k + ':' + str(v) for k, v in pt.items() if v > 0])
|
|
149
|
+
lines.append(' ' + proj + ': ' + str(vue) + ' Vue (' + types_str + ')')
|
|
150
|
+
|
|
151
|
+
lines.append('Top components: ' + ', '.join(list(meta.get('common_components', {}).keys())[:8]))
|
|
152
|
+
lines.append('Field map: ' + str(meta.get('field_count', 0)) + ' entries')
|
|
153
|
+
|
|
154
|
+
if os.path.isdir(os.path.join(BASE, 'data', 'style')):
|
|
155
|
+
lines.append('Style PDFs: data/style/ (design specs, code takes priority)')
|
|
156
|
+
|
|
157
|
+
return NL.join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def main():
|
|
161
|
+
parts = []
|
|
162
|
+
parts.append('<qoder-context>')
|
|
163
|
+
dev = get_developer()
|
|
164
|
+
if dev:
|
|
165
|
+
name = dev.get('name', 'unknown')
|
|
166
|
+
role = dev.get('role', 'unknown')
|
|
167
|
+
parts.append('Developer: ' + name + ' (' + role + ')')
|
|
168
|
+
parts.append('Workspace: workspace/members/' + name + '/')
|
|
169
|
+
else:
|
|
170
|
+
parts.append('Developer: NOT SET. Run /wl-init to register.')
|
|
171
|
+
team = get_team_size()
|
|
172
|
+
if team > 0:
|
|
173
|
+
parts.append('Team members: ' + str(team))
|
|
174
|
+
tasks = get_active_tasks()
|
|
175
|
+
if tasks:
|
|
176
|
+
parts.append('Active tasks:')
|
|
177
|
+
for t in tasks:
|
|
178
|
+
parts.append(' [' + t['status'] + '] ' + t['name'] + ' - ' + t['title'])
|
|
179
|
+
|
|
180
|
+
parts.append('')
|
|
181
|
+
parts.append(CRITICAL_RULE)
|
|
182
|
+
|
|
183
|
+
idx_info = get_index_info()
|
|
184
|
+
if idx_info:
|
|
185
|
+
parts.append('')
|
|
186
|
+
parts.append(idx_info)
|
|
187
|
+
|
|
188
|
+
style_info = get_style_info()
|
|
189
|
+
if style_info:
|
|
190
|
+
parts.append('')
|
|
191
|
+
parts.append(style_info)
|
|
192
|
+
|
|
193
|
+
parts.append('</qoder-context>')
|
|
194
|
+
print(NL.join(parts))
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
if __name__ == '__main__':
|
|
198
|
+
try:
|
|
199
|
+
main()
|
|
200
|
+
except Exception as e:
|
|
201
|
+
# A broken hook must never kill the session - emit minimal context
|
|
202
|
+
print('<qoder-context>')
|
|
203
|
+
print('session-start hook error: ' + str(e)[:200])
|
|
204
|
+
print('</qoder-context>')
|