@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
package/bin/cli.js
CHANGED
|
@@ -306,12 +306,20 @@ async function doInit(name, roleArg) {
|
|
|
306
306
|
// 源: templates/qoder/* 目标: .qoder/*
|
|
307
307
|
// init 复用 update 的三路快照保护: 新装时目标不存在, 保护不触发;
|
|
308
308
|
// 重跑 init 时用户改过的引擎文件 (如自定义 skill) 不会被覆盖。
|
|
309
|
+
// ⚠ 但如果快照缺失/损坏(如 git checkout 后), 三路保护会误保护所有文件
|
|
310
|
+
// → 修复: 快照缺失时, init 强制全覆盖(重新建立基线), 不保护。
|
|
311
|
+
const snapshotPath = path.join(cwd, ".qoder", ".engine-snapshot.json");
|
|
312
|
+
const hasSnapshot = fs.existsSync(snapshotPath);
|
|
313
|
+
if (hasExisting && !hasSnapshot) {
|
|
314
|
+
console.log(" [注意] 快照缺失, 引擎文件将强制全量覆盖 (重新建立基线)");
|
|
315
|
+
}
|
|
316
|
+
const useMode = (hasExisting && hasSnapshot) ? "update" : "init";
|
|
309
317
|
const qoderSrc = path.join(T, "qoder");
|
|
310
318
|
let copied = 0;
|
|
311
319
|
if (fs.existsSync(qoderSrc)) {
|
|
312
320
|
const snap = readSnapshot(cwd);
|
|
313
321
|
const newSnap = {};
|
|
314
|
-
const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"),
|
|
322
|
+
const r = copyDirRecursive(qoderSrc, path.join(cwd, ".qoder"), useMode, snap, newSnap);
|
|
315
323
|
copied = r.copied;
|
|
316
324
|
writeSnapshot(cwd, newSnap);
|
|
317
325
|
if (r.protectedN > 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
|
+
}
|
|
@@ -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
|
|