@hupan56/wlkj 3.1.28 → 3.1.30
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 +9 -1
- package/package.json +1 -1
- package/templates/qoder/commands/wl-task.md +11 -4
- package/templates/qoder/scripts/capability/smoke_test_report.json +12 -12
- package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
- package/templates/qoder/scripts/domain/report/report_snapshot.py +87 -6
- package/templates/qoder/scripts/domain/task/team_sync.py +34 -2
- package/templates/qoder/scripts/domain/task/zentao_panel.py +331 -0
- package/templates/qoder/scripts/domain/task/zentao_sync.py +6 -5
- package/templates/qoder/scripts/foundation/integrations/zentao_client.py +38 -0
- package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +14 -2
- package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +211 -92
- package/templates/root/AGENTS.md +12 -12
- package/templates/qoder/contracts/CHANGELOG.md +0 -418
- package/templates/qoder/contracts/README.md +0 -184
- package/templates/qoder/contracts/code.md +0 -81
- package/templates/qoder/contracts/commit.md +0 -86
- package/templates/qoder/contracts/contract-header.md +0 -76
- package/templates/qoder/contracts/design.md +0 -106
- package/templates/qoder/contracts/fallback.md +0 -126
- package/templates/qoder/contracts/isolation.md +0 -119
- package/templates/qoder/contracts/prd.md +0 -118
- package/templates/qoder/contracts/schemas/design-spec.schema.json +0 -46
- package/templates/qoder/contracts/schemas/prd.schema.json +0 -36
- package/templates/qoder/contracts/schemas/test-cases.schema.json +0 -40
- package/templates/qoder/contracts/spec.md +0 -81
- package/templates/qoder/contracts/task.md +0 -125
- package/templates/qoder/contracts/test.md +0 -112
- package/templates/qoder/scripts/domain/task/__pycache__/syncgate.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/__pycache__/bootstrap.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/identity/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/identity/__pycache__/identity.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/io/__pycache__/__init__.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/io/__pycache__/atomicio.cpython-39.pyc +0 -0
- package/templates/qoder/scripts/foundation/io/__pycache__/filelock.cpython-39.pyc +0 -0
|
@@ -115,35 +115,13 @@ from foundation.protocol.mcp_base import BaseMCPServer, tool
|
|
|
115
115
|
|
|
116
116
|
|
|
117
117
|
def _assignee_account(item):
|
|
118
|
-
"""从任务/Bug 提取指派人账号 (
|
|
119
|
-
|
|
120
|
-
禅道 API 的 assignedTo 可能是:
|
|
121
|
-
- dict: {'id':14,'account':'zhengkang','realname':'郑康'} (列表接口常见)
|
|
122
|
-
- str: 'zhengkang' (详情接口常见)
|
|
123
|
-
- None/其它: 返回空串
|
|
124
|
-
旧代码用 str(dict)==account 比较, 永远匹配不上 → 过滤全失效。本函数统一取 account。
|
|
125
|
-
"""
|
|
126
|
-
a = item.get('assignedTo') if isinstance(item, dict) else None
|
|
127
|
-
if isinstance(a, dict):
|
|
128
|
-
return str(a.get('account', ''))
|
|
129
|
-
if isinstance(a, str):
|
|
130
|
-
return a
|
|
131
|
-
return ''
|
|
118
|
+
"""从任务/Bug 提取指派人账号 (转发 ZentaoClient, 单一真相源)。"""
|
|
119
|
+
return ZentaoClient.assignee_account(item)
|
|
132
120
|
|
|
133
121
|
|
|
134
122
|
def _openedby_account(item):
|
|
135
|
-
"""从任务/Bug 提取创建人账号 (
|
|
136
|
-
|
|
137
|
-
用途: Bug 的归属判定 —— 团队习惯提了 bug 不指派 (assignedTo=None), bug 留在创建人头上。
|
|
138
|
-
"我的 bug" 应包含 openedBy==我 (我提的), 不能只认 assignedTo。
|
|
139
|
-
openedBy 在禅道里通常是字符串 (账号), 兼容 dict 格式。
|
|
140
|
-
"""
|
|
141
|
-
o = item.get('openedBy') if isinstance(item, dict) else None
|
|
142
|
-
if isinstance(o, dict):
|
|
143
|
-
return str(o.get('account', ''))
|
|
144
|
-
if isinstance(o, str):
|
|
145
|
-
return o
|
|
146
|
-
return ''
|
|
123
|
+
"""从任务/Bug 提取创建人账号 (转发 ZentaoClient, 单一真相源)。"""
|
|
124
|
+
return ZentaoClient.openedby_account(item)
|
|
147
125
|
|
|
148
126
|
|
|
149
127
|
class ZentaoMCPServer(BaseMCPServer):
|
|
@@ -196,8 +174,9 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
196
174
|
"""归属校验 (写操作前调用): 查 item 详情, 校验归属。
|
|
197
175
|
|
|
198
176
|
防止误改队友任务/Bug。返回 None=放行, 字符串=拒绝原因。
|
|
199
|
-
- 未配置 MY_ACCOUNT
|
|
200
|
-
|
|
177
|
+
- 未配置 MY_ACCOUNT: fail-closed 拒绝 (对齐 _require_my_account; 共享账号下禅道
|
|
178
|
+
权限分不出谁, 未识别身份 = 无法判定归属 → 拒绝写, 防静默放行改队友数据)。
|
|
179
|
+
- 查询失败: 放行 (网络抖动是暂时的, 不锁死用户; 禅道侧权限仍有一层兜底)。
|
|
201
180
|
- 归属语义 (v3.1.6): task 只认 assignedTo==我; bug/story 认 assignedTo==我 OR openedBy==我
|
|
202
181
|
(原因: bug/story 创建后 assignedTo 常为空, 用户提的自己能 resolve/close 是合理的;
|
|
203
182
|
task 则不放宽 —— task 的 openedBy 是组长拆的, 归属以 assignedTo 为准)。
|
|
@@ -208,7 +187,9 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
208
187
|
item_id: 任务/Bug ID
|
|
209
188
|
"""
|
|
210
189
|
if not MY_ACCOUNT:
|
|
211
|
-
|
|
190
|
+
# fail-closed: 共享账号下无法判定归属 → 拒绝写, 防改错队友
|
|
191
|
+
return ('🚫 未识别禅道身份, 无法校验归属, 已拒绝写操作(防改错队友)。\n'
|
|
192
|
+
' 修复: /wl-init 配置禅道账号, 或 member.json 加 "zentao_account"。')
|
|
212
193
|
r = _api('GET', '/%ss/%s' % (kind, item_id))
|
|
213
194
|
if r.status_code not in (200, 201):
|
|
214
195
|
return None # 查询失败不阻塞 (禅道权限兜底)
|
|
@@ -230,31 +211,61 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
230
211
|
# 旧工作流只查最近几个执行 → 漏掉早期任务 (用户实测: 61 执行只查 12 个,
|
|
231
212
|
# 漏 9 个未开始任务)。这些辅助方法封装全量扫描, 供 my_workbench 等工具复用。
|
|
232
213
|
|
|
233
|
-
def
|
|
234
|
-
"""
|
|
235
|
-
r = _api('GET', '/executions', {'limit': limit})
|
|
236
|
-
if r.status_code not in (200, 201):
|
|
237
|
-
return None
|
|
238
|
-
data = r.json()
|
|
239
|
-
execs = data.get('executions') if isinstance(data, dict) else None
|
|
240
|
-
if not isinstance(execs, list):
|
|
241
|
-
return None
|
|
242
|
-
return [(e.get('id'), str(e.get('name', ''))[:20], e.get('status', '')) for e in execs]
|
|
214
|
+
def _paged_list(self, path, params, list_key, page_size=100, max_pages=20):
|
|
215
|
+
"""分页拉全量 (循环 page+limit, 按 id 去重防禅道不支持 page)。
|
|
243
216
|
|
|
244
|
-
|
|
245
|
-
|
|
217
|
+
禅道列表接口默认 limit≈20, 大集合超默认页就静默漏数据 (如产品>20 → 漏整个产品,
|
|
218
|
+
连带它的 bugs/stories 全漏)。本方法循环翻页, 读到不足一页或撞全重复即停。
|
|
219
|
+
禅道若不支持 page 参数(总返回第一页), 按 id 去重保证数据正确, 只是多几次冗余请求。
|
|
220
|
+
Returns: (合并去重列表, 是否截断[读满 max_pages 仍未到尾])。
|
|
221
|
+
"""
|
|
222
|
+
seen, all_items = set(), []
|
|
223
|
+
truncated = False
|
|
224
|
+
for page in range(1, max_pages + 1):
|
|
225
|
+
p = dict(params or {})
|
|
226
|
+
p.update({'limit': page_size, 'page': page})
|
|
227
|
+
r = _api('GET', path, p)
|
|
228
|
+
if r.status_code not in (200, 201):
|
|
229
|
+
break
|
|
230
|
+
data = r.json()
|
|
231
|
+
items = data.get(list_key) if isinstance(data, dict) else None
|
|
232
|
+
if not isinstance(items, list) or not items:
|
|
233
|
+
break
|
|
234
|
+
new = 0
|
|
235
|
+
for it in items:
|
|
236
|
+
if not isinstance(it, dict):
|
|
237
|
+
continue
|
|
238
|
+
key = it.get('id', id(it))
|
|
239
|
+
if key in seen:
|
|
240
|
+
continue
|
|
241
|
+
seen.add(key)
|
|
242
|
+
all_items.append(it)
|
|
243
|
+
new += 1
|
|
244
|
+
if new == 0:
|
|
245
|
+
break # 本页全重复(禅道不支持 page) → 停
|
|
246
|
+
if len(items) < page_size:
|
|
247
|
+
break # 不足一页 = 最后一页
|
|
248
|
+
else:
|
|
249
|
+
truncated = True # 读满 max_pages 仍未到尾
|
|
250
|
+
return all_items, truncated
|
|
246
251
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
+
def _fetch_all_executions(self):
|
|
253
|
+
"""全量拉执行列表 (循环翻页, 防漏)。返回 [(id,name,status),...] 或 None。"""
|
|
254
|
+
execs, _ = self._paged_list('/executions', None, 'executions')
|
|
255
|
+
out = [(e.get('id'), str(e.get('name', ''))[:20], e.get('status', ''))
|
|
256
|
+
for e in execs if isinstance(e, dict) and e.get('id') is not None]
|
|
257
|
+
return out or None
|
|
258
|
+
|
|
259
|
+
def _collect_all_tasks(self):
|
|
260
|
+
"""全量扫所有执行, 收集全部任务(不过滤归属)。
|
|
261
|
+
|
|
262
|
+
_collect_my_tasks(再过滤我的) 与 team_tasks_summary(全量聚合) 的共同遍历骨架(DRY)。
|
|
263
|
+
Returns: (任务列表, None) 或 (None, 错误)。每条带 execution/execution_name。
|
|
252
264
|
"""
|
|
253
265
|
execs = self._fetch_all_executions()
|
|
254
266
|
if execs is None:
|
|
255
267
|
return None, '查询执行列表失败, 稍后重试。'
|
|
256
|
-
|
|
257
|
-
mine = []
|
|
268
|
+
all_t = []
|
|
258
269
|
for eid, ename, _ in execs:
|
|
259
270
|
r = _api('GET', '/executions/%s/tasks' % eid, {'limit': 200})
|
|
260
271
|
if r.status_code not in (200, 201):
|
|
@@ -266,14 +277,29 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
266
277
|
for t in tasks:
|
|
267
278
|
if not isinstance(t, dict):
|
|
268
279
|
continue
|
|
269
|
-
if _assignee_account(t) != MY_ACCOUNT:
|
|
270
|
-
continue
|
|
271
|
-
if status_filter and t.get('status') != status_filter:
|
|
272
|
-
continue
|
|
273
280
|
t2 = dict(t)
|
|
274
281
|
t2['execution'] = eid
|
|
275
282
|
t2['execution_name'] = ename
|
|
276
|
-
|
|
283
|
+
all_t.append(t2)
|
|
284
|
+
return all_t, None
|
|
285
|
+
|
|
286
|
+
def _collect_my_tasks(self, status_filter=None):
|
|
287
|
+
"""指派给我(MY_ACCOUNT)的任务(在 _collect_all_tasks 基础上过滤归属)。
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
status_filter: 可选, 只保留该状态 (如 'wait'/'doing'); None=全部。
|
|
291
|
+
Returns: (任务列表, None) 或 (None, 错误)。
|
|
292
|
+
"""
|
|
293
|
+
all_t, err = self._collect_all_tasks()
|
|
294
|
+
if err:
|
|
295
|
+
return None, err
|
|
296
|
+
mine = []
|
|
297
|
+
for t in all_t:
|
|
298
|
+
if _assignee_account(t) != MY_ACCOUNT:
|
|
299
|
+
continue
|
|
300
|
+
if status_filter and t.get('status') != status_filter:
|
|
301
|
+
continue
|
|
302
|
+
mine.append(t)
|
|
277
303
|
return mine, None
|
|
278
304
|
|
|
279
305
|
def _collect_my_bugs(self):
|
|
@@ -285,23 +311,11 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
285
311
|
assignedTo=None → 旧逻辑 Bug=0, 用户"明明有 BUG"却查不到)。
|
|
286
312
|
返回 (bug列表, None) 或 (None, 错误)。bug dict 加 _why 字段标记归属来源。
|
|
287
313
|
"""
|
|
288
|
-
|
|
289
|
-
if r.status_code not in (200, 201):
|
|
290
|
-
return None, '查询产品列表失败。'
|
|
291
|
-
data = r.json()
|
|
292
|
-
prods = data.get('products') if isinstance(data, dict) else None
|
|
293
|
-
if not isinstance(prods, list):
|
|
294
|
-
return None, '产品列表响应异常。'
|
|
314
|
+
prods, _ = self._paged_list('/products', None, 'products')
|
|
295
315
|
mine = []
|
|
296
316
|
for p in prods:
|
|
297
317
|
pid = p.get('id')
|
|
298
|
-
|
|
299
|
-
if r.status_code not in (200, 201):
|
|
300
|
-
continue
|
|
301
|
-
data = r.json()
|
|
302
|
-
bugs = data.get('bugs') if isinstance(data, dict) else None
|
|
303
|
-
if not isinstance(bugs, list):
|
|
304
|
-
continue
|
|
318
|
+
bugs, _ = self._paged_list('/products/%s/bugs' % pid, None, 'bugs')
|
|
305
319
|
for b in bugs:
|
|
306
320
|
if not isinstance(b, dict):
|
|
307
321
|
continue
|
|
@@ -326,24 +340,11 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
326
340
|
只认 assignedTo 会让用户提的需求查不到 (同 Bug 的踩坑)。
|
|
327
341
|
返回 (story列表, None) 或 (None, 错误)。story dict 加 _why 字段标记来源。
|
|
328
342
|
"""
|
|
329
|
-
|
|
330
|
-
if r.status_code not in (200, 201):
|
|
331
|
-
return None, '查询产品列表失败。'
|
|
332
|
-
data = r.json()
|
|
333
|
-
prods = data.get('products') if isinstance(data, dict) else None
|
|
334
|
-
if not isinstance(prods, list):
|
|
335
|
-
return None, '产品列表响应异常。'
|
|
343
|
+
prods, _ = self._paged_list('/products', None, 'products')
|
|
336
344
|
mine = []
|
|
337
345
|
for p in prods:
|
|
338
346
|
pid = p.get('id')
|
|
339
|
-
|
|
340
|
-
# ★ 禅道列表接口可能返回 201 (Created), 不能只认 200
|
|
341
|
-
if r.status_code not in (200, 201):
|
|
342
|
-
continue
|
|
343
|
-
data = r.json()
|
|
344
|
-
sts = data.get('stories') if isinstance(data, dict) else None
|
|
345
|
-
if not isinstance(sts, list):
|
|
346
|
-
continue
|
|
347
|
+
sts, _ = self._paged_list('/products/%s/stories' % pid, None, 'stories')
|
|
347
348
|
for s in sts:
|
|
348
349
|
if not isinstance(s, dict):
|
|
349
350
|
continue
|
|
@@ -1013,9 +1014,13 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1013
1014
|
if r.status_code not in (200, 201):
|
|
1014
1015
|
return '查询失败: %s' % _err_text(r)
|
|
1015
1016
|
s = r.json()
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1017
|
+
asg = _assignee_account(s) or '空'
|
|
1018
|
+
opn = _openedby_account(s) or '空'
|
|
1019
|
+
return ('需求 #%s: %s\n状态: %s | 优先级: %s | 模块: %s | 计划: %s\n'
|
|
1020
|
+
'指派: %s | 创建: %s\n描述: %s') % (
|
|
1021
|
+
sid, s.get('title', ''), s.get('status', ''), s.get('pri', ''),
|
|
1022
|
+
s.get('module', ''), s.get('plan', ''),
|
|
1023
|
+
asg, opn, str(s.get('spec', ''))[:200])
|
|
1019
1024
|
|
|
1020
1025
|
@tool("create_story",
|
|
1021
1026
|
"在禅道创建需求(PRD发布→禅道需求)。权限不足会返回友好提示不报错。例: create_story(product_id=1, title='品质案件催办', spec='需求详情', pri=3, module=104)",
|
|
@@ -1142,6 +1147,9 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1142
1147
|
}, "required": ["story_id"]})
|
|
1143
1148
|
def _t_update_story(self, args):
|
|
1144
1149
|
sid = args.get('story_id', 0)
|
|
1150
|
+
guard = self._assert_owner('story', sid)
|
|
1151
|
+
if guard:
|
|
1152
|
+
return guard
|
|
1145
1153
|
payload = {}
|
|
1146
1154
|
for k in ('title', 'spec', 'pri'):
|
|
1147
1155
|
if args.get(k) is not None:
|
|
@@ -1160,6 +1168,9 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1160
1168
|
}, "required": ["story_id"]})
|
|
1161
1169
|
def _t_delete_story(self, args):
|
|
1162
1170
|
sid = args.get('story_id', 0)
|
|
1171
|
+
guard = self._assert_owner('story', sid)
|
|
1172
|
+
if guard:
|
|
1173
|
+
return guard
|
|
1163
1174
|
return self._safe_write(
|
|
1164
1175
|
'DELETE', '/stories/%s' % sid, None,
|
|
1165
1176
|
'✅ 需求 #%s 已删除' % sid,
|
|
@@ -1237,11 +1248,11 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1237
1248
|
t = r.json()
|
|
1238
1249
|
asg = _assignee_account(t) or '空'
|
|
1239
1250
|
opn = _openedby_account(t) or '空'
|
|
1240
|
-
return ('任务 #%s: %s\n状态: %s | 类型: %s | 优先级: %s\n'
|
|
1251
|
+
return ('任务 #%s: %s\n状态: %s | 类型: %s | 优先级: %s | 模块: %s\n'
|
|
1241
1252
|
'指派: %s | 创建: %s\n'
|
|
1242
1253
|
'工时: 预计=%s 消耗=%s 剩余=%s\n截止: %s\n需求: %s 执行: %s') % (
|
|
1243
1254
|
tid, t.get('name', ''), t.get('status', ''), t.get('type', ''),
|
|
1244
|
-
t.get('pri', ''), asg, opn,
|
|
1255
|
+
t.get('pri', ''), t.get('module', ''), asg, opn,
|
|
1245
1256
|
t.get('estimate', ''), t.get('consumed', ''), t.get('left', ''),
|
|
1246
1257
|
t.get('deadline', ''), t.get('story', ''), t.get('execution', ''))
|
|
1247
1258
|
|
|
@@ -1554,11 +1565,11 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1554
1565
|
b = r.json()
|
|
1555
1566
|
asg = _assignee_account(b) or '空'
|
|
1556
1567
|
opn = _openedby_account(b) or '空'
|
|
1557
|
-
return ('Bug #%s: %s\n状态: %s | 严重度: %s | 优先级: %s | 解决方案: %s\n'
|
|
1568
|
+
return ('Bug #%s: %s\n状态: %s | 严重度: %s | 优先级: %s | 解决方案: %s | 模块: %s\n'
|
|
1558
1569
|
'指派: %s | 创建: %s | 影响版本: %s | 解决版本: %s\n'
|
|
1559
1570
|
'关联需求: %s | 类型: %s\n描述: %s') % (
|
|
1560
1571
|
bid, b.get('title', ''), b.get('status', ''),
|
|
1561
|
-
b.get('severity', ''), b.get('pri', ''), b.get('resolution', ''),
|
|
1572
|
+
b.get('severity', ''), b.get('pri', ''), b.get('resolution', ''), b.get('module', ''),
|
|
1562
1573
|
asg, opn, b.get('openedBuild', ''), b.get('resolvedBuild', ''),
|
|
1563
1574
|
b.get('story', ''),
|
|
1564
1575
|
b.get('type', ''), str(b.get('steps', ''))[:200])
|
|
@@ -1609,6 +1620,9 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1609
1620
|
}, "required": ["bug_id"]})
|
|
1610
1621
|
def _t_update_bug(self, args):
|
|
1611
1622
|
bid = args.get('bug_id', 0)
|
|
1623
|
+
guard = self._assert_owner('bug', bid)
|
|
1624
|
+
if guard:
|
|
1625
|
+
return guard
|
|
1612
1626
|
payload = {}
|
|
1613
1627
|
for k in ('title', 'severity', 'pri', 'openedBuild'):
|
|
1614
1628
|
if args.get(k) is not None:
|
|
@@ -1627,6 +1641,9 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1627
1641
|
}, "required": ["bug_id"]})
|
|
1628
1642
|
def _t_delete_bug(self, args):
|
|
1629
1643
|
bid = args.get('bug_id', 0)
|
|
1644
|
+
guard = self._assert_owner('bug', bid)
|
|
1645
|
+
if guard:
|
|
1646
|
+
return guard
|
|
1630
1647
|
return self._safe_write(
|
|
1631
1648
|
'DELETE', '/bugs/%s' % bid, None,
|
|
1632
1649
|
'✅ Bug #%s 已删除' % bid,
|
|
@@ -1744,13 +1761,16 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1744
1761
|
if serr:
|
|
1745
1762
|
return serr
|
|
1746
1763
|
|
|
1747
|
-
# 过滤掉终态 (除非显式要看历史)
|
|
1764
|
+
# 过滤掉终态 (除非显式要看历史)。三域对称: 都用正向白名单,
|
|
1765
|
+
# 对齐 task 的语义 (完成即消失)。旧版 bug 把 resolved 算活动、
|
|
1766
|
+
# story 把 draft 算活动 → 计数虚高, 与"我手头还要干的活"直觉不符。
|
|
1748
1767
|
LIVE_TASK = {'wait', 'doing', 'pause'}
|
|
1749
|
-
LIVE_BUG = {'active',
|
|
1768
|
+
LIVE_BUG = {'active'} # resolved=已解决待验证, 像任务 done, 剔除
|
|
1769
|
+
LIVE_STORY = {'active', 'reviewing', 'changing'} # 草稿/终态剔除
|
|
1750
1770
|
if not include_closed:
|
|
1751
1771
|
tasks = [t for t in tasks if t.get('status') in LIVE_TASK]
|
|
1752
1772
|
bugs = [b for b in bugs if b.get('status') in LIVE_BUG]
|
|
1753
|
-
stories = [s for s in stories if s.get('status')
|
|
1773
|
+
stories = [s for s in stories if s.get('status') in LIVE_STORY]
|
|
1754
1774
|
|
|
1755
1775
|
lines = ['📋 我(%s)的禅道工作台 — 任务 %d · 需求 %d · Bug %d' % (
|
|
1756
1776
|
MY_ACCOUNT, len(tasks), len(stories), len(bugs))]
|
|
@@ -1877,6 +1897,105 @@ class ZentaoMCPServer(BaseMCPServer):
|
|
|
1877
1897
|
lines.append('\n💡 说"看Bug#XXXX"或"Bug解决了"')
|
|
1878
1898
|
return '\n'.join(lines)
|
|
1879
1899
|
|
|
1900
|
+
@tool("team_tasks_summary",
|
|
1901
|
+
"团队任务聚合(跨执行全量, 不漏)。复用全量遍历, 一个工具覆盖三场景。"
|
|
1902
|
+
"mode: team=按人聚合(doing/wait/done计数+工时, 站会用); "
|
|
1903
|
+
"overdue=超期清单(deadline<今天且未完, 风险排查); "
|
|
1904
|
+
"by_execution=按执行完成率(PMO看进度)。例: team_tasks_summary() / team_tasks_summary(mode='overdue')",
|
|
1905
|
+
{"type": "object", "properties": {
|
|
1906
|
+
"mode": {"type": "string", "enum": ["team", "overdue", "by_execution"],
|
|
1907
|
+
"description": "聚合模式, 默认 team"},
|
|
1908
|
+
}, "required": []})
|
|
1909
|
+
def _t_team_tasks_summary(self, args):
|
|
1910
|
+
gate = self._gate()
|
|
1911
|
+
if gate:
|
|
1912
|
+
return gate
|
|
1913
|
+
mode = args.get('mode') or 'team'
|
|
1914
|
+
if mode not in ('team', 'overdue', 'by_execution'):
|
|
1915
|
+
return '⚠️ mode 只支持 team / overdue / by_execution。'
|
|
1916
|
+
all_t, err = self._collect_all_tasks()
|
|
1917
|
+
if err:
|
|
1918
|
+
return err
|
|
1919
|
+
if not all_t:
|
|
1920
|
+
return '没有查到任何任务(执行列表为空)。'
|
|
1921
|
+
if mode == 'overdue':
|
|
1922
|
+
return self._fmt_overdue(all_t)
|
|
1923
|
+
if mode == 'by_execution':
|
|
1924
|
+
return self._fmt_by_execution(all_t)
|
|
1925
|
+
return self._fmt_team(all_t)
|
|
1926
|
+
|
|
1927
|
+
def _fmt_team(self, tasks):
|
|
1928
|
+
"""按人聚合: 每人的 doing/wait/done 计数 + 消耗/预计工时。"""
|
|
1929
|
+
from collections import defaultdict
|
|
1930
|
+
by_acc = defaultdict(lambda: {'doing': 0, 'wait': 0, 'done': 0, 'consumed': 0.0, 'estimate': 0.0})
|
|
1931
|
+
for t in tasks:
|
|
1932
|
+
acc = _assignee_account(t) or '(未指派)'
|
|
1933
|
+
st = t.get('status', '')
|
|
1934
|
+
rec = by_acc[acc]
|
|
1935
|
+
if st == 'doing':
|
|
1936
|
+
rec['doing'] += 1
|
|
1937
|
+
elif st in ('wait', 'pause'):
|
|
1938
|
+
rec['wait'] += 1
|
|
1939
|
+
elif st in ('done', 'closed'):
|
|
1940
|
+
rec['done'] += 1
|
|
1941
|
+
for k in ('consumed', 'estimate'):
|
|
1942
|
+
try:
|
|
1943
|
+
rec[k] += float(t.get(k) or 0)
|
|
1944
|
+
except (TypeError, ValueError):
|
|
1945
|
+
pass
|
|
1946
|
+
rows = sorted(by_acc.items(), key=lambda kv: -(kv[1]['doing'] + kv[1]['wait']))
|
|
1947
|
+
lines = ['👥 团队任务聚合 (共 %d 个任务, %d 人)' % (len(tasks), len(by_acc))]
|
|
1948
|
+
lines.append('\n 账号 进行 未开始 已完成 消耗h/预计h')
|
|
1949
|
+
for acc, r in rows:
|
|
1950
|
+
lines.append(' %-12s %4d %4d %4d %5.1f/%5.1f' % (
|
|
1951
|
+
acc[:12], r['doing'], r['wait'], r['done'], r['consumed'], r['estimate']))
|
|
1952
|
+
lines.append('\n💡 说 "team_tasks_summary mode=overdue" 看超期, 或 mode=by_execution 看执行进度')
|
|
1953
|
+
return '\n'.join(lines)
|
|
1954
|
+
|
|
1955
|
+
def _fmt_overdue(self, tasks):
|
|
1956
|
+
"""超期清单: deadline < 今天 且 未到终态。"""
|
|
1957
|
+
from datetime import datetime
|
|
1958
|
+
today = datetime.now().strftime('%Y-%m-%d')
|
|
1959
|
+
overdue = [t for t in tasks
|
|
1960
|
+
if t.get('status') not in ('done', 'closed', 'cancel', 'cancelled')
|
|
1961
|
+
and (t.get('deadline') or '')[:10] < today
|
|
1962
|
+
and (t.get('deadline') or '')[:10]]
|
|
1963
|
+
if not overdue:
|
|
1964
|
+
return '✅ 没有超期任务 (deadline<%s 且未完的共 0 个)。' % today
|
|
1965
|
+
overdue.sort(key=lambda t: (t.get('deadline') or '')[:10])
|
|
1966
|
+
lines = ['🔴 超期任务清单 (deadline<%s 且未完, 共 %d 个):' % (today, len(overdue))]
|
|
1967
|
+
for t in overdue:
|
|
1968
|
+
lines.append(' [#%s] %s 截止=%s 状态=%s 指派=%s 执行=%s' % (
|
|
1969
|
+
t.get('id'), str(t.get('name', ''))[:30], (t.get('deadline') or '')[:10],
|
|
1970
|
+
t.get('status'), _assignee_account(t) or '空', t.get('execution_name', '')))
|
|
1971
|
+
lines.append('\n💡 说 "team_tasks_summary mode=team" 看每人手头活')
|
|
1972
|
+
return '\n'.join(lines)
|
|
1973
|
+
|
|
1974
|
+
def _fmt_by_execution(self, tasks):
|
|
1975
|
+
"""按执行聚合: 完成率 + 工时。"""
|
|
1976
|
+
from collections import defaultdict
|
|
1977
|
+
by_exec = defaultdict(lambda: {'total': 0, 'done': 0, 'consumed': 0.0, 'estimate': 0.0})
|
|
1978
|
+
for t in tasks:
|
|
1979
|
+
name = t.get('execution_name') or str(t.get('execution', '?'))
|
|
1980
|
+
rec = by_exec[name]
|
|
1981
|
+
rec['total'] += 1
|
|
1982
|
+
if t.get('status') in ('done', 'closed'):
|
|
1983
|
+
rec['done'] += 1
|
|
1984
|
+
for k in ('consumed', 'estimate'):
|
|
1985
|
+
try:
|
|
1986
|
+
rec[k] += float(t.get(k) or 0)
|
|
1987
|
+
except (TypeError, ValueError):
|
|
1988
|
+
pass
|
|
1989
|
+
rows = sorted(by_exec.items(), key=lambda kv: -kv[1]['total'])
|
|
1990
|
+
lines = ['📊 执行完成率 (共 %d 个执行, %d 个任务)' % (len(by_exec), len(tasks))]
|
|
1991
|
+
lines.append('\n 执行 完成/总数 完成率 消耗h/预计h')
|
|
1992
|
+
for name, r in rows:
|
|
1993
|
+
rate = (r['done'] * 100 // r['total']) if r['total'] else 0
|
|
1994
|
+
lines.append(' %-18s %4d/%4d %3d%% %6.1f/%6.1f' % (
|
|
1995
|
+
name[:18], r['done'], r['total'], rate, r['consumed'], r['estimate']))
|
|
1996
|
+
lines.append('\n💡 说 "team_tasks_summary mode=overdue" 看风险任务')
|
|
1997
|
+
return '\n'.join(lines)
|
|
1998
|
+
|
|
1880
1999
|
# ── 用户 User (2) ── (指派任务/Bug 必须知道有哪些账号) ─────────────
|
|
1881
2000
|
|
|
1882
2001
|
@tool("list_users",
|
package/templates/root/AGENTS.md
CHANGED
|
@@ -76,8 +76,8 @@ Examples:
|
|
|
76
76
|
|
|
77
77
|
PMs on this team do not know git. ALL git operations are invisible:
|
|
78
78
|
- After any publish/save action (/wl-prd publish, /wl-task create/finish,
|
|
79
|
-
/wl-report save), run: `python .qoder/scripts/task/team_sync.py push`
|
|
80
|
-
- To get teammates' latest work: `python .qoder/scripts/task/team_sync.py pull`
|
|
79
|
+
/wl-report save), run: `python .qoder/scripts/domain/task/team_sync.py push`
|
|
80
|
+
- To get teammates' latest work: `python .qoder/scripts/domain/task/team_sync.py pull`
|
|
81
81
|
- If team_sync prints SYNC_CONFLICT, YOU (the AI) resolve the conflict
|
|
82
82
|
yourself (it tells you the commands). NEVER ask the user to run git.
|
|
83
83
|
- When the user says 同步/保存/提交 in natural language -> team_sync, not raw git.
|
|
@@ -135,7 +135,7 @@ app/mobile/h5/移动端 -> Carmg-H5
|
|
|
135
135
|
|
|
136
136
|
3 projects: Carmg-H5(1378 files), fywl-ics(8973 files, 3903 APIs), fywl-ui(3270 files)
|
|
137
137
|
Search: /wl-search <keyword> returns compact results (~200 bytes)
|
|
138
|
-
Weekly sync: python .qoder/scripts/task/git_sync.py
|
|
138
|
+
Weekly sync: python .qoder/scripts/domain/task/git_sync.py
|
|
139
139
|
|
|
140
140
|
### MCP Servers (QoderWork / Qoder IDE)
|
|
141
141
|
注册在 `~/.qoderwork/mcp.json`,setup.py 一键配齐:
|
|
@@ -144,7 +144,7 @@ Weekly sync: python .qoder/scripts/task/git_sync.py
|
|
|
144
144
|
- `qoder-mysql` (stdio) — 只读查真实数据库结构/数据。自动拉起。
|
|
145
145
|
- `lanhu` (STDIO) — 蓝湖设计稿直读,设计师发链接 AI 提取颜色/尺寸/字体/切图。
|
|
146
146
|
**STDIO 模式:开 QoderWork 自动起、关自动停**(QoderWork 原生进程管理,无需手动 start)。
|
|
147
|
-
cookie 按角色隔离(UI 能改稿/PM 只读):wrapper 读当前 `.developer` → 该角色的 `workspace/members/{dev}/.secrets/lanhu.env`(gitignored)。换角色改 `.developer` + 配该角色 cookie → 重启 QoderWork。首次配置跑 `python .qoder/scripts/setup/setup_lanhu.py`。
|
|
147
|
+
cookie 按角色隔离(UI 能改稿/PM 只读):wrapper 读当前 `.developer` → 该角色的 `workspace/members/{dev}/.secrets/lanhu.env`(gitignored)。换角色改 `.developer` + 配该角色 cookie → 重启 QoderWork。首次配置跑 `python .qoder/scripts/deployment/setup/setup_lanhu.py`。
|
|
148
148
|
|
|
149
149
|
### Style Index (Prototype Reference)
|
|
150
150
|
UI patterns from Vue files: Tables(718), Forms(301), Detail(129), Dashboard(90)
|
|
@@ -153,10 +153,10 @@ Form components: Input(2262), Select(1049), ApiSelect(725), DatePicker(265)
|
|
|
153
153
|
Field map: 2383 entries
|
|
154
154
|
|
|
155
155
|
Style search:
|
|
156
|
-
python .qoder/scripts/kg/search_index.py --style table/form/modal/dashboard
|
|
157
|
-
python .qoder/scripts/kg/search_index.py --field <field_name>
|
|
158
|
-
python .qoder/scripts/kg/search_index.py --components
|
|
159
|
-
python .qoder/scripts/kg/search_index.py --vben # Vben Admin CSS variables
|
|
156
|
+
python .qoder/scripts/domain/kg/search/search_index.py --style table/form/modal/dashboard
|
|
157
|
+
python .qoder/scripts/domain/kg/search/search_index.py --field <field_name>
|
|
158
|
+
python .qoder/scripts/domain/kg/search/search_index.py --components
|
|
159
|
+
python .qoder/scripts/domain/kg/search/search_index.py --vben # Vben Admin CSS variables
|
|
160
160
|
|
|
161
161
|
Prototype templates:
|
|
162
162
|
.qoder/templates/prototype-web.html # Web管理端 (Vben Admin + Ant Design)
|
|
@@ -191,7 +191,7 @@ Each product loads rules from a different carrier — keep them in sync:
|
|
|
191
191
|
| QoderWork 桌面 Agent | 安装 .qoder/skills/ 下的技能 | 自然语言 -> skills |
|
|
192
192
|
|
|
193
193
|
> 本流水线聚焦 Qoder 全系列,4 种载体功能等价。知识图谱 17 个能力(影响分析/
|
|
194
|
-
> 覆盖矩阵/功能画像/业务流程/多跳遍历…)通过统一 CLI `python .qoder/scripts/kg/kg.py`
|
|
194
|
+
> 覆盖矩阵/功能画像/业务流程/多跳遍历…)通过统一 CLI `python .qoder/scripts/domain/kg/kg.py`
|
|
195
195
|
> 全系列触达;QoderWork 环境也可直接调 MCP 工具。
|
|
196
196
|
|
|
197
197
|
## QoderWork Integration: Optional Enhancement Only (Progressive Enhancement)
|
|
@@ -222,11 +222,11 @@ Each product loads rules from a different carrier — keep them in sync:
|
|
|
222
222
|
macOS/Linux -> scripts/setup_weekly_cron.sh. Only ONE machine per team
|
|
223
223
|
needs this; everyone else gets the graph via /wl-init's auto pull.
|
|
224
224
|
- Environment problems of any kind -> run:
|
|
225
|
-
`python .qoder/scripts/setup/init_doctor.py` (add --fix to auto-repair).
|
|
225
|
+
`python .qoder/scripts/deployment/setup/init_doctor.py` (add --fix to auto-repair).
|
|
226
226
|
|
|
227
227
|
## Fast Path: one call to get all context
|
|
228
228
|
|
|
229
|
-
`python .qoder/scripts/kg/context_pack.py <业务词> --platform <web|app>` returns
|
|
229
|
+
`python .qoder/scripts/domain/kg/search/context_pack.py <业务词> --platform <web|app>` returns
|
|
230
230
|
code files + page examples + field specs + PRDs + APIs + style/icon pointers
|
|
231
231
|
in ONE call. Use it FIRST; fall back to search_index.py for drill-down.
|
|
232
232
|
|
|
@@ -237,7 +237,7 @@ Web = Ant Design inline SVG, APP = Vant font icons. Emoji in a prototype = FAIL.
|
|
|
237
237
|
|
|
238
238
|
## Quality Gate (EVA)
|
|
239
239
|
|
|
240
|
-
Before publishing any PRD: `python .qoder/scripts/
|
|
240
|
+
Before publishing any PRD: `python .qoder/scripts/validation/metrics/eval_prd.py <prd> [prototype]`
|
|
241
241
|
must PASS (>=80%). Metrics definition: data/docs/constitution/eva-metrics.md
|
|
242
242
|
|
|
243
243
|
## Workflow Usability (可用性)
|