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.
- package/CHANGELOG.md +12 -1
- package/README.md +4 -4
- package/app/core/acp_client.py +426 -0
- package/app/core/backend_profiles.py +188 -0
- package/app/core/builtin_agent.py +48 -37
- package/app/core/command_auth.py +302 -0
- package/app/core/gitmod.py +6 -2
- package/app/core/jobs.py +3 -2
- package/app/core/manager.py +59 -4
- package/app/core/market_remote.py +20 -7
- package/app/core/modelhub.py +21 -5
- package/app/core/openapi.py +32 -0
- package/app/core/pipeline.py +34 -9
- package/app/core/project_memory.py +73 -6
- package/app/core/publish/auto.py +91 -20
- package/app/core/publish/fanqie.py +30 -0
- package/app/core/publish/flows-fanqie-calibrated.json +81 -0
- package/app/core/publish/flows-qimao-calibrated.json +58 -0
- package/app/core/publish/ledger.py +24 -1
- package/app/core/publish/manager.py +36 -9
- package/app/core/publish/qimao.py +17 -0
- package/app/core/quality_gate.py +7 -0
- package/app/core/registry.py +13 -0
- package/app/core/runner.py +13 -1
- package/app/main.py +75 -2
- package/app/ui/app.js +167 -15
- package/app/ui/board.css +5 -1
- package/app/ui/board.js +16 -0
- package/app/ui/i18n.js +26 -1
- package/app/ui/index.html +20 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
898
|
-
|
|
899
|
-
|
|
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
|
-
|
|
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":
|
|
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,
|
|
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
|
|
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
|
|
package/app/core/quality_gate.py
CHANGED
|
@@ -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 = []
|
package/app/core/registry.py
CHANGED
|
@@ -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
|
package/app/core/runner.py
CHANGED
|
@@ -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:
|