@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.
- 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/deployment/setup/init_doctor.py +20 -6
- 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_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
package/package.json
CHANGED
|
@@ -43,14 +43,21 @@ PY=$(python --version >/dev/null 2>&1 && echo python || echo python3)
|
|
|
43
43
|
- **建任务**:`create_task` 的 `assignedTo` **默认指派给当前开发者**(从 member.json 的 zentao_account),显式传参才派别人。
|
|
44
44
|
- 隔离红线见 `contracts/isolation.md`「禅道同步」。
|
|
45
45
|
|
|
46
|
-
### 第 0
|
|
47
|
-
|
|
46
|
+
### 第 0 步:按操作类型决定要不要探测禅道
|
|
47
|
+
|
|
48
|
+
禅道工具自带内网/凭据门(不通返回友好提示)。**读类操作直接调,省掉探测往返**:
|
|
49
|
+
|
|
50
|
+
| 操作类型 | 探测? | 走法 |
|
|
51
|
+
|---------|:----:|------|
|
|
52
|
+
| **读**(看我的活/列任务/看详情/Bug状态) | ❌ 不探测 | 直接 `my_workbench`/`list_my_tasks`/`get_*`,门会兜底 |
|
|
53
|
+
| **写**(create/start/finish/assign/resolve/改/删) | ✅ 探测 | 先 `check_zentao_status`,或写前 `get_*` 确认归属时顺手探,避免写一半断网 |
|
|
54
|
+
| 用户明说"离线/先存本地" | — | 即使可达也走本地模式 |
|
|
55
|
+
|
|
48
56
|
- **✅ 可达** → 走**禅道模式**(下方各动作的「禅道走法」)
|
|
49
57
|
- **❌ 不可达 / 未配置** → 走**本地模式**(下方各动作的原有 `task.py` 走法,离线兜底,不报错不阻塞)
|
|
50
|
-
- **用户明确说"离线"/"先存本地"** → 即使禅道可达也走本地模式
|
|
51
58
|
|
|
52
59
|
> 这套判定让禅道有就同步、没内网就静默回退 —— 符合 AGENTS.md「本地数据是唯一事实源、连接器是可选增强、断网不阻塞」原则。
|
|
53
|
-
>
|
|
60
|
+
> ★ 高频的"看我的活"现在是 **1 次调用直达**(旧版 probe + workbench 两次),思考链更短。
|
|
54
61
|
|
|
55
62
|
### 列表勾选式交互(禅道模式的统一范式)
|
|
56
63
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
|
-
"timestamp": "2026-06-
|
|
2
|
+
"timestamp": "2026-06-29 11:20:28",
|
|
3
3
|
"servers": {
|
|
4
4
|
"qoder-knowledge-graph": {
|
|
5
5
|
"available": true,
|
|
6
6
|
"tools_indexed": 17,
|
|
7
|
-
"latency_ms":
|
|
7
|
+
"latency_ms": 484.0,
|
|
8
8
|
"sample_tools": [
|
|
9
9
|
"search_code",
|
|
10
10
|
"search_api",
|
|
@@ -12,13 +12,13 @@
|
|
|
12
12
|
"get_impact",
|
|
13
13
|
"context_360"
|
|
14
14
|
],
|
|
15
|
-
"spawn_pid":
|
|
15
|
+
"spawn_pid": 66880,
|
|
16
16
|
"status": "PASS"
|
|
17
17
|
},
|
|
18
18
|
"qoder-zentao": {
|
|
19
19
|
"available": true,
|
|
20
|
-
"tools_indexed":
|
|
21
|
-
"latency_ms":
|
|
20
|
+
"tools_indexed": 68,
|
|
21
|
+
"latency_ms": 766.0,
|
|
22
22
|
"sample_tools": [
|
|
23
23
|
"check_zentao_status",
|
|
24
24
|
"get_zentao_url",
|
|
@@ -26,13 +26,13 @@
|
|
|
26
26
|
"get_product",
|
|
27
27
|
"create_product"
|
|
28
28
|
],
|
|
29
|
-
"spawn_pid":
|
|
29
|
+
"spawn_pid": 48620,
|
|
30
30
|
"status": "PASS"
|
|
31
31
|
},
|
|
32
32
|
"qoder-mysql": {
|
|
33
33
|
"available": true,
|
|
34
34
|
"tools_indexed": 6,
|
|
35
|
-
"latency_ms":
|
|
35
|
+
"latency_ms": 609.0,
|
|
36
36
|
"sample_tools": [
|
|
37
37
|
"list_envs",
|
|
38
38
|
"query_schema",
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
"query_distinct",
|
|
41
41
|
"list_databases"
|
|
42
42
|
],
|
|
43
|
-
"spawn_pid":
|
|
43
|
+
"spawn_pid": 67572,
|
|
44
44
|
"status": "PASS"
|
|
45
45
|
},
|
|
46
46
|
"lanhu": {
|
|
47
47
|
"available": true,
|
|
48
48
|
"tools_indexed": 13,
|
|
49
|
-
"latency_ms":
|
|
49
|
+
"latency_ms": 3313.0,
|
|
50
50
|
"sample_tools": [
|
|
51
51
|
"lanhu_resolve_invite_link",
|
|
52
52
|
"lanhu_list_product_documents",
|
|
@@ -54,13 +54,13 @@
|
|
|
54
54
|
"lanhu_get_ai_analyze_page_result",
|
|
55
55
|
"lanhu_get_designs"
|
|
56
56
|
],
|
|
57
|
-
"spawn_pid":
|
|
57
|
+
"spawn_pid": 32460,
|
|
58
58
|
"status": "PASS"
|
|
59
59
|
}
|
|
60
60
|
},
|
|
61
61
|
"call_tool": {
|
|
62
62
|
"status": "PASS",
|
|
63
|
-
"latency_ms":
|
|
63
|
+
"latency_ms": 3547.0,
|
|
64
64
|
"text_preview": "Chinese: 保险 -> English: ins insurance vehlife/insurance\n\n[Carmg-H5] 12 files\n Carmg-H5/src/.../main/List.vue -> data/code/Carmg-H5/src/modules/icsAbnormalCenter/module/main/List.vue\n Carmg-H5/src/",
|
|
65
65
|
"is_error": false
|
|
66
66
|
},
|
|
@@ -89,6 +89,6 @@
|
|
|
89
89
|
"all_pass": true,
|
|
90
90
|
"active_servers": 4,
|
|
91
91
|
"total_servers": 4,
|
|
92
|
-
"total_tools":
|
|
92
|
+
"total_tools": 104
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"timestamp": "2026-06-26 21:25:55",
|
|
3
|
+
"servers": {
|
|
4
|
+
"qoder-knowledge-graph": {
|
|
5
|
+
"available": true,
|
|
6
|
+
"tools_indexed": 17,
|
|
7
|
+
"latency_ms": 437.0,
|
|
8
|
+
"sample_tools": [
|
|
9
|
+
"search_code",
|
|
10
|
+
"search_api",
|
|
11
|
+
"search_prd",
|
|
12
|
+
"get_impact",
|
|
13
|
+
"context_360"
|
|
14
|
+
],
|
|
15
|
+
"spawn_pid": 38616,
|
|
16
|
+
"status": "PASS"
|
|
17
|
+
},
|
|
18
|
+
"qoder-zentao": {
|
|
19
|
+
"available": true,
|
|
20
|
+
"tools_indexed": 66,
|
|
21
|
+
"latency_ms": 3657.0,
|
|
22
|
+
"sample_tools": [
|
|
23
|
+
"check_zentao_status",
|
|
24
|
+
"get_zentao_url",
|
|
25
|
+
"list_products",
|
|
26
|
+
"get_product",
|
|
27
|
+
"create_product"
|
|
28
|
+
],
|
|
29
|
+
"spawn_pid": 11224,
|
|
30
|
+
"status": "PASS"
|
|
31
|
+
},
|
|
32
|
+
"qoder-mysql": {
|
|
33
|
+
"available": true,
|
|
34
|
+
"tools_indexed": 6,
|
|
35
|
+
"latency_ms": 3484.0,
|
|
36
|
+
"sample_tools": [
|
|
37
|
+
"list_envs",
|
|
38
|
+
"query_schema",
|
|
39
|
+
"query_data",
|
|
40
|
+
"query_distinct",
|
|
41
|
+
"list_databases"
|
|
42
|
+
],
|
|
43
|
+
"spawn_pid": 37868,
|
|
44
|
+
"status": "PASS"
|
|
45
|
+
},
|
|
46
|
+
"lanhu": {
|
|
47
|
+
"available": true,
|
|
48
|
+
"tools_indexed": 13,
|
|
49
|
+
"latency_ms": 3156.0,
|
|
50
|
+
"sample_tools": [
|
|
51
|
+
"lanhu_resolve_invite_link",
|
|
52
|
+
"lanhu_list_product_documents",
|
|
53
|
+
"lanhu_get_pages",
|
|
54
|
+
"lanhu_get_ai_analyze_page_result",
|
|
55
|
+
"lanhu_get_designs"
|
|
56
|
+
],
|
|
57
|
+
"spawn_pid": 36836,
|
|
58
|
+
"status": "PASS"
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"call_tool": {
|
|
62
|
+
"status": "PASS",
|
|
63
|
+
"latency_ms": 3953.0,
|
|
64
|
+
"text_preview": "Chinese: 保险 -> English: ins insurance vehlife/insurance\n\n[Carmg-H5] 12 files\n Carmg-H5/src/.../main/List.vue -> data/code/Carmg-H5/src/modules/icsAbnormalCenter/module/main/List.vue\n Carmg-H5/src/",
|
|
65
|
+
"is_error": false
|
|
66
|
+
},
|
|
67
|
+
"memory": {
|
|
68
|
+
"available": true,
|
|
69
|
+
"providers": [
|
|
70
|
+
{
|
|
71
|
+
"provider": "McpMemoryProvider",
|
|
72
|
+
"available": true,
|
|
73
|
+
"hit": true
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"provider": "FileMemoryCap",
|
|
77
|
+
"available": true,
|
|
78
|
+
"hit": false
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"provider": "NoOpMemoryCap",
|
|
82
|
+
"available": false,
|
|
83
|
+
"hit": false
|
|
84
|
+
}
|
|
85
|
+
],
|
|
86
|
+
"status": "PASS"
|
|
87
|
+
},
|
|
88
|
+
"summary": {
|
|
89
|
+
"all_pass": true,
|
|
90
|
+
"active_servers": 4,
|
|
91
|
+
"total_servers": 4,
|
|
92
|
+
"total_tools": 102
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -485,19 +485,33 @@ def check_index(fix):
|
|
|
485
485
|
print('\n--- 5. 知识图谱 (data/index/) ---')
|
|
486
486
|
age = index_age_days()
|
|
487
487
|
if age is None:
|
|
488
|
-
# 完全缺失
|
|
488
|
+
# 完全缺失 → 先尝试 team_sync pull 拉管理员建好的 kg.duckdb
|
|
489
|
+
if fix:
|
|
490
|
+
print(' 索引缺失, 先从团队仓库拉取 (team_sync pull)...')
|
|
491
|
+
if run_script('team_sync.py', 'pull', timeout=120):
|
|
492
|
+
# pull 成功, 检查 kg.duckdb 是否拉到了
|
|
493
|
+
age2 = index_age_days()
|
|
494
|
+
if age2 is not None:
|
|
495
|
+
say(OK, '已从团队仓库拉取知识图谱 ({:.1f} 天前构建)'.format(age2))
|
|
496
|
+
actions.append('从团队拉取知识图谱')
|
|
497
|
+
return
|
|
498
|
+
# pull 了但 kg.duckdb 还是没到(管理员还没建过?)
|
|
499
|
+
print(' team_sync pull 完成, 但 kg.duckdb 未在仓库中')
|
|
500
|
+
else:
|
|
501
|
+
say(WARN, 'team_sync pull 失败 (离线? 冲突?)')
|
|
502
|
+
|
|
503
|
+
# pull 失败或没拉到 → 降级: 有源码就本地全量构建
|
|
489
504
|
if _has_source_code():
|
|
490
|
-
# 有源码 → 可以建索引
|
|
491
505
|
if fix:
|
|
492
|
-
print('
|
|
506
|
+
print(' 降级: 本地全量构建 (一次性, 之后都是增量) ...')
|
|
493
507
|
if run_script('git_sync.py', '--index-only'):
|
|
494
|
-
actions.append('全量构建知识图谱')
|
|
508
|
+
actions.append('全量构建知识图谱 (本地降级)')
|
|
495
509
|
else:
|
|
496
510
|
problem('索引构建失败, 查看上方报错')
|
|
497
511
|
else:
|
|
498
|
-
problem('知识图谱不存在', '加 --fix
|
|
512
|
+
problem('知识图谱不存在', '加 --fix: 先拉团队, 拉不到再本地构建')
|
|
499
513
|
else:
|
|
500
|
-
# 无源码 →
|
|
514
|
+
# 无源码 → 建不了索引
|
|
501
515
|
problem('索引缺失且无源码 (data/code/ 为空)',
|
|
502
516
|
'无法构建知识图谱。看下方"降级状态"说明, 配源码后重跑 init')
|
|
503
517
|
return
|
|
@@ -11,6 +11,7 @@ report_snapshot.py — /wl-report 的一次性数据聚合器 (性能优化)
|
|
|
11
11
|
3. journal/feedback — 今天的学习/反馈事件
|
|
12
12
|
4. git log --author — 我的提交 (扫 data/code/ 各项目)
|
|
13
13
|
5. drafts/ + specs/prd — 我写的 PRD/草稿
|
|
14
|
+
6. zentao 我的任务 — 禅道真实工时(consumed), 周报"完成任务/工时"用此(可选, 不通降级)
|
|
14
15
|
|
|
15
16
|
输出: JSON, 缺失的字段标 null 并注明原因。AI 拿到后直接写报告, 不用再探查。
|
|
16
17
|
所有数据严格按 .developer 里的开发者名过滤 (report 的隔离铁律)。
|
|
@@ -204,6 +205,83 @@ def _journal_today(repo, author, days):
|
|
|
204
205
|
return events[:100] if events else []
|
|
205
206
|
|
|
206
207
|
|
|
208
|
+
def _f(v):
|
|
209
|
+
"""安全转 float (工时聚合用), 失败返回 0。"""
|
|
210
|
+
try:
|
|
211
|
+
return float(v or 0)
|
|
212
|
+
except (TypeError, ValueError):
|
|
213
|
+
return 0.0
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _zentao_my_tasks(repo, days):
|
|
217
|
+
"""从禅道拉'我的'任务(含真实 consumed 工时), 供周报用。
|
|
218
|
+
|
|
219
|
+
周报的"完成任务/工时"应基于禅道真实数据, 而非本地 task.json 缓存
|
|
220
|
+
(本地缓存常滞后/缺工时)。禅道不通/未配置/未识别身份 → 返回 None,
|
|
221
|
+
静默降级不阻塞周报 (符合"连接器可选增强、断网不阻塞")。
|
|
222
|
+
"""
|
|
223
|
+
scripts = os.path.join(repo, ".qoder", "scripts")
|
|
224
|
+
if scripts not in sys.path:
|
|
225
|
+
sys.path.insert(0, scripts)
|
|
226
|
+
try:
|
|
227
|
+
from foundation.integrations.zentao_client import ZentaoClient
|
|
228
|
+
from foundation.core.paths import get_developer
|
|
229
|
+
from foundation.identity.identity import get_member
|
|
230
|
+
except Exception:
|
|
231
|
+
return None
|
|
232
|
+
client = ZentaoClient.from_config()
|
|
233
|
+
if client is None:
|
|
234
|
+
return None
|
|
235
|
+
try:
|
|
236
|
+
ok, _ = client.check_intranet()
|
|
237
|
+
if not ok:
|
|
238
|
+
return None
|
|
239
|
+
dev = get_developer()
|
|
240
|
+
my_acc = ""
|
|
241
|
+
if dev:
|
|
242
|
+
my_acc = (get_member(dev) or {}).get("zentao_account", "")
|
|
243
|
+
if not my_acc:
|
|
244
|
+
return None
|
|
245
|
+
r = client.api("GET", "/executions", {"limit": 200})
|
|
246
|
+
if r.status_code not in (200, 201):
|
|
247
|
+
return None
|
|
248
|
+
execs = r.json().get("executions") if isinstance(r.json(), dict) else None
|
|
249
|
+
if not isinstance(execs, list):
|
|
250
|
+
return None
|
|
251
|
+
mine = []
|
|
252
|
+
for e in execs:
|
|
253
|
+
eid = e.get("id")
|
|
254
|
+
try:
|
|
255
|
+
r2 = client.api("GET", "/executions/%s/tasks" % eid, {"limit": 200})
|
|
256
|
+
except Exception:
|
|
257
|
+
continue
|
|
258
|
+
if r2.status_code not in (200, 201):
|
|
259
|
+
continue
|
|
260
|
+
tasks = r2.json().get("tasks") if isinstance(r2.json(), dict) else None
|
|
261
|
+
if not isinstance(tasks, list):
|
|
262
|
+
continue
|
|
263
|
+
for t in tasks:
|
|
264
|
+
if isinstance(t, dict) and ZentaoClient.assignee_account(t) == my_acc:
|
|
265
|
+
mine.append({
|
|
266
|
+
"id": t.get("id"), "name": str(t.get("name", ""))[:40],
|
|
267
|
+
"status": t.get("status"), "consumed": t.get("consumed"),
|
|
268
|
+
"estimate": t.get("estimate"), "left": t.get("left"),
|
|
269
|
+
"deadline": t.get("deadline"), "execution": e.get("name", ""),
|
|
270
|
+
})
|
|
271
|
+
except Exception:
|
|
272
|
+
return None
|
|
273
|
+
done = [x for x in mine if x.get("status") in ("done", "closed")]
|
|
274
|
+
return {
|
|
275
|
+
"my_account": my_acc,
|
|
276
|
+
"total": len(mine),
|
|
277
|
+
"done_count": len(done),
|
|
278
|
+
"consumed_hours": round(sum(_f(x.get("consumed")) for x in mine), 1),
|
|
279
|
+
"estimate_hours": round(sum(_f(x.get("estimate")) for x in mine), 1),
|
|
280
|
+
"active_items": [x for x in mine if x.get("status") not in ("done", "closed")][:30],
|
|
281
|
+
"note": "禅道真实工时(非本地缓存); 周报'完成任务/工时'用 consumed_hours",
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
207
285
|
def snapshot(repo=None, days=None):
|
|
208
286
|
"""收集 wl-report 需要的全部数据, 返回 dict。"""
|
|
209
287
|
repo = repo or _repo_root()
|
|
@@ -226,12 +304,13 @@ def snapshot(repo=None, days=None):
|
|
|
226
304
|
# 2-5 四个独立数据源并行采集 (task list 最慢~440ms, 并行后总耗时≈最慢的那个)
|
|
227
305
|
# task list / git log / journal / prds 互不依赖 → 用线程池同时跑, 砍掉串行等待。
|
|
228
306
|
from concurrent.futures import ThreadPoolExecutor
|
|
229
|
-
with ThreadPoolExecutor(max_workers=
|
|
307
|
+
with ThreadPoolExecutor(max_workers=5) as pool:
|
|
230
308
|
f_tasks = pool.submit(_run_script, ".qoder/scripts/domain/task/task.py", repo,
|
|
231
309
|
["list", "--mine"])
|
|
232
310
|
f_journal = pool.submit(_journal_today, repo, developer, days)
|
|
233
311
|
f_commits = pool.submit(_git_log_for_author, repo, developer, days)
|
|
234
312
|
f_prds = pool.submit(_list_prds_by_author, repo, developer, days)
|
|
313
|
+
f_zentao = pool.submit(_zentao_my_tasks, repo, days)
|
|
235
314
|
|
|
236
315
|
task_out, task_ok = f_tasks.result()
|
|
237
316
|
data["my_tasks"] = task_out if task_ok else None
|
|
@@ -239,6 +318,7 @@ def snapshot(repo=None, days=None):
|
|
|
239
318
|
data["journal"] = f_journal.result()
|
|
240
319
|
data["commits"] = f_commits.result()
|
|
241
320
|
data["my_prds"] = f_prds.result()
|
|
321
|
+
data["zentao_tasks"] = f_zentao.result() # 禅道真实工时(周报用); None=未连通/未配置
|
|
242
322
|
|
|
243
323
|
# 6. 报告日期范围 (供 AI 套模板)
|
|
244
324
|
today = datetime.now().strftime("%Y-%m-%d")
|
|
@@ -249,11 +329,12 @@ def snapshot(repo=None, days=None):
|
|
|
249
329
|
|
|
250
330
|
|
|
251
331
|
def main():
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
332
|
+
import argparse
|
|
333
|
+
p = argparse.ArgumentParser(description='wl-report 数据快照 (一次性聚合全部数据源)')
|
|
334
|
+
p.add_argument('repo', nargs='?', help='仓库根路径 (默认从当前目录向上找 .qoder)')
|
|
335
|
+
p.add_argument('--days', type=int, default=1, help='回看天数 (默认1=今天/日报; 7=周报)')
|
|
336
|
+
a = p.parse_args()
|
|
337
|
+
data = snapshot(a.repo, a.days)
|
|
257
338
|
print(json.dumps(data, ensure_ascii=False, indent=2, default=str))
|
|
258
339
|
|
|
259
340
|
|
|
@@ -267,13 +267,45 @@ def _do_pull_locked(quiet, branch):
|
|
|
267
267
|
"""锁内的实际 pull 逻辑。"""
|
|
268
268
|
r = git('pull', '--rebase', '--autostash', 'origin', branch, timeout=90)
|
|
269
269
|
if r.returncode != 0:
|
|
270
|
+
err = (r.stderr or '') + (r.stdout or '')
|
|
270
271
|
if r.returncode == 124:
|
|
271
272
|
print('Pull 超时 (网络/认证问题): ' + (r.stderr or '').strip()[:150])
|
|
272
273
|
print('检查 git 凭证或团队仓库连通性。继续离线工作, 下次同步自动补推。')
|
|
273
274
|
return 1
|
|
274
|
-
if 'CONFLICT' in
|
|
275
|
+
if 'CONFLICT' in err or rebase_in_progress():
|
|
275
276
|
return report_conflict(r.stderr or r.stdout)
|
|
276
|
-
|
|
277
|
+
# ⚠ 常见问题: untracked working tree files would be overwritten
|
|
278
|
+
# 那台机器上有 init 生成的未跟踪文件 (.gitignore, .qoder/.engine-*),
|
|
279
|
+
# git pull --rebase 拒绝覆盖。解决: git checkout 先强制覆盖未跟踪文件。
|
|
280
|
+
if 'untracked working tree files would be overwritten' in err:
|
|
281
|
+
print('检测到未跟踪文件冲突, 尝试强制覆盖...')
|
|
282
|
+
# 提取冲突文件名
|
|
283
|
+
conflict_files = []
|
|
284
|
+
for line in err.splitlines():
|
|
285
|
+
line = line.strip()
|
|
286
|
+
if line and not line.startswith('error:') and not line.startswith('Please'):
|
|
287
|
+
conflict_files.append(line)
|
|
288
|
+
# git checkout -- <files> 强制覆盖
|
|
289
|
+
if conflict_files:
|
|
290
|
+
git('checkout', '--', *conflict_files[:50], timeout=30)
|
|
291
|
+
# 重试 pull
|
|
292
|
+
r2 = git('pull', '--rebase', '--autostash', 'origin', branch, timeout=90)
|
|
293
|
+
if r2.returncode == 0:
|
|
294
|
+
_auto_resolve_kg_conflict()
|
|
295
|
+
touch_pull_marker()
|
|
296
|
+
if not quiet:
|
|
297
|
+
print('Pulled latest from team (强制覆盖未跟踪文件后成功)。')
|
|
298
|
+
return 0
|
|
299
|
+
print('强制覆盖后仍失败: ' + (r2.stderr or '').strip()[:200])
|
|
300
|
+
else:
|
|
301
|
+
# 直接 git checkout -f 强制
|
|
302
|
+
git('checkout', '-f', 'origin/' + branch, timeout=60)
|
|
303
|
+
r2 = git('reset', '--hard', 'origin/' + branch, timeout=60)
|
|
304
|
+
if r2.returncode == 0:
|
|
305
|
+
if not quiet:
|
|
306
|
+
print('Pulled latest from team (强制重置后成功)。')
|
|
307
|
+
return 0
|
|
308
|
+
print('Pull failed (network/remote?): ' + err.strip()[:200])
|
|
277
309
|
print('继续离线工作, 产出不会丢失, 下次同步会自动补推。')
|
|
278
310
|
return 1
|
|
279
311
|
|
|
@@ -137,7 +137,7 @@ def cmd_pull(args, creds, my_account):
|
|
|
137
137
|
print('[DRY-RUN] 仅预览, 不写本地。加 --apply 才真写。\n')
|
|
138
138
|
|
|
139
139
|
r = creds.api('GET', '/executions/%s/tasks' % eid, {'limit': 100})
|
|
140
|
-
if r.status_code
|
|
140
|
+
if r.status_code not in (200, 201):
|
|
141
141
|
print('查询失败: %s %s' % (r.status_code, r.text[:120]))
|
|
142
142
|
return 3
|
|
143
143
|
data = r.json()
|
|
@@ -146,9 +146,10 @@ def cmd_pull(args, creds, my_account):
|
|
|
146
146
|
print('响应格式异常 (无 tasks 列表)。')
|
|
147
147
|
return 3
|
|
148
148
|
|
|
149
|
-
# ── 归属硬过滤: 只取 assignedTo == 我
|
|
149
|
+
# ── 归属硬过滤: 只取 assignedTo == 我 (用 ZentaoClient.assignee_account,
|
|
150
|
+
# 兼容禅道 assignedTo 返回 dict 的格式; 旧版 str(dict)==account 永远不匹配 → 拉到 0 个) ──
|
|
150
151
|
mine = [t for t in tasks if isinstance(t, dict)
|
|
151
|
-
and
|
|
152
|
+
and ZentaoClient.assignee_account(t) == my_account]
|
|
152
153
|
skipped = len(tasks) - len(mine)
|
|
153
154
|
print('执行 #%s 共 %d 个任务, 归属我的 %d 个 (跳过队友 %d 个, 不拉)。\n'
|
|
154
155
|
% (eid, len(tasks), len(mine), skipped))
|
|
@@ -225,8 +226,8 @@ def cmd_push(args, creds, my_account, developer):
|
|
|
225
226
|
if zid:
|
|
226
227
|
# 已有 zentao_id → 更新前校验归属一致性
|
|
227
228
|
chk = creds.api('GET', '/tasks/%s' % zid)
|
|
228
|
-
if chk.status_code
|
|
229
|
-
remote_assign =
|
|
229
|
+
if chk.status_code in (200, 201):
|
|
230
|
+
remote_assign = ZentaoClient.assignee_account(chk.json())
|
|
230
231
|
if remote_assign != my_account:
|
|
231
232
|
print(' 🚫 拒绝更新 #%s「%s」: 禅道 assignedTo=%s ≠ 你(%s)'
|
|
232
233
|
' (防误改队友任务)' % (zid, title, remote_assign, my_account))
|
|
@@ -241,3 +241,41 @@ class ZentaoClient:
|
|
|
241
241
|
return r.json().get('error', r.text[:120])
|
|
242
242
|
except Exception:
|
|
243
243
|
return r.text[:120]
|
|
244
|
+
|
|
245
|
+
# ------------------------------------------------------------------
|
|
246
|
+
# 归属解析 (纯数据提取, sync + mcp 共用 —— 消除 str(dict)==account bug 根因)
|
|
247
|
+
# ------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def _account_field(item, field: str) -> str:
|
|
251
|
+
"""从 item 提取账号字段 (兼容禅道两种返回格式)。
|
|
252
|
+
|
|
253
|
+
禅道 API 的 assignedTo/openedBy 可能是:
|
|
254
|
+
- dict: {'id':14,'account':'zhengkang','realname':'郑康'} (列表接口常见)
|
|
255
|
+
- str: 'zhengkang' (详情接口常见)
|
|
256
|
+
- None/其它: 返回空串
|
|
257
|
+
|
|
258
|
+
历史 bug: 旧代码用 ``str(dict)==account`` 比较, dict 被转成
|
|
259
|
+
``"{'account':'zhengkang'}"`` 永远 != 'zhengkang' → 归属过滤全失效
|
|
260
|
+
(sync pull 永远拉 0 个; push 永远误判归属不一致而拒绝更新)。
|
|
261
|
+
本方法统一取 account 子字段, 是归属判定的唯一真相源。
|
|
262
|
+
"""
|
|
263
|
+
if not isinstance(item, dict):
|
|
264
|
+
return ''
|
|
265
|
+
v = item.get(field)
|
|
266
|
+
if isinstance(v, dict):
|
|
267
|
+
return str(v.get('account', ''))
|
|
268
|
+
if isinstance(v, str):
|
|
269
|
+
return v
|
|
270
|
+
return ''
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def assignee_account(item) -> str:
|
|
274
|
+
"""指派人账号 (assignedTo)。task/bug/story 的归属主字段。"""
|
|
275
|
+
return ZentaoClient._account_field(item, 'assignedTo')
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def openedby_account(item) -> str:
|
|
279
|
+
"""创建人账号 (openedBy)。bug/story 归属的放宽字段
|
|
280
|
+
(团队习惯提了 bug 不指派, 留在创建人头上)。"""
|
|
281
|
+
return ZentaoClient._account_field(item, 'openedBy')
|
|
@@ -381,10 +381,22 @@ def find_python_with(mod):
|
|
|
381
381
|
return None
|
|
382
382
|
|
|
383
383
|
|
|
384
|
+
def _find_launcher():
|
|
385
|
+
"""定位 mcp_launcher.py 真实位置。
|
|
386
|
+
|
|
387
|
+
launcher 由 QoderWork 安装到用户级 ~/.qoderwork/ (不在仓库内),
|
|
388
|
+
兼容少数仓库内就近部署的情况。返回 (路径, 是否存在)。
|
|
389
|
+
"""
|
|
390
|
+
for cand in (QODERWORK_DIR / 'mcp_launcher.py', SCRIPTS_DIR / 'mcp_launcher.py'):
|
|
391
|
+
if cand.is_file():
|
|
392
|
+
return cand, True
|
|
393
|
+
return QODERWORK_DIR / 'mcp_launcher.py', False
|
|
394
|
+
|
|
395
|
+
|
|
384
396
|
def test_mcp(name):
|
|
385
397
|
"""测试一个 MCP 能否正常响应 initialize."""
|
|
386
|
-
launcher =
|
|
387
|
-
if not
|
|
398
|
+
launcher, exists = _find_launcher()
|
|
399
|
+
if not exists:
|
|
388
400
|
return {'name': name, 'status': 'ERROR',
|
|
389
401
|
'detail': f'launcher 不存在: {launcher}'}
|
|
390
402
|
|