codebee 0.1.66 → 0.1.67
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 +13 -1
- package/app/core/acceptance.py +100 -0
- package/app/core/assets.py +110 -0
- package/app/core/attachments.py +92 -1
- package/app/core/bookmeta.py +160 -26
- package/app/core/defectretro.py +43 -0
- package/app/core/flows.py +47 -1
- package/app/core/jobs.py +20 -4
- package/app/core/knowledge.py +50 -3
- package/app/core/manager.py +7 -0
- package/app/core/market_remote.py +124 -26
- package/app/core/modelhub.py +29 -0
- package/app/core/operations.py +228 -0
- package/app/core/paihang.py +58 -29
- package/app/core/pipeline.py +205 -11
- package/app/core/planner.py +4 -1
- package/app/core/publish/browser.py +6 -0
- package/app/core/publish/fanqie.py +31 -0
- package/app/core/publish/flow.py +9 -1
- package/app/core/publish/ledger.py +7 -2
- package/app/core/publish/manager.py +91 -22
- package/app/core/revisions.py +51 -0
- package/app/core/runner.py +27 -12
- package/app/core/skills.py +94 -1
- package/app/core/store.py +207 -11
- package/app/core/volumes.py +34 -0
- package/app/core/zentao.py +21 -3
- package/app/main.py +140 -21
- package/app/ui/app.js +407 -17
- package/app/ui/artdiff.js +74 -0
- package/app/ui/i18n.js +51 -1
- package/app/ui/index.html +6 -0
- package/app/ui/style.css +96 -24
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -19,7 +19,7 @@ import re
|
|
|
19
19
|
import threading
|
|
20
20
|
import time
|
|
21
21
|
|
|
22
|
-
from . import aiflavor, attachments, branching, catalog, chaptersafe, dispatch_log, history, hooks, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage, volumes
|
|
22
|
+
from . import aiflavor, attachments, branching, catalog, chaptersafe, defectretro, dispatch_log, flows, history, hooks, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage, volumes
|
|
23
23
|
from . import builtin_agent
|
|
24
24
|
from . import diagnostics
|
|
25
25
|
from . import paths as paths_mod
|
|
@@ -365,6 +365,44 @@ def _binding_dead_msg(agent):
|
|
|
365
365
|
"请在「模型调度(可选)」页为该 CLI 指定已启用的供应商")
|
|
366
366
|
|
|
367
367
|
|
|
368
|
+
def _binding_brief(agent):
|
|
369
|
+
"""步骤级绑定速记(借鉴 WorkDSH ResolvedExecutionBinding:模型目录只是发现
|
|
370
|
+
信息,解析结果才是权威)。把本步实际执行用的 provider 链与上游 host 压成
|
|
371
|
+
一行——配额烧错家/模型漂移直接对账步骤行,不必翻日志文本里的审计行。
|
|
372
|
+
只记身份不记密钥:env 形态的注入也只取 URL 的 host 部分。"""
|
|
373
|
+
from . import upstream as _up
|
|
374
|
+
entries = []
|
|
375
|
+
for item in (agent.get("call_chain") or [])[:4]:
|
|
376
|
+
if not isinstance(item, dict):
|
|
377
|
+
continue
|
|
378
|
+
pid = str(item.get("provider_id") or "").strip()
|
|
379
|
+
prov = item.get("provider") if isinstance(item.get("provider"), dict) else {}
|
|
380
|
+
host = ""
|
|
381
|
+
try:
|
|
382
|
+
host = _up.normalize_upstream(
|
|
383
|
+
str(prov.get("base_url") or prov.get("baseUrl") or ""))
|
|
384
|
+
except Exception:
|
|
385
|
+
host = ""
|
|
386
|
+
if pid and host and host not in pid:
|
|
387
|
+
entries.append("%s@%s" % (pid, host))
|
|
388
|
+
else:
|
|
389
|
+
entries.append(pid or host or "?")
|
|
390
|
+
if not entries:
|
|
391
|
+
# env 注入形态(如 ANTHROPIC_BASE_URL):URL 值的 host 不敏感,可入账
|
|
392
|
+
for k, v in sorted((agent.get("env") or {}).items()):
|
|
393
|
+
s = str(v or "")
|
|
394
|
+
if s.startswith(("http://", "https://")):
|
|
395
|
+
try:
|
|
396
|
+
host = _up.normalize_upstream(s)
|
|
397
|
+
except Exception:
|
|
398
|
+
continue
|
|
399
|
+
if host:
|
|
400
|
+
entries.append("%s=%s" % (str(k)[:24], host))
|
|
401
|
+
if len(entries) >= 3:
|
|
402
|
+
break
|
|
403
|
+
return ",".join(entries[:3])[:120]
|
|
404
|
+
|
|
405
|
+
|
|
368
406
|
def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner.DEFAULT_TIMEOUT, note="", resume=None, images=None, require_tools=False, swap_guard=None):
|
|
369
407
|
"""执行一个智能体步骤并记录。返回 runner 统一结果。
|
|
370
408
|
|
|
@@ -402,7 +440,8 @@ def _run_step(run_id, role, agent, prompt, workdir, readonly, ev, timeout=runner
|
|
|
402
440
|
note = ((note + ";") if note else "") + "⚠ " + dead_msg
|
|
403
441
|
step, log_abs = store.add_step(run_id, role, agent["id"],
|
|
404
442
|
agent.get("label", agent["id"]), note=note,
|
|
405
|
-
model=agent.get("model") or ""
|
|
443
|
+
model=agent.get("model") or "",
|
|
444
|
+
provider=_binding_brief(agent))
|
|
406
445
|
start = time.time()
|
|
407
446
|
if dead_binding:
|
|
408
447
|
from .error_codes import ErrorCode
|
|
@@ -473,7 +512,8 @@ def _run_builtin_step(run_id, role, bi, prompt, workdir, ev, note="", images=Non
|
|
|
473
512
|
_wait_gate(run_id, ev)
|
|
474
513
|
deadline = _ensure_budget(run_id)
|
|
475
514
|
step, log_abs = store.add_step(run_id, role, "builtin", "CodeBee", note=note,
|
|
476
|
-
model=bi.get("model") or ""
|
|
515
|
+
model=bi.get("model") or "",
|
|
516
|
+
provider=str(bi.get("provider_id") or "")[:120])
|
|
477
517
|
start = time.time()
|
|
478
518
|
agent_pseudo = {"id": "builtin", "label": "CodeBee", "kind": "builtin", "mode": "real",
|
|
479
519
|
"provider": {"id": bi.get("provider_id") or "",
|
|
@@ -650,9 +690,15 @@ def _finish_step_result(run_id, step, res, role, agent, start):
|
|
|
650
690
|
status = "timeout"
|
|
651
691
|
else:
|
|
652
692
|
status = "done" if res["ok"] else "failed"
|
|
693
|
+
step_summary = (res.get("text") or res.get("error") or "")[:600]
|
|
694
|
+
if raw.get("quiescence_unknown"):
|
|
695
|
+
# 杀树未确认清空(借鉴 WorkDSH 取消结算分层:kill 完成 ≠ 停稳):
|
|
696
|
+
# 孙进程可能仍在跑,孤儿清扫稍后兜底。只提示不改变终态语义。
|
|
697
|
+
step_summary = ((step_summary + "\n") if step_summary else "") + \
|
|
698
|
+
"⚠ 进程树未确认清空:孙进程可能仍在运行(孤儿清扫稍后兜底)"
|
|
653
699
|
store.finish_step(run_id, step["n"],
|
|
654
700
|
status,
|
|
655
|
-
summary=
|
|
701
|
+
summary=step_summary,
|
|
656
702
|
exit_code=res.get("raw", {}).get("exit_code"),
|
|
657
703
|
cost_usd=res.get("cost_usd", 0.0),
|
|
658
704
|
tokens=res.get("tokens", 0),
|
|
@@ -1915,6 +1961,10 @@ def _run_direct(run, task, agents, ev, stats, mode):
|
|
|
1915
1961
|
# 扫榜选材(借鉴 oh-story 扫榜):抓七猫排行榜公开数据注入,
|
|
1916
1962
|
# AI 做选题洞察;抓取失败回落普通直连提示词
|
|
1917
1963
|
prompt = paihang.rank_scan_prompt(task.get("goal") or "") or ""
|
|
1964
|
+
if task.get("type") == "defect_retro" and bi is not None:
|
|
1965
|
+
# 缺陷复盘(借鉴 test-defect-retrospective):三视角分析框架
|
|
1966
|
+
# 注入,缺陷导出 CSV 走任务附件通道(框架恒有、不回落)
|
|
1967
|
+
prompt = defectretro.retro_prompt(task.get("goal") or "")
|
|
1918
1968
|
if not prompt:
|
|
1919
1969
|
if bi is not None:
|
|
1920
1970
|
prompt = (BUILTIN_DIRECT_PROMPT
|
|
@@ -2144,10 +2194,10 @@ NOVEL_CRITIQUE_PROMPT = """你是严格的评审(不要使用任何工具、
|
|
|
2144
2194
|
请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
2145
2195
|
{
|
|
2146
2196
|
"scores": {"__DIMKEYS__"},
|
|
2147
|
-
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题"}],
|
|
2197
|
+
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
|
|
2148
2198
|
"summary": "一句话总评"
|
|
2149
2199
|
}
|
|
2150
|
-
每个维度打 1-10 分(可为小数),宁严勿宽。
|
|
2200
|
+
每个维度打 1-10 分(可为小数),宁严勿宽。major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
|
|
2151
2201
|
|
|
2152
2202
|
## 待评审稿件
|
|
2153
2203
|
---
|
|
@@ -2260,6 +2310,18 @@ def _read_ledger(workdir):
|
|
|
2260
2310
|
return txt or "(暂无记录)"
|
|
2261
2311
|
|
|
2262
2312
|
|
|
2313
|
+
def _read_ledger_raw(workdir):
|
|
2314
|
+
"""账本原文(缺文件/读失败返回空串)——供 _ledger_watchlist 解析,
|
|
2315
|
+
不带注入面的兜底文案。"""
|
|
2316
|
+
p = os.path.abspath(os.path.join(str(workdir or ""), LEDGER_FILE))
|
|
2317
|
+
if not _inside(workdir, p) or not os.path.isfile(p):
|
|
2318
|
+
return ""
|
|
2319
|
+
try:
|
|
2320
|
+
return _read_text_any_enc(p)
|
|
2321
|
+
except OSError:
|
|
2322
|
+
return ""
|
|
2323
|
+
|
|
2324
|
+
|
|
2263
2325
|
def _shrink_context_block(sk_block, bible, budget=12000):
|
|
2264
2326
|
"""分层上下文降级(长提示词在容量受限通道上会挂起/秒拒,2026-09-17 讯飞实测)。
|
|
2265
2327
|
|
|
@@ -2369,10 +2431,11 @@ SERIAL_GLOBAL_PROMPT = """你是网文主编(不要修改任何文件)。全
|
|
|
2369
2431
|
请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
2370
2432
|
{
|
|
2371
2433
|
"scores": {"__DIMKEYS__"},
|
|
2372
|
-
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)"}],
|
|
2434
|
+
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
|
|
2373
2435
|
"summary": "一句话总评:是否达到可签约水平"
|
|
2374
2436
|
}
|
|
2375
2437
|
每个维度打 1-10 分,宁严勿宽。重点关注:主线一致性、人物弧光、节奏、爽点密度、完本感。
|
|
2438
|
+
major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
|
|
2376
2439
|
|
|
2377
2440
|
## 全书目标
|
|
2378
2441
|
__GOAL__
|
|
@@ -2446,6 +2509,87 @@ def _wc(text):
|
|
|
2446
2509
|
return len(re.sub(r"\s", "", text or ""))
|
|
2447
2510
|
|
|
2448
2511
|
|
|
2512
|
+
_ANCHOR_EDGE_PUNCT = "。!?,、;:…—·,.!?;:\"'“”‘’()()[]【】《》<>*#-"
|
|
2513
|
+
|
|
2514
|
+
|
|
2515
|
+
def _norm_anchor(s):
|
|
2516
|
+
"""锚校验归一:去全部空白 + 剥首尾标点(引文摘录常多带一个句号/引号,
|
|
2517
|
+
中英混排空格差异也不应判假)。"""
|
|
2518
|
+
s = re.sub(r"\s+", "", str(s or ""))
|
|
2519
|
+
return s.strip(_ANCHOR_EDGE_PUNCT)
|
|
2520
|
+
|
|
2521
|
+
|
|
2522
|
+
def _anchor_issues(issues, manuscript):
|
|
2523
|
+
"""评审证据锚(ARIS Anti-Autoresearch 借鉴):issue 带 quote(稿件原文
|
|
2524
|
+
摘录)时做确定性 containment 校验——模型只提议发现,锚定事实由规则裁决。
|
|
2525
|
+
|
|
2526
|
+
三态:quote 命中稿件 → anchored=True;quote 给了但对不上 → anchored=False
|
|
2527
|
+
(降档殿后+标记,不删除:模型改写式引用时保留可见性);没给 quote →
|
|
2528
|
+
anchored=None(老格式评审灰度兼容,不标记不排序影响)。返回 (issues, 未锚定数)。"""
|
|
2529
|
+
ms = _norm_anchor(manuscript)
|
|
2530
|
+
unanchored = 0
|
|
2531
|
+
for it in issues or []:
|
|
2532
|
+
q = _norm_anchor(it.get("quote"))
|
|
2533
|
+
if not q:
|
|
2534
|
+
it["anchored"] = None
|
|
2535
|
+
continue
|
|
2536
|
+
it["anchored"] = len(q) >= 6 and q in ms
|
|
2537
|
+
if it["anchored"] is False:
|
|
2538
|
+
unanchored += 1
|
|
2539
|
+
return issues, unanchored
|
|
2540
|
+
|
|
2541
|
+
|
|
2542
|
+
def _major_lines(majors, limit=8):
|
|
2543
|
+
"""majors → 修订面意见行:锚定的在前,未锚定殿后并带标记(模型据此
|
|
2544
|
+
分配采信权重);无 quote 的老格式行居中不动。"""
|
|
2545
|
+
ranked = sorted(majors or [],
|
|
2546
|
+
key=lambda x: {True: 0, False: 2}.get(x.get("anchored"), 1))
|
|
2547
|
+
lines = []
|
|
2548
|
+
for x in ranked[:limit]:
|
|
2549
|
+
mark = ("(未锚定:引文与稿件不符,酌情采信)"
|
|
2550
|
+
if x.get("anchored") is False else "")
|
|
2551
|
+
lines.append("- [%s] %s%s" % (x.get("dim", "?"),
|
|
2552
|
+
str(x.get("note", ""))[:140], mark))
|
|
2553
|
+
return lines
|
|
2554
|
+
|
|
2555
|
+
|
|
2556
|
+
_LEDGER_DONE_WORDS = ("已兑现", "已回收", "已解除", "已完成", "已了结", "已交付")
|
|
2557
|
+
|
|
2558
|
+
|
|
2559
|
+
def _ledger_watchlist(ledger_text, upto_chapter, min_age=3, limit=8):
|
|
2560
|
+
"""陈年承诺督促(continuum 承诺/伏笔账借鉴):按「## 第 N 章」段解析
|
|
2561
|
+
资源账本,承诺/伏笔类条目现状未了结、且距本章 ≥min_age 章未更新 →
|
|
2562
|
+
观查清单(推送起草面督促推进/回收)。账本按章追加、同名条目以最后
|
|
2563
|
+
出现的章为准;老账本无该类条目返回空。"""
|
|
2564
|
+
last = {} # (类型, 名称) -> (最后章号, 现状)
|
|
2565
|
+
cur = 0
|
|
2566
|
+
for ln in str(ledger_text or "").splitlines():
|
|
2567
|
+
m = re.match(r"##\s*第\s*(\d+)\s*章", ln.strip())
|
|
2568
|
+
if m:
|
|
2569
|
+
cur = int(m.group(1))
|
|
2570
|
+
continue
|
|
2571
|
+
s = ln.strip().lstrip("-*").strip()
|
|
2572
|
+
if not s or "|" not in s:
|
|
2573
|
+
continue
|
|
2574
|
+
parts = [p.strip() for p in s.split("|")]
|
|
2575
|
+
if len(parts) < 3:
|
|
2576
|
+
continue
|
|
2577
|
+
typ, name, status = parts[0], parts[1], "|".join(parts[2:])
|
|
2578
|
+
if "承诺" not in typ and "伏笔" not in typ:
|
|
2579
|
+
continue
|
|
2580
|
+
last[(typ, name)] = (cur, status)
|
|
2581
|
+
out = []
|
|
2582
|
+
for (typ, name), (ch_no, status) in last.items():
|
|
2583
|
+
if any(w in status for w in _LEDGER_DONE_WORDS):
|
|
2584
|
+
continue
|
|
2585
|
+
age = upto_chapter - ch_no
|
|
2586
|
+
if age >= min_age:
|
|
2587
|
+
out.append({"type": typ, "name": name, "status": status,
|
|
2588
|
+
"age": age, "last_chapter": ch_no})
|
|
2589
|
+
out.sort(key=lambda x: -x["age"])
|
|
2590
|
+
return out[:limit]
|
|
2591
|
+
|
|
2592
|
+
|
|
2449
2593
|
def _write_chapter(workdir, i, text):
|
|
2450
2594
|
# 安全落盘委托(inkos 安全章节工作区借鉴):tmp+原子改名,写一半
|
|
2451
2595
|
# 崩溃不留半章冒充成稿;路径守卫在 chaptersafe(resolve+parents)
|
|
@@ -2702,6 +2846,19 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
2702
2846
|
ev_check_lines = [[]] # 每章重置:第一份非空评审的逐项判定
|
|
2703
2847
|
ledger_lines = [] # 本章全部评审的账本增量并集
|
|
2704
2848
|
ledger_txt = _read_ledger(workdir) # 截至上一章的资源账本(起草注入)
|
|
2849
|
+
# 陈年承诺督促(continuum 借鉴):账本里 ≥3 章未推进且未了结的承诺/
|
|
2850
|
+
# 伏笔点名给起草面——「本章推进/回收其一,或明确有意留白」;老账本
|
|
2851
|
+
# 无承诺类条目时静默不占字节
|
|
2852
|
+
try:
|
|
2853
|
+
_wl = _ledger_watchlist(_read_ledger_raw(workdir), i)
|
|
2854
|
+
if _wl:
|
|
2855
|
+
ledger_txt += ("\n\n### ⚠ 陈年承诺/伏笔(多章未推进)\n"
|
|
2856
|
+
+ "\n".join("- %s「%s」:%s(已 %d 章未更新——本章应推进、"
|
|
2857
|
+
"回收其一,或在大纲允许处明确留白)"
|
|
2858
|
+
% (w["type"], w["name"], w["status"], w["age"])
|
|
2859
|
+
for w in _wl))
|
|
2860
|
+
except Exception:
|
|
2861
|
+
pass
|
|
2705
2862
|
prev = ""
|
|
2706
2863
|
draft_sid = "" # §07 T1.1:本轮 draft/复用章的会话 id(revise 复用;reuse 时为空)
|
|
2707
2864
|
if i > 1:
|
|
@@ -3234,9 +3391,11 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3234
3391
|
rounds_used = 2
|
|
3235
3392
|
majors = [x for x in issues_all if x.get("chapter") == i
|
|
3236
3393
|
and x.get("severity") == "major"][:8]
|
|
3394
|
+
majors, _unanchored = _anchor_issues(majors, text)
|
|
3237
3395
|
crit_lines = ["- %s:%.1f(章阈值 %.1f)" % (d, means[d], threshold_ch) for d in dims]
|
|
3238
|
-
crit_lines +=
|
|
3239
|
-
|
|
3396
|
+
crit_lines += _major_lines(majors)
|
|
3397
|
+
if _unanchored:
|
|
3398
|
+
crit_lines.append("- ⚠ %d 条 major 意见的引文未能在稿件中锚定,已降序殿后" % _unanchored)
|
|
3240
3399
|
if rnd == 1 and race_losers and impl.get("mode") == "real":
|
|
3241
3400
|
# 赛马败者精华回收(连载版):一次选择器调用提炼落选稿优点,
|
|
3242
3401
|
# 与评审意见一并喂给首轮修订;失败静默不影响修订
|
|
@@ -3962,8 +4121,10 @@ def _run_content_review(run, task, agents, ev, stats, mode):
|
|
|
3962
4121
|
# 修订
|
|
3963
4122
|
crit_lines = ["- %s:%.1f(阈值 %.1f)" % (d, means[d], threshold) for d in dims]
|
|
3964
4123
|
majors = [i for i in issues_all if i.get("severity") == "major"][:8]
|
|
3965
|
-
|
|
3966
|
-
|
|
4124
|
+
majors, _unanchored = _anchor_issues(majors, manuscript)
|
|
4125
|
+
crit_lines += _major_lines(majors)
|
|
4126
|
+
if _unanchored:
|
|
4127
|
+
crit_lines.append("- ⚠ %d 条 major 意见的引文未能在稿件中锚定,已降序殿后" % _unanchored)
|
|
3967
4128
|
if r == 1 and bestof_improvements:
|
|
3968
4129
|
# 败者精华回收(freebuff suggestedImprovements 的落地):终审从落选
|
|
3969
4130
|
# 候选稿提炼的优点,首轮修订时与评审意见一并喂给作者
|
|
@@ -4387,6 +4548,39 @@ def execute_run(run_id):
|
|
|
4387
4548
|
store.update_run(run_id, expected_status="running", status="failed",
|
|
4388
4549
|
error="找不到任务 %s" % run.get("task_id"), ended_at=_now())
|
|
4389
4550
|
return
|
|
4551
|
+
# 流程修订漂移诊断(借鉴 WorkDSH ADR-0010 修订钉住):任务参数创建时已
|
|
4552
|
+
# 固化,改流程不影响固化字段;但历史任务缺失字段的兜底仍取当前流程定义,
|
|
4553
|
+
# 续跑/重试可能静默吃到新流程。漂移不阻断——「改完流程再重跑」是合法
|
|
4554
|
+
# 路径——但必须留痕可对账:run 记结构化 flow_drift + 用户可见 warning。
|
|
4555
|
+
_drift = flows.flow_drift(task)
|
|
4556
|
+
if _drift:
|
|
4557
|
+
store.update_run(run_id, flow_drift=_drift)
|
|
4558
|
+
try:
|
|
4559
|
+
_warns = (store.get_run(run_id) or {}).get("warnings") or []
|
|
4560
|
+
store.update_run(run_id, warnings=_warns + [
|
|
4561
|
+
"流程「%s」在任务创建后已修改(%s → %s);任务固化参数不受影响,"
|
|
4562
|
+
"历史任务缺失字段的兜底取值已随新定义"
|
|
4563
|
+
% (_drift["flow"], _drift["pinned"][:8], _drift["current"][:8])])
|
|
4564
|
+
except Exception:
|
|
4565
|
+
pass
|
|
4566
|
+
print("[CodeBee] 流程漂移:%s(任务 %s 创建后流程已修改 %s → %s)"
|
|
4567
|
+
% (run_id, task.get("id"), _drift["pinned"][:8], _drift["current"][:8]),
|
|
4568
|
+
flush=True)
|
|
4569
|
+
# 附件完整性对账(借鉴 WorkDSH 输入引用修订):任务钉的附件指纹 vs 当前
|
|
4570
|
+
# 文件内容。换过/删过给明确诊断——续跑吃到「不是当初那份附件」不能无声。
|
|
4571
|
+
try:
|
|
4572
|
+
_adrift = attachments.verify_task(task, task.get("workdir"))
|
|
4573
|
+
if _adrift:
|
|
4574
|
+
store.update_run(run_id, attachment_drift=_adrift)
|
|
4575
|
+
_names = "、".join(d["name"] for d in _adrift[:3]) + (
|
|
4576
|
+
" 等 %d 项" % len(_adrift) if len(_adrift) > 3 else "")
|
|
4577
|
+
_warns2 = (store.get_run(run_id) or {}).get("warnings") or []
|
|
4578
|
+
store.update_run(run_id, warnings=_warns2 + [
|
|
4579
|
+
"附件在任务创建后已变化(%s):%s;本次执行使用当前文件内容"
|
|
4580
|
+
% (", ".join(sorted({d["status"] for d in _adrift})), _names)])
|
|
4581
|
+
print("[CodeBee] 附件漂移:%s(%s)" % (run_id, _names), flush=True)
|
|
4582
|
+
except Exception:
|
|
4583
|
+
pass
|
|
4390
4584
|
try:
|
|
4391
4585
|
_ensure_budget(run_id)
|
|
4392
4586
|
except TaskTimeout:
|
package/app/core/planner.py
CHANGED
|
@@ -133,6 +133,8 @@ CODE_PLAN_PROMPT = """你是技术负责人。请先判定任务难度,再按
|
|
|
133
133
|
子任务拆分:easy 一律只拆 1 个子任务——一个子任务把目标完整做完并自检通过即收尾,
|
|
134
134
|
禁止拆出独立的核查/联调/收尾子任务;只有 hard 才允许多个子任务(至多 __N__ 个),
|
|
135
135
|
粒度要可独立验证,且最后一个子任务必须包含整体联调/收尾。
|
|
136
|
+
hard 也不做任务没要的防御性扩展子任务(缓存/重试/监控/通用抽象层)——
|
|
137
|
+
发现真实风险写进对应子任务的 detail 里备注,由人决定要不要做。
|
|
136
138
|
|
|
137
139
|
## 先探索再计划(重要,借鉴 OpenSpec explore)
|
|
138
140
|
拆解之前先用你的读文件/搜索工具**实际查看工作目录**,找到目标相关的真实文件与函数,再据此拆解。
|
|
@@ -248,7 +250,8 @@ def _norm_chapters(data, n, vol_per=0, vol_start=1, vol_plan=None):
|
|
|
248
250
|
# 分卷:只收本批真正用到的卷的卷名/卷弧光(模型可能多写或乱写卷号,一律按
|
|
249
251
|
# 本批章号范围过滤)。卷边界不在这里——它由 volumes.build_plan 从章号推导。
|
|
250
252
|
if vol_plan:
|
|
251
|
-
named = volumes.norm_volumes(data.get("volumes"), vol_per, vol_start, n
|
|
253
|
+
named = volumes.norm_volumes(data.get("volumes"), vol_per, vol_start, n,
|
|
254
|
+
plan=vol_plan)
|
|
252
255
|
if named:
|
|
253
256
|
res["volumes"] = named
|
|
254
257
|
return res
|
|
@@ -342,6 +342,12 @@ class Page:
|
|
|
342
342
|
pass
|
|
343
343
|
time.sleep(0.4)
|
|
344
344
|
return
|
|
345
|
+
host = _up(url).netloc
|
|
346
|
+
if not host:
|
|
347
|
+
# 无主机名=无效地址(占位符未替换等)。放行会在下方被「host 为空
|
|
348
|
+
# 视为已到位」吞掉——页面停在 about:blank 还假装导航成功(0924 案)。
|
|
349
|
+
raise BrowserError("无效的导航地址(无主机名,占位符可能未替换):%s"
|
|
350
|
+
% str(url)[:90])
|
|
345
351
|
try:
|
|
346
352
|
self.send("Page.navigate", {"url": url}, timeout=timeout)
|
|
347
353
|
except BrowserError:
|
|
@@ -126,3 +126,34 @@ def chapter_manage_url(book):
|
|
|
126
126
|
if bid:
|
|
127
127
|
return "https://fanqienovel.com/main/writer/chapter-manage/" + bid
|
|
128
128
|
return CONFIG["home"] + "book-manage"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def resolve_book_id(page, book):
|
|
132
|
+
"""登记缺 book_id 时按书名在作家后台找回:点书卡进详情,从 URL 提取。
|
|
133
|
+
|
|
134
|
+
建书成功但 id 提取落空(跳转链没走完/auto_submit=false 人工提交)的书,
|
|
135
|
+
发章前经 manager._resolve_book_id 调到这里补账;找不回返回空串不拦死。"""
|
|
136
|
+
import re
|
|
137
|
+
import time
|
|
138
|
+
from . import flow as _flow
|
|
139
|
+
title = str((book or {}).get("title") or "").strip()
|
|
140
|
+
if not title:
|
|
141
|
+
return ""
|
|
142
|
+
try:
|
|
143
|
+
page.navigate(CONFIG["home"], timeout=30)
|
|
144
|
+
except Exception:
|
|
145
|
+
return ""
|
|
146
|
+
r = None
|
|
147
|
+
for _try in range(6): # 书列表慢渲染
|
|
148
|
+
r = page.call(_flow._click_match_js(), [title], 300)
|
|
149
|
+
if (r or {}).get("ok"):
|
|
150
|
+
break
|
|
151
|
+
time.sleep(1.0)
|
|
152
|
+
if not (r or {}).get("ok"):
|
|
153
|
+
return ""
|
|
154
|
+
time.sleep(2.5) # 详情页跳转收尾
|
|
155
|
+
m = re.search(r"book-info/(\d+)|chapter-manage/(\d+)|writer/(\d+)/publish",
|
|
156
|
+
str(page.url() or ""))
|
|
157
|
+
if not m:
|
|
158
|
+
return ""
|
|
159
|
+
return m.group(1) or m.group(2) or m.group(3) or ""
|
package/app/core/publish/flow.py
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
from __future__ import annotations
|
|
23
23
|
|
|
24
24
|
import json
|
|
25
|
+
import re
|
|
25
26
|
import time
|
|
26
27
|
|
|
27
28
|
from .browser import BrowserError
|
|
@@ -122,6 +123,13 @@ def run_flow(page, steps, values=None, config=None, auto_submit=False,
|
|
|
122
123
|
url = url.replace("{%s}" % k, str(v))
|
|
123
124
|
for k, v in (values or {}).items(): # editor_url/draft_url 等任务级占位
|
|
124
125
|
url = url.replace("{%s}" % k, str(v))
|
|
126
|
+
# 占位符没被吃掉=values 缺键(平台模块缺 URL 生成器/登记不全)。
|
|
127
|
+
# 直接放行会让浏览器停在 about:blank,到 url_any 才误报
|
|
128
|
+
# 「未登录或改版」——在这里点名缺哪个键(0924 番茄三连败)。
|
|
129
|
+
left = re.findall(r"\{[A-Za-z_][A-Za-z0-9_]*\}", url)
|
|
130
|
+
if left:
|
|
131
|
+
raise FlowError("URL 占位符 %s 没有对应值:%s"
|
|
132
|
+
% (",".join(left), url[:90]))
|
|
125
133
|
note(i, "打开 %s" % url)
|
|
126
134
|
# 重页(编辑器/管理列表)可按步放宽;缺省 45s(30s 对慢网偏紧)
|
|
127
135
|
page.navigate(url, timeout=float(st.get("timeout") or 45))
|
|
@@ -410,7 +418,7 @@ def run_flow(page, steps, values=None, config=None, auto_submit=False,
|
|
|
410
418
|
else:
|
|
411
419
|
msg += "(常见原因:表单校验未过,如简介字数不足)"
|
|
412
420
|
raise FlowError(msg)
|
|
413
|
-
note(i, "
|
|
421
|
+
note(i, "页面标记校验通过(%s)" % u[:80])
|
|
414
422
|
else:
|
|
415
423
|
raise FlowError("未知步骤类型:%s" % act)
|
|
416
424
|
except FlowError:
|
|
@@ -24,7 +24,8 @@ from .. import paths
|
|
|
24
24
|
LOCK = threading.RLock()
|
|
25
25
|
|
|
26
26
|
FIELDS = ("ts", "day", "platform", "action", "task_id", "chapter_no",
|
|
27
|
-
"book_id", "title", "ok", "error", "shot"
|
|
27
|
+
"book_id", "title", "ok", "error", "shot", "operation_id",
|
|
28
|
+
"operation_status", "remote_receipt")
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
def _month_file(day):
|
|
@@ -32,7 +33,8 @@ def _month_file(day):
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
def record(platform, action, task_id="", chapter_no=0, book_id="",
|
|
35
|
-
title="", ok=True, error="", shot=""
|
|
36
|
+
title="", ok=True, error="", shot="", operation_id="",
|
|
37
|
+
operation_status="", remote_receipt=""):
|
|
36
38
|
"""追加一条发布记录。异常全吞——记账失败绝不能影响发布主流程。"""
|
|
37
39
|
try:
|
|
38
40
|
rec = {
|
|
@@ -47,6 +49,9 @@ def record(platform, action, task_id="", chapter_no=0, book_id="",
|
|
|
47
49
|
"ok": bool(ok),
|
|
48
50
|
"error": str(error or "")[:300],
|
|
49
51
|
"shot": str(shot)[:200],
|
|
52
|
+
"operation_id": str(operation_id or "")[:80],
|
|
53
|
+
"operation_status": str(operation_status or "")[:20],
|
|
54
|
+
"remote_receipt": str(remote_receipt or "")[:300],
|
|
50
55
|
}
|
|
51
56
|
with LOCK:
|
|
52
57
|
paths.PUBLISH_DIR.mkdir(parents=True, exist_ok=True)
|
|
@@ -19,10 +19,11 @@ view()(SSE/轮询)看进度;线程内任何异常都落终态,绝不悬
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import json
|
|
22
|
+
import re
|
|
22
23
|
import threading
|
|
23
24
|
import time
|
|
24
25
|
|
|
25
|
-
from .. import paths
|
|
26
|
+
from .. import operations, paths
|
|
26
27
|
from . import fanqie, flow, ledger, qimao
|
|
27
28
|
from .browser import Browser, BrowserError, Page
|
|
28
29
|
|
|
@@ -406,9 +407,13 @@ def create_book_async(task_id, plat, auto_submit=False):
|
|
|
406
407
|
data = meta["data"]
|
|
407
408
|
mod = PLATFORMS[plat]
|
|
408
409
|
logs = []
|
|
410
|
+
book_name = (data.get("book_name") or "").strip()
|
|
411
|
+
operation_id = operations.begin(
|
|
412
|
+
"publish:%s:create_book" % plat,
|
|
413
|
+
{"task_id": task_id, "title": book_name}, task_id=task_id,
|
|
414
|
+
metadata={"platform": plat, "action": "create_book"})
|
|
409
415
|
|
|
410
416
|
def run():
|
|
411
|
-
book_name = (data.get("book_name") or "").strip()
|
|
412
417
|
try:
|
|
413
418
|
b, page = _open_page(plat)
|
|
414
419
|
values = mod.values_create_book(data)
|
|
@@ -419,27 +424,49 @@ def create_book_async(task_id, plat, auto_submit=False):
|
|
|
419
424
|
shot=lambda n: page.screenshot(ledger.shot_path(plat, task_id, n)),
|
|
420
425
|
log=logs.append)
|
|
421
426
|
# book_id:创建成功后平台跳书籍详情/编辑器,从 URL 提取
|
|
422
|
-
# (番茄 book-info/<id>;七猫 information?id=<id>)。
|
|
423
|
-
#
|
|
427
|
+
# (番茄 book-info/<id>;七猫 information?id=<id>)。url_any 过了
|
|
428
|
+
# 但页面还在跳转链上时再等几轮;提取落空把最终 URL 记进日志,
|
|
429
|
+
# 别再静默登记空 id(发章只能兜底找书,直达编辑器就废了)。
|
|
424
430
|
book_id = ""
|
|
425
431
|
try:
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
432
|
+
for _try in range(6):
|
|
433
|
+
m_url = re.search(r"book-info/(\d+)|information\?id=(\d+)",
|
|
434
|
+
str(page.url() or ""))
|
|
435
|
+
if m_url:
|
|
436
|
+
book_id = m_url.group(1) or m_url.group(2)
|
|
437
|
+
break
|
|
438
|
+
time.sleep(0.8)
|
|
439
|
+
if not book_id:
|
|
440
|
+
logs.append("book_id 提取落空,最终页面:%s"
|
|
441
|
+
% str(page.url() or "")[:120])
|
|
430
442
|
except Exception:
|
|
431
443
|
pass
|
|
444
|
+
operation_status = "confirmed" if book_id else "unknown"
|
|
445
|
+
if book_id:
|
|
446
|
+
operations.confirm(operation_id, remote_receipt=book_id,
|
|
447
|
+
metadata={"platform": plat, "action": "create_book"})
|
|
448
|
+
else:
|
|
449
|
+
operations.mark_unknown(
|
|
450
|
+
operation_id,
|
|
451
|
+
"建书流程完成但未取得远端 book_id,请先在平台核对后再重试",
|
|
452
|
+
metadata={"platform": plat, "action": "create_book"})
|
|
432
453
|
ledger.record(plat, "create_book", task_id=task_id, title=book_name,
|
|
433
|
-
book_id=book_id, ok=
|
|
434
|
-
error="\n".join(logs)[:2000]
|
|
435
|
-
|
|
454
|
+
book_id=book_id, ok=bool(book_id),
|
|
455
|
+
error=("\n".join(logs)[:2000] if book_id else
|
|
456
|
+
"建书结果未知:未取得远端 book_id;请先对账"),
|
|
457
|
+
shot=str(ledger.shot_path(plat, task_id, "")),
|
|
458
|
+
operation_id=operation_id, operation_status=operation_status,
|
|
459
|
+
remote_receipt=book_id)
|
|
436
460
|
ledger.save_book(task_id, plat, {"book_id": book_id, "title": book_name})
|
|
437
461
|
_set(plat, status="connected", error="")
|
|
438
462
|
except Exception as e:
|
|
463
|
+
operations.finish_exception(operation_id, e)
|
|
464
|
+
operation_status = (operations.get(operation_id) or {}).get("status") or "failed"
|
|
439
465
|
_set(plat, status="error", error="建书失败:%s" % e)
|
|
440
466
|
ledger.record(plat, "create_book", task_id=task_id, title=book_name,
|
|
441
467
|
ok=False, error=str(e)[:300],
|
|
442
|
-
shot=str(ledger.shot_path(plat, task_id, ""))
|
|
468
|
+
shot=str(ledger.shot_path(plat, task_id, "")),
|
|
469
|
+
operation_id=operation_id, operation_status=operation_status)
|
|
443
470
|
finally:
|
|
444
471
|
_save()
|
|
445
472
|
|
|
@@ -515,6 +542,32 @@ def _login_guard(plat):
|
|
|
515
542
|
return False, "浏览器不可用:%s" % e
|
|
516
543
|
|
|
517
544
|
|
|
545
|
+
def _url_values(mod, book):
|
|
546
|
+
"""平台模块提供的 URL 占位值(chapter_manage_url/draft_url/editor_url),
|
|
547
|
+
逐键取、缺哪个跳哪个。曾按序连赋 + except AttributeError 兜底:draft_url
|
|
548
|
+
只有七猫有,番茄走到第二行即断,editor_url 永远赋不上——{editor_url}
|
|
549
|
+
残进 navigate,页面停在 about:blank 误报「未登录或改版」(0924 三连败)。"""
|
|
550
|
+
out = {}
|
|
551
|
+
for key in ("chapter_manage_url", "draft_url", "editor_url"):
|
|
552
|
+
fn = getattr(mod, key, None)
|
|
553
|
+
if callable(fn):
|
|
554
|
+
out[key] = fn(book)
|
|
555
|
+
return out
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def _resolve_book_id(plat, page, book):
|
|
559
|
+
"""登记缺 book_id 时按书名在作家后台找回(平台模块可选提供
|
|
560
|
+
resolve_book_id)。找不回返回空串:流程按无 id 兜底路径继续,让后续
|
|
561
|
+
步骤如实报错,绝不在这里拦死。找回后回写 books.json,下次直达。"""
|
|
562
|
+
fn = getattr(PLATFORMS[plat], "resolve_book_id", None)
|
|
563
|
+
if not callable(fn) or str((book or {}).get("book_id") or "").strip():
|
|
564
|
+
return ""
|
|
565
|
+
try:
|
|
566
|
+
return str(fn(page, book) or "").strip()
|
|
567
|
+
except Exception:
|
|
568
|
+
return ""
|
|
569
|
+
|
|
570
|
+
|
|
518
571
|
# ---------------------------------------------------------------- 动作:发章
|
|
519
572
|
def read_chapter(fp):
|
|
520
573
|
"""读章节文件 → (章号, 标题, 正文, 错误)。UTF-8→GBK 回退(章稿乱码教训)。
|
|
@@ -558,6 +611,10 @@ def upload_chapter_async(task_id, plat, chapter_file, auto_submit=False):
|
|
|
558
611
|
book = ledger.book_for(task_id, plat)
|
|
559
612
|
if not book:
|
|
560
613
|
return False, "该任务尚未在此平台建书,请先「创建作品」"
|
|
614
|
+
# 闭包中需要在找回 book_id 后更新书籍引用;使用独立副本避免
|
|
615
|
+
# Python 将 book 误判为 run() 的局部变量,导致前置解析触发
|
|
616
|
+
# UnboundLocalError。
|
|
617
|
+
book_ref = dict(book)
|
|
561
618
|
if _st(plat).get("status") == "busy":
|
|
562
619
|
return False, "该平台有操作正在进行中"
|
|
563
620
|
ch_no, title, body, err = read_chapter(chapter_file)
|
|
@@ -583,30 +640,42 @@ def upload_chapter_async(task_id, plat, chapter_file, auto_submit=False):
|
|
|
583
640
|
|
|
584
641
|
mod = PLATFORMS[plat]
|
|
585
642
|
values = {"chapter_title": title, "chapter_body": body,
|
|
586
|
-
"book_name":
|
|
587
|
-
|
|
588
|
-
values["chapter_manage_url"] = mod.chapter_manage_url(book)
|
|
589
|
-
values["draft_url"] = mod.draft_url(book)
|
|
590
|
-
values["editor_url"] = mod.editor_url(book)
|
|
591
|
-
except AttributeError:
|
|
592
|
-
pass
|
|
643
|
+
"book_name": book_ref.get("title") or ""}
|
|
644
|
+
values.update(_url_values(mod, book_ref)) # verify/draft/editor 页 URL(流程占位)
|
|
593
645
|
logs = []
|
|
646
|
+
operation_id = operations.begin(
|
|
647
|
+
"publish:%s:upload_chapter" % plat,
|
|
648
|
+
{"task_id": task_id, "chapter_no": ch_no, "title": title}, task_id=task_id,
|
|
649
|
+
metadata={"platform": plat, "action": "upload_chapter"})
|
|
594
650
|
|
|
595
651
|
def run():
|
|
596
652
|
try:
|
|
597
653
|
b, page = _open_page(plat)
|
|
654
|
+
bid = _resolve_book_id(plat, page, book_ref)
|
|
655
|
+
if bid: # 建书时没提上 id 的书,发章前补账
|
|
656
|
+
book_ref.update(book_id=bid)
|
|
657
|
+
values.update(_url_values(mod, book_ref))
|
|
658
|
+
ledger.save_book(task_id, plat, book_ref)
|
|
659
|
+
logs.append("已按书名找回 book_id=%s 并更新登记" % bid)
|
|
598
660
|
flow.run_flow(page, load_flow(plat, "upload_chapter"), values=values,
|
|
599
661
|
config=mod.CONFIG, auto_submit=auto_submit,
|
|
600
662
|
shot=lambda n: page.screenshot(ledger.shot_path(plat, task_id, n)),
|
|
601
663
|
log=logs.append)
|
|
664
|
+
operations.confirm(operation_id, metadata={"platform": plat,
|
|
665
|
+
"action": "upload_chapter",
|
|
666
|
+
"chapter_no": ch_no})
|
|
602
667
|
ledger.record(plat, "upload_chapter", task_id=task_id, chapter_no=ch_no,
|
|
603
|
-
book_id=
|
|
668
|
+
book_id=book_ref.get("book_id") or "", title=title, ok=True,
|
|
669
|
+
operation_id=operation_id, operation_status="confirmed")
|
|
604
670
|
_set(plat, status="connected", error="")
|
|
605
671
|
except Exception as e:
|
|
672
|
+
operations.finish_exception(operation_id, e)
|
|
673
|
+
operation_status = (operations.get(operation_id) or {}).get("status") or "failed"
|
|
606
674
|
_set(plat, status="error", error="发章失败:%s" % e)
|
|
607
675
|
ledger.record(plat, "upload_chapter", task_id=task_id, chapter_no=ch_no,
|
|
608
|
-
book_id=
|
|
609
|
-
ok=False, error=str(e)[:300]
|
|
676
|
+
book_id=book_ref.get("book_id") or "", title=title,
|
|
677
|
+
ok=False, error=str(e)[:300], operation_id=operation_id,
|
|
678
|
+
operation_status=operation_status)
|
|
610
679
|
finally:
|
|
611
680
|
_save()
|
|
612
681
|
|