@hupan56/wlkj 3.1.27 → 3.1.29

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 (37) hide show
  1. package/package.json +1 -1
  2. package/templates/qoder/commands/wl-task.md +11 -4
  3. package/templates/qoder/scripts/capability/smoke_test_report.json +12 -12
  4. package/templates/qoder/scripts/capability/smoke_test_report.json.new +94 -0
  5. package/templates/qoder/scripts/deployment/setup/init_doctor.py +20 -6
  6. package/templates/qoder/scripts/domain/report/report_snapshot.py +87 -6
  7. package/templates/qoder/scripts/domain/task/team_sync.py +34 -2
  8. package/templates/qoder/scripts/domain/task/zentao_sync.py +6 -5
  9. package/templates/qoder/scripts/foundation/integrations/zentao_client.py +38 -0
  10. package/templates/qoder/scripts/protocol/mcp/mcp_doctor.py +14 -2
  11. package/templates/qoder/scripts/protocol/mcp/zentao_mcp_server.py +211 -92
  12. package/templates/root/AGENTS.md +12 -12
  13. package/templates/qoder/contracts/CHANGELOG.md +0 -418
  14. package/templates/qoder/contracts/README.md +0 -184
  15. package/templates/qoder/contracts/code.md +0 -81
  16. package/templates/qoder/contracts/commit.md +0 -86
  17. package/templates/qoder/contracts/contract-header.md +0 -76
  18. package/templates/qoder/contracts/design.md +0 -106
  19. package/templates/qoder/contracts/fallback.md +0 -126
  20. package/templates/qoder/contracts/isolation.md +0 -119
  21. package/templates/qoder/contracts/prd.md +0 -118
  22. package/templates/qoder/contracts/schemas/design-spec.schema.json +0 -46
  23. package/templates/qoder/contracts/schemas/prd.schema.json +0 -36
  24. package/templates/qoder/contracts/schemas/test-cases.schema.json +0 -40
  25. package/templates/qoder/contracts/spec.md +0 -81
  26. package/templates/qoder/contracts/task.md +0 -125
  27. package/templates/qoder/contracts/test.md +0 -112
  28. package/templates/qoder/scripts/domain/task/__pycache__/syncgate.cpython-39.pyc +0 -0
  29. package/templates/qoder/scripts/foundation/__pycache__/__init__.cpython-39.pyc +0 -0
  30. package/templates/qoder/scripts/foundation/__pycache__/bootstrap.cpython-39.pyc +0 -0
  31. package/templates/qoder/scripts/foundation/core/__pycache__/__init__.cpython-39.pyc +0 -0
  32. package/templates/qoder/scripts/foundation/core/__pycache__/paths.cpython-39.pyc +0 -0
  33. package/templates/qoder/scripts/foundation/identity/__pycache__/__init__.cpython-39.pyc +0 -0
  34. package/templates/qoder/scripts/foundation/identity/__pycache__/identity.cpython-39.pyc +0 -0
  35. package/templates/qoder/scripts/foundation/io/__pycache__/__init__.cpython-39.pyc +0 -0
  36. package/templates/qoder/scripts/foundation/io/__pycache__/atomicio.cpython-39.pyc +0 -0
  37. 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 提取创建人账号 (openedBy 字段, 同 _assignee_account 的兼容逻辑)。
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 (兼容旧环境): 不校验, 返回 None (放行, 退回旧行为)。
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
- return None # 未配置归属不强制 (兼容)
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 _fetch_all_executions(self, limit=200):
234
- """全量拉执行列表 (默认 200, 覆盖团队历史执行量)。返回 [(id,name,status),...] None"""
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
- def _collect_my_tasks(self, status_filter=None):
245
- """全量扫所有执行, 收集指派给我(MY_ACCOUNT)的任务。
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
- Args:
248
- status_filter: 可选, 只保留该状态 (如 'wait'/'doing'); None=全部非终态。
249
- Returns:
250
- (任务列表, 执行映射) (None, 错误提示)。任务为统一 dict:
251
- {id,name,status,pri,execution,execution_name,estimate,left,deadline,story}
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
- emap = {eid: ename for eid, ename, _ in execs}
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
- mine.append(t2)
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
- r = _api('GET', '/products', None)
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
- r = _api('GET', '/products/%s/bugs' % pid, {'limit': 500})
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
- r = _api('GET', '/products', None)
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
- r = _api('GET', '/products/%s/stories' % pid, {'limit': 500})
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
- return '需求 #%s: %s\n状态: %s\n优先级: %s\n描述: %s' % (
1017
- sid, s.get('title', ''), s.get('status', ''),
1018
- s.get('pri', ''), str(s.get('spec', ''))[:200])
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', 'resolved'}
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') not in ('closed',)]
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",
@@ -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/report/eval_prd.py <prd> [prototype]`
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 (可用性)