codebee 0.1.56 → 0.1.58
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/CHANGELOG.md +12 -0
- package/README.md +4 -3
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- package/app/.playwright-cli/run-detail-after.png +0 -0
- package/app/core/beekeeper.py +183 -0
- package/app/core/branching.py +31 -6
- package/app/core/jobs.py +3 -3
- package/app/core/pipeline.py +4143 -4112
- package/app/core/publish/browser.py +4 -0
- package/app/core/remote.py +2 -0
- package/app/core/runner.py +486 -474
- package/app/core/selfupdate.py +345 -336
- package/app/core/skill_scan.py +11 -2
- package/app/core/store.py +95 -64
- package/app/core/zentao.py +80 -0
- package/app/main.py +63 -5
- package/app/ui/app.js +13954 -13639
- package/app/ui/board.css +732 -650
- package/app/ui/i18n.js +10 -2
- package/app/ui/index.html +1 -2
- package/app/ui/style.css +6106 -6107
- package/package.json +1 -1
package/app/core/skill_scan.py
CHANGED
|
@@ -19,14 +19,19 @@ _PATTERNS = [
|
|
|
19
19
|
(r"\beval\s*\(", "代码执行", "动态 eval 执行任意代码"),
|
|
20
20
|
(r"\bexec\s*\(", "代码执行", "exec 执行任意代码"),
|
|
21
21
|
(r"subprocess|os\.system|Popen", "代码执行", "起子进程执行命令"),
|
|
22
|
+
(r"child_process", "代码执行", "Node.js 子进程(可能执行任意命令)"),
|
|
22
23
|
# 数据外发
|
|
23
24
|
(r"https?://(?!api\.|docs\.|github\.com|raw\.githubusercontent)[a-z0-9.-]+/(upload|collect|track|ingest|webhook|callback)",
|
|
24
25
|
"数据外发", "向非常规端点上传/回调数据"),
|
|
25
26
|
(r"requests\.post|urllib\.request|fetch\s*\(", "网络请求", "发起网络请求(确认目标可信)"),
|
|
27
|
+
(r"base64.{0,10}(decode|b64decode|atob)", "数据外发",
|
|
28
|
+
"base64 解码(可能隐藏混淆载荷)"),
|
|
26
29
|
# 敏感信息读取
|
|
27
30
|
(r"os\.environ|process\.env|getenv", "环境读取", "读取环境变量(可能带走密钥)"),
|
|
28
31
|
(r"\.ssh/|\.aws/|\.npmrc|\.gitconfig|credentials|\.env\b", "敏感文件", "触碰凭据/密钥文件路径"),
|
|
29
32
|
(r"keychain|credential manager|dpapi", "敏感文件", "访问系统凭据库"),
|
|
33
|
+
(r"\.claude/|\.zcode/|\.kimi-code/|\.codebee/", "敏感文件",
|
|
34
|
+
"触碰 AI 助手配置目录(可能篡改系统提示词或模型绑定)"),
|
|
30
35
|
# 提示注入特征
|
|
31
36
|
(r"(ignore|disregard|forget).{0,30}(previous|above|prior|all).{0,20}(instruction|prompt|rule)",
|
|
32
37
|
"提示注入", "指令覆盖话术(试图无视既有规则)"),
|
|
@@ -65,13 +70,17 @@ def scan_text(text, max_findings=12):
|
|
|
65
70
|
def risk_label(findings):
|
|
66
71
|
"""发现列表 → 风险标签(装前提示行用)。
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
73
|
+
三级:高危(代码执行/敏感文件/可疑意图/数据外发/base64 解码)任一
|
|
74
|
+
命中 = 高风险;中危类(网络请求/环境读取/提示注入/越权人格)≥2 类
|
|
75
|
+
同时命中 = 中风险;有发现 = 注意;空 = 干净。"""
|
|
70
76
|
if not findings:
|
|
71
77
|
return ""
|
|
72
78
|
cats = {f["category"] for f in findings}
|
|
73
79
|
if cats & {"代码执行", "敏感文件", "可疑意图", "数据外发"}:
|
|
74
80
|
return "⚠ 高风险"
|
|
81
|
+
medium_cats = cats & {"网络请求", "环境读取", "提示注入", "越权人格"}
|
|
82
|
+
if len(medium_cats) >= 2:
|
|
83
|
+
return "⚠ 中风险"
|
|
75
84
|
return "△ 注意"
|
|
76
85
|
|
|
77
86
|
|
package/app/core/store.py
CHANGED
|
@@ -28,11 +28,11 @@ _RUNS = {}
|
|
|
28
28
|
# 长跑也不会无限增长(finish_step 里调 clear_stream_state)。
|
|
29
29
|
_STREAM_LOCK = threading.Lock()
|
|
30
30
|
_STREAM_TS = {}
|
|
31
|
-
LIVE_TEXT_MAX = 20000 # 单步骤实时思考/正文的保留上限(与 builtin_agent 同口径)
|
|
32
|
-
|
|
33
|
-
DEFAULT_TASK_TIMEOUT_S = 3600
|
|
34
|
-
MIN_TASK_TIMEOUT_S = 1
|
|
35
|
-
MAX_TASK_TIMEOUT_S = 7 * 24 * 3600
|
|
31
|
+
LIVE_TEXT_MAX = 20000 # 单步骤实时思考/正文的保留上限(与 builtin_agent 同口径)
|
|
32
|
+
|
|
33
|
+
DEFAULT_TASK_TIMEOUT_S = 3600
|
|
34
|
+
MIN_TASK_TIMEOUT_S = 1
|
|
35
|
+
MAX_TASK_TIMEOUT_S = 7 * 24 * 3600
|
|
36
36
|
|
|
37
37
|
# ---------------------------------------------------------------- 状态版本(SSE 事件驱动)
|
|
38
38
|
# 任何落盘写都算状态变化:SSE 连接等版本号变化才构建/推送全量状态,
|
|
@@ -61,38 +61,38 @@ def wait_state_change(last_ver, timeout):
|
|
|
61
61
|
return _STATE_VER
|
|
62
62
|
|
|
63
63
|
|
|
64
|
-
def _new_id(prefix):
|
|
65
|
-
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
66
|
-
# 10k random suffixes are not unique under a burst of task/run creation.
|
|
67
|
-
# Reserve against both indexes and disk so a collision cannot silently
|
|
68
|
-
# overwrite a queued run and make one of several submissions disappear.
|
|
69
|
-
with LOCK:
|
|
70
|
-
for _ in range(100):
|
|
71
|
-
candidate = "%s-%s-%04d" % (prefix, stamp, secrets.randbelow(10000))
|
|
72
|
-
if candidate in _TASKS or candidate in _RUNS:
|
|
73
|
-
continue
|
|
74
|
-
if (paths.TASKS_DIR / (candidate + ".json")).exists():
|
|
75
|
-
continue
|
|
76
|
-
if (paths.RUNS_DIR / candidate).exists():
|
|
77
|
-
continue
|
|
78
|
-
return candidate
|
|
79
|
-
raise RuntimeError("无法生成唯一记录 ID")
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _safe_name(s):
|
|
83
|
-
return re.sub(r"[^0-9A-Za-z_-]+", "-", str(s))[:40].strip("-") or "x"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _task_timeout_s(value=None):
|
|
87
|
-
"""Resolve one bounded wall-clock budget for a task."""
|
|
88
|
-
raw = value
|
|
89
|
-
if raw in (None, ""):
|
|
90
|
-
raw = os.environ.get("TUTTI_TASK_TIMEOUT_S", DEFAULT_TASK_TIMEOUT_S)
|
|
91
|
-
try:
|
|
92
|
-
seconds = int(float(raw))
|
|
93
|
-
except (TypeError, ValueError):
|
|
94
|
-
seconds = DEFAULT_TASK_TIMEOUT_S
|
|
95
|
-
return max(MIN_TASK_TIMEOUT_S, min(MAX_TASK_TIMEOUT_S, seconds))
|
|
64
|
+
def _new_id(prefix):
|
|
65
|
+
stamp = time.strftime("%Y%m%d-%H%M%S")
|
|
66
|
+
# 10k random suffixes are not unique under a burst of task/run creation.
|
|
67
|
+
# Reserve against both indexes and disk so a collision cannot silently
|
|
68
|
+
# overwrite a queued run and make one of several submissions disappear.
|
|
69
|
+
with LOCK:
|
|
70
|
+
for _ in range(100):
|
|
71
|
+
candidate = "%s-%s-%04d" % (prefix, stamp, secrets.randbelow(10000))
|
|
72
|
+
if candidate in _TASKS or candidate in _RUNS:
|
|
73
|
+
continue
|
|
74
|
+
if (paths.TASKS_DIR / (candidate + ".json")).exists():
|
|
75
|
+
continue
|
|
76
|
+
if (paths.RUNS_DIR / candidate).exists():
|
|
77
|
+
continue
|
|
78
|
+
return candidate
|
|
79
|
+
raise RuntimeError("无法生成唯一记录 ID")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _safe_name(s):
|
|
83
|
+
return re.sub(r"[^0-9A-Za-z_-]+", "-", str(s))[:40].strip("-") or "x"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _task_timeout_s(value=None):
|
|
87
|
+
"""Resolve one bounded wall-clock budget for a task."""
|
|
88
|
+
raw = value
|
|
89
|
+
if raw in (None, ""):
|
|
90
|
+
raw = os.environ.get("TUTTI_TASK_TIMEOUT_S", DEFAULT_TASK_TIMEOUT_S)
|
|
91
|
+
try:
|
|
92
|
+
seconds = int(float(raw))
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
seconds = DEFAULT_TASK_TIMEOUT_S
|
|
95
|
+
return max(MIN_TASK_TIMEOUT_S, min(MAX_TASK_TIMEOUT_S, seconds))
|
|
96
96
|
|
|
97
97
|
|
|
98
98
|
# ---------------------------------------------------------------- 任务
|
|
@@ -158,8 +158,8 @@ def create_task(payload):
|
|
|
158
158
|
"status": "created",
|
|
159
159
|
}
|
|
160
160
|
thinking = str(payload.get("thinking") or "standard").strip().lower()
|
|
161
|
-
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
162
|
-
task["timeout_s"] = _task_timeout_s(payload.get("timeout_s"))
|
|
161
|
+
task["thinking"] = thinking if thinking in ("auto", "low", "standard", "high") else "auto"
|
|
162
|
+
task["timeout_s"] = _task_timeout_s(payload.get("timeout_s"))
|
|
163
163
|
if flow["engine"] == "direct":
|
|
164
164
|
task["direct_provider_id"] = _text(payload.get("direct_provider_id"),
|
|
165
165
|
"direct_provider_id")[:80]
|
|
@@ -546,7 +546,7 @@ def load_all():
|
|
|
546
546
|
_save_json(paths.TASKS_DIR / (t["id"] + ".json"), t)
|
|
547
547
|
continue
|
|
548
548
|
latest = max(runs, key=lambda r: r["id"])
|
|
549
|
-
if latest.get("status") in ("done", "failed", "cancelled", "timeout"):
|
|
549
|
+
if latest.get("status") in ("done", "failed", "cancelled", "timeout"):
|
|
550
550
|
t["status"] = latest["status"]
|
|
551
551
|
_save_json(paths.TASKS_DIR / (t["id"] + ".json"), t)
|
|
552
552
|
|
|
@@ -562,7 +562,7 @@ def set_run_estimator(estimator):
|
|
|
562
562
|
_RUN_ESTIMATOR = estimator if callable(estimator) else None
|
|
563
563
|
|
|
564
564
|
|
|
565
|
-
def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
565
|
+
def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
566
566
|
run = {
|
|
567
567
|
"id": _new_id("r" if kind == "orchestration" else "m"),
|
|
568
568
|
"kind": kind, # orchestration | mgmt
|
|
@@ -570,14 +570,14 @@ def create_run(kind, title, task_id=None, entry_id=None, op=None):
|
|
|
570
570
|
"task_id": task_id, "entry_id": entry_id, "op": op,
|
|
571
571
|
"status": "queued", "steps": [], "messages": [],
|
|
572
572
|
"created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
573
|
-
"started_at": None, "ended_at": None,
|
|
574
|
-
"deadline_at": None,
|
|
573
|
+
"started_at": None, "ended_at": None,
|
|
574
|
+
"deadline_at": None,
|
|
575
575
|
"cost_usd": 0.0, "tokens": 0, "error": "",
|
|
576
576
|
"verdict": None, "summary": "",
|
|
577
577
|
}
|
|
578
|
-
if kind == "orchestration" and task_id:
|
|
579
|
-
task = get_task(task_id) or {}
|
|
580
|
-
run["deadline_at"] = time.time() + _task_timeout_s(task.get("timeout_s"))
|
|
578
|
+
if kind == "orchestration" and task_id:
|
|
579
|
+
task = get_task(task_id) or {}
|
|
580
|
+
run["deadline_at"] = time.time() + _task_timeout_s(task.get("timeout_s"))
|
|
581
581
|
try:
|
|
582
582
|
eta = _RUN_ESTIMATOR(
|
|
583
583
|
task_type=task.get("type") or "", mode=task.get("mode") or "auto",
|
|
@@ -661,6 +661,37 @@ def task_runs(task_id):
|
|
|
661
661
|
return runs
|
|
662
662
|
|
|
663
663
|
|
|
664
|
+
def task_runs_page(task_id, offset=0, limit=3):
|
|
665
|
+
"""分页读取任务运行,只复制当前页;全量轻统计在详情打开时按需计算。"""
|
|
666
|
+
with LOCK:
|
|
667
|
+
runs = [r for r in _RUNS.values() if r.get("task_id") == task_id]
|
|
668
|
+
runs.sort(key=lambda run: run["id"], reverse=True)
|
|
669
|
+
page = [dict(run) for run in runs[offset:offset + limit]]
|
|
670
|
+
total_steps = settled_steps = total_tokens = 0
|
|
671
|
+
total_cost = 0.0
|
|
672
|
+
for run in runs:
|
|
673
|
+
steps = run.get("steps") or []
|
|
674
|
+
total_steps += len(steps)
|
|
675
|
+
settled_steps += sum(1 for step in steps
|
|
676
|
+
if step.get("status") not in ("running", "queued"))
|
|
677
|
+
try:
|
|
678
|
+
total_cost += float(run.get("cost_usd") or 0)
|
|
679
|
+
except (TypeError, ValueError):
|
|
680
|
+
pass
|
|
681
|
+
try:
|
|
682
|
+
total_tokens += int(run.get("tokens") or 0)
|
|
683
|
+
except (TypeError, ValueError):
|
|
684
|
+
pass
|
|
685
|
+
return {
|
|
686
|
+
"runs": page,
|
|
687
|
+
"total": len(runs),
|
|
688
|
+
"offset": offset,
|
|
689
|
+
"has_more": offset + len(page) < len(runs),
|
|
690
|
+
"totals": {"steps": total_steps, "settled_steps": settled_steps,
|
|
691
|
+
"cost_usd": total_cost, "tokens": total_tokens},
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
|
|
664
695
|
def task_side(task_id):
|
|
665
696
|
"""任务检查器(右缘停靠列)的轻量聚合:最新 run 摘要 + 进度步骤 +
|
|
666
697
|
git 分支/裁决状态 + 变更行级统计 + 成品文件。
|
|
@@ -762,27 +793,27 @@ def update_run(run_id, expected_status=None, **fields):
|
|
|
762
793
|
# 这套在落终态瞬间即时收口——打磨组长被取消/异常打断时 UI 不闪
|
|
763
794
|
# 假「工作中」)。run 终态后不可能还有步骤在真跑。
|
|
764
795
|
st = fields.get("status")
|
|
765
|
-
if st in ("done", "failed", "cancelled", "timeout"):
|
|
766
|
-
end_s = time.strftime("%H:%M:%S")
|
|
767
|
-
for s in run["steps"]:
|
|
768
|
-
if s.get("status") in ("queued", "running"):
|
|
769
|
-
s["status"] = "timeout" if st == "timeout" else "cancelled"
|
|
770
|
-
s["ended_at"] = s.get("ended_at") or end_s
|
|
771
|
-
if not s.get("summary"):
|
|
772
|
-
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
773
|
-
if st == "timeout" else
|
|
774
|
-
"步骤未正常收尾(终态自动恢复)")
|
|
796
|
+
if st in ("done", "failed", "cancelled", "timeout"):
|
|
797
|
+
end_s = time.strftime("%H:%M:%S")
|
|
798
|
+
for s in run["steps"]:
|
|
799
|
+
if s.get("status") in ("queued", "running"):
|
|
800
|
+
s["status"] = "timeout" if st == "timeout" else "cancelled"
|
|
801
|
+
s["ended_at"] = s.get("ended_at") or end_s
|
|
802
|
+
if not s.get("summary"):
|
|
803
|
+
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
804
|
+
if st == "timeout" else
|
|
805
|
+
"步骤未正常收尾(终态自动恢复)")
|
|
775
806
|
_save_json(paths.RUNS_DIR / run_id / "run.json", run)
|
|
776
807
|
# 状态回填:起跑与结束都同步任务状态。只回填终态的话,run 在跑、
|
|
777
808
|
# 任务永远显示「排队中」(2026-09-18 实案:「# 重写·续」run 已 running
|
|
778
809
|
# 写了一小时,任务卡 queued,用户以为卡死连点重试)。
|
|
779
|
-
if st in ("running", "done", "failed", "cancelled", "timeout"):
|
|
810
|
+
if st in ("running", "done", "failed", "cancelled", "timeout"):
|
|
780
811
|
tid = run.get("task_id")
|
|
781
812
|
task = _TASKS.get(tid) if tid else None
|
|
782
813
|
if task:
|
|
783
814
|
task["status"] = st
|
|
784
815
|
_save_json(paths.TASKS_DIR / (tid + ".json"), task)
|
|
785
|
-
if st in ("done", "failed", "cancelled", "timeout"):
|
|
816
|
+
if st in ("done", "failed", "cancelled", "timeout"):
|
|
786
817
|
# run_end 钩子(2026-09-22):终态即触发,后台跑副作用型脚本
|
|
787
818
|
# (回写知识库等);失败/超时静默,绝不拖慢收尾路径
|
|
788
819
|
try:
|
|
@@ -873,17 +904,17 @@ def recover_orphaned_runs():
|
|
|
873
904
|
step_now = time.strftime("%H:%M:%S")
|
|
874
905
|
with LOCK:
|
|
875
906
|
for r in _RUNS.values():
|
|
876
|
-
if r.get("status") not in ("done", "failed", "cancelled", "timeout"):
|
|
877
|
-
continue
|
|
907
|
+
if r.get("status") not in ("done", "failed", "cancelled", "timeout"):
|
|
908
|
+
continue
|
|
878
909
|
changed = False
|
|
879
910
|
for s in r.get("steps") or []:
|
|
880
911
|
if s.get("status") in ("queued", "running"):
|
|
881
|
-
s["status"] = "timeout" if r.get("status") == "timeout" else "cancelled"
|
|
912
|
+
s["status"] = "timeout" if r.get("status") == "timeout" else "cancelled"
|
|
882
913
|
s["ended_at"] = s.get("ended_at") or step_now
|
|
883
914
|
if not s.get("summary"):
|
|
884
|
-
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
885
|
-
if r.get("status") == "timeout" else
|
|
886
|
-
"步骤未正常收尾(取消/中断自动恢复)")
|
|
915
|
+
s["summary"] = ("步骤未正常收尾(任务总时限已到)"
|
|
916
|
+
if r.get("status") == "timeout" else
|
|
917
|
+
"步骤未正常收尾(取消/中断自动恢复)")
|
|
887
918
|
changed = True
|
|
888
919
|
if changed:
|
|
889
920
|
_save_json(paths.RUNS_DIR / r["id"] / "run.json", r)
|
package/app/core/zentao.py
CHANGED
|
@@ -329,6 +329,10 @@ _TOKEN = {"v": "", "at": 0.0}
|
|
|
329
329
|
_APIBASE = {}
|
|
330
330
|
# 通道自适应:原始地址 → "rest"(≥15 REST v1)/ "old"(老版 module-method JSON 接口)
|
|
331
331
|
_MODE = {}
|
|
332
|
+
# web 端路由形态:原始地址 → "pathinfo"(伪静态 bug-view-N.html,官方默认)
|
|
333
|
+
# / "get"(index.php?m=bug&f=view&bugID=N)。bug 详情链接用(2026-09-23 真机实证
|
|
334
|
+
# 伪静态部署对 GET 式链接不路由)。
|
|
335
|
+
_WEBSTYLE = {}
|
|
332
336
|
_OLD = {"api": "", "sid": "", "at": 0.0}
|
|
333
337
|
_OLD_FORM = {"form": ""} # 命中的密码形态 "plain" | "md5chain",缓存避免重复试
|
|
334
338
|
_REST_LAST_ERR = {"msg": ""}
|
|
@@ -349,6 +353,37 @@ def resolved_base_url(base_url):
|
|
|
349
353
|
return re.sub(r"/api\.php/v1$", "", hit)
|
|
350
354
|
|
|
351
355
|
|
|
356
|
+
def probe_web_style(base_url):
|
|
357
|
+
"""探测禅道 web 端路由形态(bug 详情链接用):GET web 根(裸根 + 常见 /zentao
|
|
358
|
+
子路径都试),看登录跳转里是伪静态路由(user-login-xxx.html)还是 GET 式
|
|
359
|
+
(index.php?m=user)。尽力而为:任何网络/守卫异常回空串,绝不影响登录与
|
|
360
|
+
扫描主流程;探出即缓存。"""
|
|
361
|
+
try:
|
|
362
|
+
b = _raw_base(base_url)
|
|
363
|
+
except ZenError:
|
|
364
|
+
return ""
|
|
365
|
+
if _WEBSTYLE.get(b):
|
|
366
|
+
return _WEBSTYLE[b]
|
|
367
|
+
style = ""
|
|
368
|
+
for root in (b + "/", b + "/zentao/"):
|
|
369
|
+
body = ""
|
|
370
|
+
try:
|
|
371
|
+
req = urllib.request.Request(_guard_url(root), headers={"Accept": "text/html"})
|
|
372
|
+
with _no_redirect_opener().open(req, timeout=HTTP_TIMEOUT) as r:
|
|
373
|
+
body = (r.read(65536) or b"").decode("utf-8", "replace")
|
|
374
|
+
except Exception:
|
|
375
|
+
body = ""
|
|
376
|
+
if "user-login-" in body or re.search(r"[a-z]+-view-\d+[^\"]*\.html", body):
|
|
377
|
+
style = "pathinfo"
|
|
378
|
+
elif "index.php?m=" in body:
|
|
379
|
+
style = "get"
|
|
380
|
+
if style:
|
|
381
|
+
break
|
|
382
|
+
if style:
|
|
383
|
+
_WEBSTYLE[b] = style
|
|
384
|
+
return style
|
|
385
|
+
|
|
386
|
+
|
|
352
387
|
def _raw_base(base_url):
|
|
353
388
|
b = str(base_url or "").strip().rstrip("/")
|
|
354
389
|
if not b:
|
|
@@ -415,6 +450,7 @@ def _rest_login(base_url, account, password):
|
|
|
415
450
|
_APIBASE[b] = api
|
|
416
451
|
_TOKEN["v"] = tok
|
|
417
452
|
_TOKEN["at"] = time.time()
|
|
453
|
+
probe_web_style(b) # bug 链接形态探测,失败不影响登录
|
|
418
454
|
return tok
|
|
419
455
|
# HTTP 200 但没有 token:REST 在但不认账密(如只认应用 code 的部署)
|
|
420
456
|
_REST_LAST_ERR["msg"] = "REST 接口不认账密(%s)" % (
|
|
@@ -592,6 +628,7 @@ def _old_login(cfg, rest_err=""):
|
|
|
592
628
|
_OLD.update(api=api, sid=sid, at=time.time())
|
|
593
629
|
_OLD_FORM["form"] = form
|
|
594
630
|
_MODE[b] = "old"
|
|
631
|
+
probe_web_style(b) # bug 链接形态探测,失败不影响登录
|
|
595
632
|
return
|
|
596
633
|
last = err
|
|
597
634
|
break # 同一台服务,账密结果与子路径无关,别再烧尝试次数
|
|
@@ -897,6 +934,40 @@ def list_bugs(cfg, product_id, profile=None):
|
|
|
897
934
|
return out[:MAX_BUGS]
|
|
898
935
|
|
|
899
936
|
|
|
937
|
+
def weekly_brief(limit=12):
|
|
938
|
+
"""本周工作素材(工作汇报起草注入用,2026-09-23 Weekly Report Generator 借鉴
|
|
939
|
+
——「从工作系统取数入素材」):分配给我的活跃任务 + 指派给我的活跃 Bug 摘要。
|
|
940
|
+
尽力而为:未配置/不可达/响应形状不对一律返回空串,绝不阻塞汇报起草。
|
|
941
|
+
多产品档案逐个聚合(同 _scan 口径),条目总量封顶 limit*2。"""
|
|
942
|
+
lines = []
|
|
943
|
+
try:
|
|
944
|
+
cfg = _cfg()
|
|
945
|
+
for profile in _profiles(cfg):
|
|
946
|
+
assigned = str(profile.get("assigned_to") or "").strip()
|
|
947
|
+
if not assigned:
|
|
948
|
+
continue
|
|
949
|
+
try:
|
|
950
|
+
d = _call("GET", "/tasks?%s" % urllib.parse.urlencode(
|
|
951
|
+
{"assignedTo": assigned, "limit": limit}), cfg=cfg)
|
|
952
|
+
tasks = d.get("tasks") if isinstance(d, dict) else None
|
|
953
|
+
for t in (tasks or [])[:limit]:
|
|
954
|
+
if isinstance(t, dict) and t.get("status") in ("wait", "doing"):
|
|
955
|
+
lines.append("- [任务#%s] %s(%s)" % (
|
|
956
|
+
t.get("id"), t.get("name") or "", t.get("status") or ""))
|
|
957
|
+
except Exception:
|
|
958
|
+
pass
|
|
959
|
+
try:
|
|
960
|
+
for b in list_bugs(cfg, profile.get("product"), profile)[:limit]:
|
|
961
|
+
lines.append("- [Bug#%s] %s(严重级 %s)" % (
|
|
962
|
+
b.get("id"), b.get("title") or "",
|
|
963
|
+
b.get("severity") or b.get("status") or ""))
|
|
964
|
+
except Exception:
|
|
965
|
+
pass
|
|
966
|
+
except Exception:
|
|
967
|
+
return ""
|
|
968
|
+
return "\n".join(lines[:limit * 2])
|
|
969
|
+
|
|
970
|
+
|
|
900
971
|
def fetch_modules(product_id):
|
|
901
972
|
"""拉产品模块清单(模块路由配置辅助)。接口不存在/失败报人话,提示手填 ID。"""
|
|
902
973
|
_ensure_loaded()
|
|
@@ -1976,6 +2047,13 @@ def bug_web_base(cfg=None):
|
|
|
1976
2047
|
return b if b.startswith(("http://", "https://")) else ""
|
|
1977
2048
|
|
|
1978
2049
|
|
|
2050
|
+
def bug_link_style(cfg=None):
|
|
2051
|
+
"""bug 详情链接形态:探测缓存优先,没探出来按伪静态(官方默认形态)。"""
|
|
2052
|
+
c = dict(cfg) if cfg else _cfg()
|
|
2053
|
+
b = str(c.get("base_url") or "").strip().rstrip("/")
|
|
2054
|
+
return _WEBSTYLE.get(b) or "pathinfo"
|
|
2055
|
+
|
|
2056
|
+
|
|
1979
2057
|
def view():
|
|
1980
2058
|
"""前端视图:配置脱敏(password 只回是否已设)+ claims 列表(新在前)。"""
|
|
1981
2059
|
_ensure_loaded()
|
|
@@ -1989,6 +2067,7 @@ def view():
|
|
|
1989
2067
|
key=lambda c: str(c.get("claimed_at") or ""), reverse=True)
|
|
1990
2068
|
return {"config": cfg,
|
|
1991
2069
|
"bug_base": bug_base,
|
|
2070
|
+
"bug_style": bug_link_style(cfg),
|
|
1992
2071
|
"claims": [dict(c) for c in claims],
|
|
1993
2072
|
"last_scan": _STATE.get("last_scan") or "",
|
|
1994
2073
|
"next_scan": _STATE.get("next_scan") or "",
|
|
@@ -2003,6 +2082,7 @@ def _test_reset():
|
|
|
2003
2082
|
_STATE["last_scan"] = _STATE["next_scan"] = _STATE["last_error"] = ""
|
|
2004
2083
|
_reset_token()
|
|
2005
2084
|
_APIBASE.clear()
|
|
2085
|
+
_WEBSTYLE.clear()
|
|
2006
2086
|
_MODE.clear()
|
|
2007
2087
|
_OLD.update(api="", sid="", at=0.0)
|
|
2008
2088
|
_OLD_FORM["form"] = ""
|
package/app/main.py
CHANGED
|
@@ -8,6 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import argparse
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
+
import math
|
|
11
12
|
import os
|
|
12
13
|
import re
|
|
13
14
|
import socket
|
|
@@ -434,7 +435,15 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
434
435
|
# 任务级详情用:该任务全部 run(含 steps),不受前端 run 窗口限制
|
|
435
436
|
if not store.get_task(m.group(1)):
|
|
436
437
|
return self._json(404, {"error": "not found"})
|
|
437
|
-
|
|
438
|
+
query = parse_qs(urlparse(self.path).query)
|
|
439
|
+
if "limit" not in query:
|
|
440
|
+
return self._json(200, {"runs": store.task_runs(m.group(1))})
|
|
441
|
+
try:
|
|
442
|
+
limit = max(1, min(20, int((query.get("limit") or [3])[0])))
|
|
443
|
+
offset = max(0, int((query.get("offset") or [0])[0]))
|
|
444
|
+
except (TypeError, ValueError):
|
|
445
|
+
return self._json(400, {"error": "invalid pagination"})
|
|
446
|
+
return self._json(200, store.task_runs_page(m.group(1), offset, limit))
|
|
438
447
|
m = re.match(r"^/api/tasks/([^/]+)/bible$", path)
|
|
439
448
|
if m:
|
|
440
449
|
# 故事圣经:查看(无令牌豁免走 _authed 已过;本机免令牌)
|
|
@@ -1988,6 +1997,13 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1988
1997
|
verdict = latest.get("verdict") or {}
|
|
1989
1998
|
route = latest.get("route") or {}
|
|
1990
1999
|
|
|
2000
|
+
def _seconds(value):
|
|
2001
|
+
try:
|
|
2002
|
+
value = float(value)
|
|
2003
|
+
except (OverflowError, TypeError, ValueError):
|
|
2004
|
+
return None
|
|
2005
|
+
return value if math.isfinite(value) and value >= 0 else None
|
|
2006
|
+
|
|
1991
2007
|
def _sec(a, b):
|
|
1992
2008
|
try:
|
|
1993
2009
|
return max(0, int(time.mktime(time.strptime(b, "%Y-%m-%d %H:%M:%S")) -
|
|
@@ -1995,6 +2011,28 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
1995
2011
|
except Exception:
|
|
1996
2012
|
return None
|
|
1997
2013
|
t0 = latest.get("started_at") or latest.get("created_at")
|
|
2014
|
+
# Run timestamps are stored only to whole seconds; two timestamps in the
|
|
2015
|
+
# same second therefore produce a misleading 0 even when a CLI step ran
|
|
2016
|
+
# for a minute. Prefer the precise persisted duration, then the sum of
|
|
2017
|
+
# step durations, and use wall-clock timestamps only as a legacy fallback.
|
|
2018
|
+
duration_s = _seconds(latest.get("duration_s"))
|
|
2019
|
+
step_durations = []
|
|
2020
|
+
for step in latest.get("steps") or []:
|
|
2021
|
+
if isinstance(step, dict):
|
|
2022
|
+
value = _seconds(step.get("duration_s"))
|
|
2023
|
+
if value is not None:
|
|
2024
|
+
step_durations.append(value)
|
|
2025
|
+
step_duration_s = round(sum(step_durations), 1) if step_durations else None
|
|
2026
|
+
if duration_s is None or duration_s == 0:
|
|
2027
|
+
if step_duration_s is not None and step_duration_s > 0:
|
|
2028
|
+
duration_s = step_duration_s
|
|
2029
|
+
else:
|
|
2030
|
+
timestamp_duration_s = (_sec(t0, latest.get("ended_at") or "")
|
|
2031
|
+
if t0 and latest.get("ended_at") else None)
|
|
2032
|
+
if timestamp_duration_s is not None:
|
|
2033
|
+
duration_s = timestamp_duration_s
|
|
2034
|
+
elif step_duration_s is not None:
|
|
2035
|
+
duration_s = step_duration_s
|
|
1998
2036
|
wd, files = "", []
|
|
1999
2037
|
try:
|
|
2000
2038
|
wd, files = store.run_artifacts(latest.get("id") or "", limit=12)
|
|
@@ -2007,8 +2045,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
2007
2045
|
"error": (latest.get("error") or "") if st in ("failed", "timeout") else "",
|
|
2008
2046
|
"executor": route.get("implementer") or verdict.get("impl") or "",
|
|
2009
2047
|
"turns": verdict.get("turns") or 0,
|
|
2010
|
-
"duration_s":
|
|
2011
|
-
if t0 and latest.get("ended_at") else None,
|
|
2048
|
+
"duration_s": duration_s,
|
|
2012
2049
|
"workdir": wd,
|
|
2013
2050
|
"files": files,
|
|
2014
2051
|
}
|
|
@@ -2504,6 +2541,19 @@ def main():
|
|
|
2504
2541
|
help="自更新重启用:先等旧实例释放端口再启动(Windows 双 LISTEN 防护)")
|
|
2505
2542
|
args = parser.parse_args()
|
|
2506
2543
|
|
|
2544
|
+
# 测试实例禁止使用默认端口 8765(2026-09-23 实案:代理测试忘带 --port,
|
|
2545
|
+
# 启动清场把真实服务当旧实例杀掉后鸠占鹊巢)。测试请显式指定独立端口。
|
|
2546
|
+
try:
|
|
2547
|
+
from core.manager import tmp_data_no_home_write as _tdnhw
|
|
2548
|
+
except Exception:
|
|
2549
|
+
_tdnhw = None
|
|
2550
|
+
if _tdnhw and _tdnhw() and args.port == 8765 and "--port" not in sys.argv:
|
|
2551
|
+
raise SystemExit("[CodeBee] 测试实例禁止使用默认端口 8765(会与真实服务冲突),"
|
|
2552
|
+
"请显式指定 --port <独立端口>")
|
|
2553
|
+
if _tdnhw and _tdnhw() and args.port == 8765:
|
|
2554
|
+
print("[CodeBee] ⚠ 测试实例正占用默认端口 8765——真实服务将无法启动;"
|
|
2555
|
+
"请改用独立端口", flush=True)
|
|
2556
|
+
|
|
2507
2557
|
# 启动每步都落一行进度(flush 强制落屏):冷启动在慢盘/杀软扫描下可能几十秒,
|
|
2508
2558
|
# 不打印会让用户以为卡死(真实案例:npm 装完首启只看到横幅像挂了)。
|
|
2509
2559
|
# 看门狗:任何阶段卡超过 20 秒,自动把所有线程堆栈打到控制台(每 20s 重复)——
|
|
@@ -2665,8 +2715,16 @@ def main():
|
|
|
2665
2715
|
from core import portscan as _ps, portguard as _pg
|
|
2666
2716
|
_hint = ""
|
|
2667
2717
|
try:
|
|
2668
|
-
|
|
2669
|
-
|
|
2718
|
+
from core.manager import tmp_data_no_home_write
|
|
2719
|
+
if tmp_data_no_home_write():
|
|
2720
|
+
# 测试实例不清场:TUTTI_DATA 在临时目录的进程若占用了真实
|
|
2721
|
+
# 端口(如 8765),清场会误杀生产服务(2026-09-23 实案)
|
|
2722
|
+
_cleared, _hint = False, "测试实例不执行端口清场"
|
|
2723
|
+
else:
|
|
2724
|
+
_cleared, _hint = _pg.clear_stale_port(
|
|
2725
|
+
args.port, paths.APP_DIR / "main.py")
|
|
2726
|
+
except Exception:
|
|
2727
|
+
_cleared, _hint = False,
|
|
2670
2728
|
except Exception:
|
|
2671
2729
|
_cleared, _hint = False, ""
|
|
2672
2730
|
if _cleared:
|