codebee 0.1.98 → 0.1.100

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.
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "_note": "七猫真机校准(2026-09-19 凌晨定稿):建书=book-manage「新建小说」→站点弹层(click_match+确认)→information 表单(fill_label/radio/级联分类/tags 四组各1-3个+确定/主角名平铺/简介);书名仅允许,:!?中文标点(·被拦)。发章=编辑器填好后「立即发布」落草稿箱(未签约书限制)→草稿行「立即发布」→「更正序号去发布」→「确认发布」三层弹窗→章节待审核。正文下限 1000 字(min_chapter_chars)。book-upload?id=<书id> 直达编辑器。;2026-09-24 加固:二级分类选完 settle 1s 再点「添加标签」(下拉关闭动画吞点击致标签弹层未开、组名切换失败案);2026-09-24 夜修:平台编辑器「立即发布」变为弹重型确认框(点后页面假死、章节未提交、草稿箱亦空——旧「立即发布落草稿箱」语义过时),改点「存草稿」再走草稿箱三层发布(草稿行立即发布→更正序号→确认发布,22:47 实证成功路径)",
3
+ "upload_chapter_draft": "2026-10-09 全部发草稿:按 upload_chapter 的「存草稿」段拆出(填标题/正文→click_real 存草稿→draft_url 草稿箱 verify 行在场)。存草稿段是 0924 实证路径的前半段,选择器同源;提交发布(立即发布三层弹窗)留给用户在草稿箱手工完成。",
3
4
  "probe_form": [
4
5
  {
5
6
  "do": "navigate",
@@ -226,6 +227,63 @@
226
227
  "settle": 6
227
228
  }
228
229
  ],
230
+ "upload_chapter_draft": [
231
+ {
232
+ "do": "navigate",
233
+ "url": "about:blank"
234
+ },
235
+ {
236
+ "do": "navigate",
237
+ "url": "{editor_url}"
238
+ },
239
+ {
240
+ "do": "url_any",
241
+ "any": [
242
+ "qimao.com"
243
+ ]
244
+ },
245
+ {
246
+ "do": "wait",
247
+ "sel": "textarea[placeholder*='章节名称']",
248
+ "timeout": 20
249
+ },
250
+ {
251
+ "do": "fill",
252
+ "sel": "textarea[placeholder*='章节名称']",
253
+ "key": "chapter_title"
254
+ },
255
+ {
256
+ "do": "wait",
257
+ "sel": ".q-contenteditable",
258
+ "timeout": 12
259
+ },
260
+ {
261
+ "do": "fill",
262
+ "sel": ".q-contenteditable",
263
+ "key": "chapter_body"
264
+ },
265
+ {
266
+ "do": "shot",
267
+ "name": "chapter-filled-draft"
268
+ },
269
+ {
270
+ "do": "click_real",
271
+ "text": "存草稿",
272
+ "tries": 25
273
+ },
274
+ {
275
+ "do": "sleep",
276
+ "s": 2
277
+ },
278
+ {
279
+ "do": "verify",
280
+ "url": "{draft_url}",
281
+ "any": [
282
+ "{chapter_title}"
283
+ ],
284
+ "settle": 5
285
+ }
286
+ ],
229
287
  "check_login": [
230
288
  {
231
289
  "do": "navigate",
@@ -111,6 +111,28 @@ def published_chapters(task_id, platform):
111
111
  return out
112
112
 
113
113
 
114
+ def drafted_chapters(task_id, platform):
115
+ """该任务在该平台已成功存草稿的章节号集合(发草稿模式的幂等口径)。
116
+
117
+ 与 published_chapters 同款连载链合并;不含校准的平台实况(实况口径里
118
+ 没有「草稿」桶,草稿章是否已由用户手工发布以重新校准后的
119
+ remote_published_nos 为准)。草稿章从待发清单剔除:已在平台存稿的章
120
+ 再跑一遍只会造出重复草稿。"""
121
+ from .. import store
122
+ try:
123
+ ids = set(store.serial_chain_ids(task_id))
124
+ except Exception:
125
+ ids = {str(task_id)}
126
+ out = set()
127
+ for r in _iter_records(3650):
128
+ if (r.get("action") == "upload_chapter_draft" and r.get("ok")
129
+ and r.get("task_id") in ids and r.get("platform") == platform):
130
+ n = r.get("chapter_no") or 0
131
+ if n > 0:
132
+ out.add(int(n))
133
+ return out
134
+
135
+
114
136
  def recent(task_id=None, platform=None, limit=50):
115
137
  """最近记录(新在前),详情页发布历史用。task_id 给定时按连载链合并。"""
116
138
  from .. import store
@@ -338,7 +360,8 @@ def consecutive_failures(platform):
338
360
  for r in reversed(list(_iter_records(7))):
339
361
  if r.get("platform") != platform:
340
362
  continue
341
- if r.get("action") not in ("upload_chapter", "create_book"):
363
+ if r.get("action") not in ("upload_chapter", "create_book",
364
+ "upload_chapter_draft"):
342
365
  continue
343
366
  t = _rec_time(r)
344
367
  if t is None:
@@ -872,8 +872,14 @@ def read_chapter(fp):
872
872
 
873
873
 
874
874
  def upload_chapter_async(task_id, plat, chapter_file, auto_submit=False,
875
- force=False, force_confirmed=False, force_reason=""):
876
- """把一章发到平台(或填好待人工确认)。幂等:已成功发布的章号拒绝重发。"""
875
+ force=False, force_confirmed=False, force_reason="",
876
+ as_draft=False):
877
+ """把一章发到平台(或填好待人工确认/as_draft 只存草稿)。
878
+
879
+ 幂等:已成功发布**或已存草稿**的章号拒绝重做——重复发会平台上出双章,
880
+ 重复存草稿会攒一箱重复稿。as_draft=True 走「存草稿」流程表
881
+ (upload_chapter_draft):内容落平台草稿箱不上线,不构成发布动作,
882
+ 质量闸不拦(发布时照拦);存稿动作无需人工确认,适合批量连跑。"""
877
883
  from .. import store
878
884
  if plat not in PLATFORMS:
879
885
  return False, "未知平台"
@@ -894,9 +900,14 @@ def upload_chapter_async(task_id, plat, chapter_file, auto_submit=False,
894
900
  ch_no, title, body, err = read_chapter(chapter_file)
895
901
  if err:
896
902
  return False, err
897
- quality = _quality_release_guard(
898
- task, plat, action="publish", target_chapter=ch_no or None,
899
- force=force, force_confirmed=force_confirmed, force_reason=force_reason)
903
+ if as_draft:
904
+ # 草稿不上线:质量闸不适用(evaluate_release action=draft 直通),
905
+ # 人工复核挪到「平台草稿箱 → 用户逐章检查后提交」那一步。
906
+ quality = {"allowed": True}
907
+ else:
908
+ quality = _quality_release_guard(
909
+ task, plat, action="publish", target_chapter=ch_no or None,
910
+ force=force, force_confirmed=force_confirmed, force_reason=force_reason)
900
911
  if not quality.get("allowed"):
901
912
  return False, "质量门禁拦截:%s" % ";".join(quality.get("blockers") or [])
902
913
  n_chars = len(body.replace("\n", "").replace(" ", ""))
@@ -912,10 +923,14 @@ def upload_chapter_async(task_id, plat, chapter_file, auto_submit=False,
912
923
  done = ledger.published_chapters(task_id, plat)
913
924
  if ch_no and ch_no in done:
914
925
  return False, "第 %d 章已成功发布过(台账幂等拦截);确需重发请手工处理" % ch_no
926
+ if as_draft and ch_no and ch_no in ledger.drafted_chapters(task_id, plat):
927
+ return False, ("第 %d 章已存过草稿(台账幂等拦截);请到平台草稿箱检查后"
928
+ "提交发布,确需重存请先在平台删旧稿" % ch_no)
915
929
  ok_login, why = _login_guard(plat)
916
930
  if not ok_login:
917
931
  return False, why
918
- _set(plat, status="busy", last_action="upload_chapter", error="")
932
+ flow_key = "upload_chapter_draft" if as_draft else "upload_chapter"
933
+ _set(plat, status="busy", last_action=flow_key, error="")
919
934
 
920
935
  mod = PLATFORMS[plat]
921
936
  values = chapter_fill_values(ch_no, title, body,
@@ -934,7 +949,7 @@ def upload_chapter_async(task_id, plat, chapter_file, auto_submit=False,
934
949
  operation_id = operations.begin(
935
950
  "publish:%s:upload_chapter" % plat,
936
951
  {"task_id": task_id, "chapter_no": ch_no, "title": title}, task_id=task_id,
937
- metadata={"platform": plat, "action": "upload_chapter"})
952
+ metadata={"platform": plat, "action": flow_key})
938
953
 
939
954
  def run():
940
955
  try:
@@ -948,14 +963,26 @@ def upload_chapter_async(task_id, plat, chapter_file, auto_submit=False,
948
963
  elif not book_ref.get("book_id"):
949
964
  raise RuntimeError("未按作品名找到远端作品,未打开章节编辑器;"
950
965
  "请检查平台作品名或补填作品 ID")
951
- flow.run_flow(page, load_flow(plat, "upload_chapter"), values=values,
966
+ flow.run_flow(page, load_flow(plat, flow_key), values=values,
952
967
  config=mod.CONFIG, auto_submit=auto_submit,
953
968
  shot=lambda n: page.screenshot(ledger.shot_path(plat, task_id, n)),
954
969
  log=logs.append)
955
970
  new_vols = values.get("_volumes")
956
971
  if isinstance(new_vols, list) and new_vols != (book_ref.get("volumes") or []):
957
972
  ledger.update_book(task_id, plat, volumes=new_vols)
958
- if auto_submit:
973
+ if as_draft:
974
+ # 草稿已落平台草稿箱(流程含验证步,走到这=验证过)。发布确认权
975
+ # 仍在用户:到平台草稿箱逐章检查后手工提交,质量闸在「发布」时
976
+ # 照常把关。
977
+ operations.confirm(operation_id, metadata={"platform": plat,
978
+ "action": "upload_chapter_draft",
979
+ "chapter_no": ch_no})
980
+ ledger.record(plat, "upload_chapter_draft", task_id=task_id,
981
+ chapter_no=ch_no, book_id=book_ref.get("book_id") or "",
982
+ title=title, ok=True, operation_id=operation_id,
983
+ operation_status="confirmed",
984
+ remote_receipt="platform-draft")
985
+ elif auto_submit:
959
986
  operations.confirm(operation_id, metadata={"platform": plat,
960
987
  "action": "upload_chapter",
961
988
  "chapter_no": ch_no})
@@ -60,6 +60,22 @@ UPLOAD_CHAPTER = [
60
60
  {"do": "submit", "sel": "button[class*=publish],button[class*=submit]"},
61
61
  ]
62
62
 
63
+ # 发章存草稿(2026-10-09 全部发草稿):内置推测表——真机校准表在
64
+ # flows-qimao-calibrated.json(按其 upload_chapter 的「存草稿」段拆出:
65
+ # 填好→存草稿→草稿箱验证行在场即成)。草稿不上线;提交发布由用户在
66
+ # 草稿箱手工点(立即发布→更正序号→确认发布三层弹窗照旧人工走)。
67
+ UPLOAD_CHAPTER_DRAFT = [
68
+ {"do": "navigate", "url": "{editor_url}"},
69
+ {"do": "url_any", "any": ["qimao.com"]},
70
+ {"do": "fill", "sel": "textarea[placeholder*='章节名称']", "key": "chapter_title"},
71
+ {"do": "wait", "sel": "[contenteditable=true],textarea[class*=content],iframe", "timeout": 10},
72
+ {"do": "fill", "sel": "[contenteditable=true],textarea[class*=content]", "key": "chapter_body"},
73
+ {"do": "shot", "name": "chapter-filled-draft"},
74
+ {"do": "click_real", "text": "存草稿", "tries": 25},
75
+ {"do": "sleep", "s": 2},
76
+ {"do": "verify", "url": "{draft_url}", "any": ["{chapter_title}"], "settle": 5},
77
+ ]
78
+
63
79
  CHECK_LOGIN = [
64
80
  {"do": "navigate", "url": "{home}"},
65
81
  {"do": "url_any", "any": ["qimao.com"]},
@@ -73,6 +89,7 @@ PROBE_FORM = [
73
89
  ]
74
90
 
75
91
  FLOWS = {"create_book": CREATE_BOOK, "upload_chapter": UPLOAD_CHAPTER,
92
+ "upload_chapter_draft": UPLOAD_CHAPTER_DRAFT,
76
93
  "check_login": CHECK_LOGIN, "probe_form": PROBE_FORM}
77
94
 
78
95
 
@@ -127,6 +127,13 @@ def evaluate_release(verdict, action="publish", target_chapter=None,
127
127
  if action == "continue":
128
128
  return {"allowed": True, "status": "continue_only", "forced": False,
129
129
  "blockers": [], "warnings": []}
130
+ if action == "draft":
131
+ # 存草稿不是发布:内容不公开展示(读者不可见),正式提交前用户还会
132
+ # 在平台上逐章过目——质量闸只在「发布/签约」这类上线动作上拦,
133
+ # 拦草稿只会把「先落平台再人工把关」的通路堵死(2026-10-09 全部
134
+ # 发草稿需求:质量未达标的 53 章先存草稿,发布仍受本闸约束)。
135
+ return {"allowed": True, "status": "draft_only", "forced": False,
136
+ "blockers": [], "warnings": []}
130
137
 
131
138
  blockers = []
132
139
  warnings = []
@@ -88,6 +88,12 @@ def effective_agents(catalog_entries, detected):
88
88
  if not pref.get("enabled", entry.get("default_enabled", False)):
89
89
  continue
90
90
  out.append(_build_agent(entry))
91
+ try:
92
+ from . import backend_profiles
93
+ out.extend(backend_profiles.runtime_agents())
94
+ except Exception:
95
+ # Broken local profile storage must not disable unrelated installed agents.
96
+ pass
91
97
  out.extend([dict(m) for m in MOCK_AGENTS])
92
98
  return out
93
99
 
@@ -98,6 +104,13 @@ def installed_agent(entry_id, catalog_entries, detected):
98
104
  续会话是用户对该 CLI 的显式指定,不依赖其是否参与自动路由;
99
105
  未安装 / 无编排配置 / id 不存在时返回 None。
100
106
  """
107
+ if str(entry_id or "").startswith("acp-"):
108
+ try:
109
+ from . import backend_profiles
110
+ return next((agent for agent in backend_profiles.runtime_agents()
111
+ if agent.get("id") == entry_id), None)
112
+ except Exception:
113
+ return None
101
114
  for entry in catalog_entries:
102
115
  if entry.get("id") != entry_id:
103
116
  continue
@@ -1678,7 +1678,8 @@ def _opencode_fast_banner_death(res):
1678
1678
 
1679
1679
  def run_agent(agent, prompt, workdir=None, readonly=True,
1680
1680
  timeout=DEFAULT_TIMEOUT, cancel_event=None, log_path=None, resume=None,
1681
- images=None, require_tools=False, deadline=None):
1681
+ images=None, require_tools=False, deadline=None, command_context=None,
1682
+ checkpoint_run_id=""):
1682
1683
  """执行一次智能体调用,返回统一结构
1683
1684
  {ok, text, json, cost_usd, tokens, error, error_code, raw}。
1684
1685
  agent 来自 registry.effective_agents();resume 为已有会话 id,仅真实智能体生效
@@ -1722,6 +1723,17 @@ def run_agent(agent, prompt, workdir=None, readonly=True,
1722
1723
  "kind": agent.get("kind", "generic"), "model": None,
1723
1724
  "attempts": []}
1724
1725
  kind = agent.get("kind", "generic")
1726
+ if kind == "acp":
1727
+ from . import acp_client
1728
+ context = dict(command_context or {})
1729
+ context.setdefault("run_id", checkpoint_run_id)
1730
+ context.setdefault("sandbox", {})
1731
+ context.setdefault("allow_terminal", not readonly)
1732
+ return acp_client.run_agent(
1733
+ agent, prompt, workdir=workdir, readonly=readonly, timeout=timeout,
1734
+ cancel_event=cancel_event, deadline=deadline,
1735
+ command_context=context, checkpoint_run_id=checkpoint_run_id,
1736
+ resume=resume)
1725
1737
  if agent.get("runtime_config_sync_failed"):
1726
1738
  return {"ok": False, "text": "", "json": None, "cost_usd": 0.0,
1727
1739
  "tokens": 0, "usage": None,
package/app/main.py CHANGED
@@ -358,6 +358,15 @@ class Handler(BaseHTTPRequestHandler):
358
358
  return self._json(200, {"entries": project_memory.list_entries(task.get("workdir"))})
359
359
  except (OSError, ValueError) as exc:
360
360
  return self._json(409, {"error": str(exc)})
361
+ if path == "/api/backend-profiles":
362
+ from core import backend_profiles
363
+ try:
364
+ return self._json(200, {"profiles": backend_profiles.list_profiles()})
365
+ except (OSError, ValueError) as exc:
366
+ return self._json(409, {"error": str(exc)})
367
+ if path == "/api/command-auth":
368
+ from core import command_auth
369
+ return self._json(200, command_auth.view())
361
370
  if path == "/api/agents/presence":
362
371
  from core import presence
363
372
  return self._json(200, {"agents": presence.list_agents()})
@@ -887,6 +896,58 @@ class Handler(BaseHTTPRequestHandler):
887
896
  return deny
888
897
  if path == "/api/tasks":
889
898
  return self._api_create_task()
899
+ if path == "/api/backend-profiles":
900
+ from core import backend_profiles
901
+ try:
902
+ return self._json(200, {"profile": backend_profiles.save(self._body() or {})})
903
+ except KeyError as exc:
904
+ return self._json(404, {"error": str(exc)})
905
+ except (OSError, TypeError, ValueError) as exc:
906
+ return self._json(400, {"error": str(exc)})
907
+ m = re.match(r"^/api/backend-profiles/([^/]+)/delete$", path)
908
+ if m:
909
+ from core import backend_profiles
910
+ try:
911
+ backend_profiles.remove(unquote(m.group(1)))
912
+ store.bump_state()
913
+ return self._json(200, {"ok": True})
914
+ except KeyError as exc:
915
+ return self._json(404, {"error": str(exc)})
916
+ except (OSError, TypeError, ValueError) as exc:
917
+ return self._json(400, {"error": str(exc)})
918
+ if path == "/api/command-auth":
919
+ from core import command_auth
920
+ body = self._body() or {}
921
+ try:
922
+ if "mode" in body:
923
+ command_auth.set_mode(body.get("mode"))
924
+ return self._json(200, command_auth.view())
925
+ except (TypeError, ValueError) as exc:
926
+ return self._json(400, {"error": str(exc)})
927
+ m = re.match(r"^/api/command-auth/requests/([^/]+)$", path)
928
+ if m:
929
+ from core import command_auth
930
+ body = self._body() or {}
931
+ try:
932
+ result = command_auth.decide(
933
+ unquote(m.group(1)), approve=body.get("approve"),
934
+ scope=body.get("scope", "once"), match=body.get("match", "exact"),
935
+ prefix=body.get("prefix"))
936
+ return self._json(200, result)
937
+ except KeyError as exc:
938
+ return self._json(404, {"error": str(exc)})
939
+ except (TypeError, ValueError) as exc:
940
+ return self._json(400, {"error": str(exc)})
941
+ m = re.match(r"^/api/command-auth/rules/([^/]+)/revoke$", path)
942
+ if m:
943
+ from core import command_auth
944
+ try:
945
+ command_auth.revoke(unquote(m.group(1)))
946
+ return self._json(200, {"ok": True})
947
+ except KeyError as exc:
948
+ return self._json(404, {"error": str(exc)})
949
+ except (TypeError, ValueError) as exc:
950
+ return self._json(400, {"error": str(exc)})
890
951
  if path == "/api/a2a/tasks":
891
952
  status, resp = self._create_and_start(self._body() or {})
892
953
  return self._json(status, {"protocol": "a2a-lite", **resp})
@@ -915,6 +976,11 @@ class Handler(BaseHTTPRequestHandler):
915
976
  body = self._body() or {}
916
977
  from core import project_memory
917
978
  try:
979
+ if str(body.get("action") or "").lower() == "feedback":
980
+ row = project_memory.feedback(
981
+ task.get("workdir"), body.get("id"), useful=body.get("useful"),
982
+ expected_version=body.get("expected_version"))
983
+ return self._json(200, {"entry": row})
918
984
  row = project_memory.decide(task.get("workdir"), body.get("id"),
919
985
  body.get("action"),
920
986
  expected_version=body.get("expected_version"))
@@ -2263,7 +2329,9 @@ class Handler(BaseHTTPRequestHandler):
2263
2329
  """批量发布全部待发章节(publish/auto.py)。
2264
2330
 
2265
2331
  护栏(每日上限/连败退避/幂等/单飞)在 auto 层,每章发起前复查;
2266
- auto_submit 默认 false——每章表单填好后停,提交权留给用户。"""
2332
+ auto_submit 默认 false——每章表单填好后停,提交权留给用户。
2333
+ as_draft=true 走「全部发草稿」:逐章存草稿不上线、不受质量闸拦、
2334
+ 无需人工确认连跑到清空(2026-10-09);草稿与直发互斥,草稿优先。"""
2267
2335
  from core import store
2268
2336
  from core.publish import auto as pub_auto
2269
2337
  if not store.get_task(task_id):
@@ -2276,7 +2344,8 @@ class Handler(BaseHTTPRequestHandler):
2276
2344
  force_confirmed = bool(body.get("force_confirmed"))
2277
2345
  force_reason = str(body.get("force_reason") or "").strip()
2278
2346
  only = body.get("chapters") # 批量选择发布:章号数组(可省=全部待发)
2279
- kwargs = {"only": only if isinstance(only, list) else None}
2347
+ kwargs = {"only": only if isinstance(only, list) else None,
2348
+ "as_draft": bool(body.get("as_draft"))}
2280
2349
  if force or force_confirmed or force_reason:
2281
2350
  kwargs.update(force=force, force_confirmed=force_confirmed,
2282
2351
  force_reason=force_reason)
@@ -3477,6 +3546,10 @@ def main():
3477
3546
  print("[CodeBee] v%s" % (_ver or "?"), flush=True)
3478
3547
  _step("正在准备数据目录…")
3479
3548
  paths.ensure_dirs()
3549
+ from core import backend_profiles as _backend_profiles
3550
+ from core import command_auth as _command_auth
3551
+ _backend_profiles.init()
3552
+ _command_auth.init()
3480
3553
  from core import attachments
3481
3554
  n_pc = attachments.cleanup_stale() # 待提交附件残留清理(崩溃/弃单不堆积)
3482
3555
  if n_pc: