codebee 0.1.99 → 0.1.101
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 +14 -0
- package/README.md +6 -5
- package/app/core/acp_client.py +426 -0
- package/app/core/backend_profiles.py +188 -0
- package/app/core/builtin_agent.py +12 -0
- 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/openapi.py +32 -0
- package/app/core/pipeline.py +110 -9
- package/app/core/plugins.py +9 -2
- package/app/core/project_memory.py +73 -6
- package/app/core/registry.py +13 -0
- package/app/core/runner.py +13 -1
- package/app/core/store.py +18 -1
- package/app/main.py +70 -0
- package/app/ui/app.js +82 -2
- package/app/ui/index.html +19 -0
- package/package.json +1 -1
package/app/core/pipeline.py
CHANGED
|
@@ -596,6 +596,16 @@ def _external_sandbox_block_reason(agent, workdir, sandbox):
|
|
|
596
596
|
if normalized.get("env_allowlist"):
|
|
597
597
|
return "沙箱拒绝:该 CLI 后端未接入环境变量白名单,拒绝运行受限任务"
|
|
598
598
|
return ""
|
|
599
|
+
if (agent or {}).get("kind") == "acp":
|
|
600
|
+
if normalized.get("network") is False:
|
|
601
|
+
return "沙箱拒绝:ACP 后端进程未接入网络隔离,拒绝运行网络受限任务"
|
|
602
|
+
if normalized.get("env_allowlist"):
|
|
603
|
+
return "沙箱拒绝:ACP 后端进程未接入环境变量白名单,拒绝运行受限任务"
|
|
604
|
+
roots = [os.path.realpath(x) for x in normalized.get("allowed_roots") or []]
|
|
605
|
+
if roots and not any(root == allowed or root.startswith(allowed + os.sep)
|
|
606
|
+
for allowed in roots):
|
|
607
|
+
return "沙箱拒绝:任务目录不在 ACP 允许目录内"
|
|
608
|
+
return ""
|
|
599
609
|
# 非 codex 外部 CLI:默认策略放行,收窄策略拒绝
|
|
600
610
|
if normalized.get("network") is False:
|
|
601
611
|
return "沙箱拒绝:该任务要求禁网,外部 CLI 无法执行网络受限任务"
|
|
@@ -818,7 +828,13 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
818
828
|
llm_caller = _make_llm_caller(agent, workdir, deadline=deadline)
|
|
819
829
|
call_kwargs = dict(workdir=workdir, readonly=readonly,
|
|
820
830
|
timeout=timeout, cancel_event=ev, log_path=str(log_abs),
|
|
821
|
-
images=images, require_tools=require_tools, deadline=deadline
|
|
831
|
+
images=images, require_tools=require_tools, deadline=deadline,
|
|
832
|
+
command_context={"run_id": session_run_id,
|
|
833
|
+
"sandbox": (store.get_task(
|
|
834
|
+
(store.get_run(session_run_id) or {}).get("task_id")
|
|
835
|
+
) or {}).get("sandbox") or {},
|
|
836
|
+
"allow_terminal": not readonly},
|
|
837
|
+
checkpoint_run_id=session_run_id)
|
|
822
838
|
|
|
823
839
|
def _call(p, **kw):
|
|
824
840
|
nonlocal usage_recorded
|
|
@@ -846,7 +862,13 @@ def _spawn_step(session_run_id, role, agent, prompt, workdir, readonly, ev,
|
|
|
846
862
|
res = runner.run_agent(agent, prompt, workdir=workdir, readonly=readonly,
|
|
847
863
|
timeout=timeout, cancel_event=ev, log_path=str(log_abs),
|
|
848
864
|
resume=resume, images=images, require_tools=require_tools,
|
|
849
|
-
deadline=deadline
|
|
865
|
+
deadline=deadline,
|
|
866
|
+
command_context={"run_id": session_run_id,
|
|
867
|
+
"sandbox": (store.get_task(
|
|
868
|
+
(store.get_run(session_run_id) or {}).get("task_id")
|
|
869
|
+
) or {}).get("sandbox") or {},
|
|
870
|
+
"allow_terminal": not readonly},
|
|
871
|
+
checkpoint_run_id=session_run_id)
|
|
850
872
|
# 默认关闭压缩和 resume 都走直通分支,也必须把真实 usage 送进预算表;否则
|
|
851
873
|
# 下一步永远看到 used=0,max_tokens_per_run 只是一个无效设置。
|
|
852
874
|
if not usage_recorded:
|
|
@@ -1561,7 +1583,8 @@ def _run_code(run, task, agents, ev, stats, mode):
|
|
|
1561
1583
|
|
|
1562
1584
|
# 项目记忆既给规划器,也给快速路径的实现者;不能因省掉独立规划而漏掉。
|
|
1563
1585
|
user_context_len = len(task.get("context") or "")
|
|
1564
|
-
project_memory = _read_project_memory(
|
|
1586
|
+
project_memory = _read_project_memory(
|
|
1587
|
+
workdir, query=task.get("goal") or task.get("title") or "")
|
|
1565
1588
|
if project_memory:
|
|
1566
1589
|
task = dict(task, context=(task.get("context") or "") + "\n\n" + project_memory)
|
|
1567
1590
|
|
|
@@ -2568,6 +2591,36 @@ __CRITIQUE__
|
|
|
2568
2591
|
- 针对性改进所有 major 问题;保持既定风格与设定。
|
|
2569
2592
|
- 完成后用 3 句话说明本轮改了什么。"""
|
|
2570
2593
|
|
|
2594
|
+
# 评审口径单一真源:评审面提示词(单章/全局)与作者面注入块从这里同源提取,
|
|
2595
|
+
# 两边永不脱节。此前评审清单/签约专项/全局口径只活在评审面,作者第一遍写完
|
|
2596
|
+
# 被打回才知道按什么标准被审(2026-10-10 实案:全章过线仍卡全局 major)。
|
|
2597
|
+
# 作者面刻意不下发评分机制本体(1-10 分/阈值/宁严勿宽)——对写作者零价值,
|
|
2598
|
+
# 透下去只喂响应试素材;清单条目本身全是质量正向要求,事前下发只有好处。
|
|
2599
|
+
REVIEW_CHECKLIST_ITEMS = (
|
|
2600
|
+
"叙述语域与视角统一",
|
|
2601
|
+
"复杂情感落到动作、五感或生理细节",
|
|
2602
|
+
"开篇在前 600 字进入具体冲突",
|
|
2603
|
+
"每章发生真实的局面变化",
|
|
2604
|
+
"无可删的重复铺垫与长段解释",
|
|
2605
|
+
)
|
|
2606
|
+
GLOBAL_REVIEW_FOCUS = ("主线一致性、人物弧光、开篇吸引力、文风表现、情感表达、"
|
|
2607
|
+
"情节推进与节奏控制")
|
|
2608
|
+
|
|
2609
|
+
|
|
2610
|
+
def author_review_rubric_block(dims, signing_dims):
|
|
2611
|
+
"""作者面评审口径块:起草/修订提示词里事前下发「会按什么标准被审」,
|
|
2612
|
+
与评审面同源(REVIEW_CHECKLIST_ITEMS / GLOBAL_REVIEW_FOCUS)。静态文本,
|
|
2613
|
+
同批各章字节一致,不碎前缀缓存。"""
|
|
2614
|
+
lines = [
|
|
2615
|
+
"## 评审口径(本书会按以下标准审稿,写作时对照自查)",
|
|
2616
|
+
"- 评分维度:" + " / ".join(dims) + "。",
|
|
2617
|
+
"- 逐项硬清单:" + ";".join(REVIEW_CHECKLIST_ITEMS) + "。",
|
|
2618
|
+
"- 全书级关注:" + GLOBAL_REVIEW_FOCUS + "。",
|
|
2619
|
+
"- 签约专项维度:" + "、".join(signing_dims) + "。",
|
|
2620
|
+
]
|
|
2621
|
+
return "\n".join(lines) + "\n"
|
|
2622
|
+
|
|
2623
|
+
|
|
2571
2624
|
NOVEL_CRITIQUE_PROMPT = """你是严格的评审(不要使用任何工具、不要修改文件,只依据下方稿件内容评审)。
|
|
2572
2625
|
请输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
|
|
2573
2626
|
{
|
|
@@ -2576,7 +2629,7 @@ NOVEL_CRITIQUE_PROMPT = """你是严格的评审(不要使用任何工具、
|
|
|
2576
2629
|
"summary": "一句话总评"
|
|
2577
2630
|
}
|
|
2578
2631
|
每个维度打 1-10 分(可为小数),宁严勿宽。major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
|
|
2579
|
-
|
|
2632
|
+
评审逐项核查以下各项是否成立:%s。""" % (";".join(REVIEW_CHECKLIST_ITEMS)) + """
|
|
2580
2633
|
|
|
2581
2634
|
## 待评审稿件
|
|
2582
2635
|
---
|
|
@@ -2862,6 +2915,7 @@ __VOLUME__
|
|
|
2862
2915
|
- 本章主要情绪/戏剧变化:__HIGHLIGHT__(按其重要性决定篇幅和表现方式;不强制放大,不为满足标签伪造高潮)
|
|
2863
2916
|
__LOCKED_CH__
|
|
2864
2917
|
__STALE_NOTE__
|
|
2918
|
+
__REDO_ISSUES__
|
|
2865
2919
|
- 正文约 __WORDS__ 字,中文,直接开写正文(可含本章标题行)。
|
|
2866
2920
|
|
|
2867
2921
|
## 前情提要(此前各章结尾摘录,衔接用)
|
|
@@ -2876,6 +2930,7 @@ __STYLE__
|
|
|
2876
2930
|
## 签约质量门禁
|
|
2877
2931
|
__QUALITY_GATE__
|
|
2878
2932
|
|
|
2933
|
+
__REVIEW_RUBRIC__
|
|
2879
2934
|
- 写完文件后,最终回复只输出一行:`第 __I__ 章完成(约 __WORDS__ 字)`——不要在回复里复述或解释正文。"""
|
|
2880
2935
|
|
|
2881
2936
|
SERIAL_REVISE_PROMPT = """你是一名网文作者。第 __I__ 章没有通过评审,请修订文件 `__FILE__`(直接改写该文件)。文件必须以 UTF-8 编码保存(PowerShell 显式加 -Encoding UTF8,禁止依赖默认编码)。
|
|
@@ -2890,6 +2945,7 @@ __BEATS__
|
|
|
2890
2945
|
(本章爽点/情绪爆点:__HIGHLIGHT__)
|
|
2891
2946
|
__LOCKED_CH__
|
|
2892
2947
|
|
|
2948
|
+
__REVIEW_RUBRIC__
|
|
2893
2949
|
## 本章评审意见
|
|
2894
2950
|
__CRITIQUE__
|
|
2895
2951
|
|
|
@@ -2913,7 +2969,7 @@ SERIAL_GLOBAL_PROMPT = """你是独立小说质量评审(不要修改任何文
|
|
|
2913
2969
|
"issues": [{"dim": "维度名", "severity": "major|minor", "note": "具体问题(指明哪一章)", "quote": "支撑该问题的稿件原文连续片段(≥8字,逐字摘录不许改写)"}],
|
|
2914
2970
|
"summary": "一句话总评:所读范围内的文本优势、主要风险与未验证项;不得预测签约结果"
|
|
2915
2971
|
}
|
|
2916
|
-
每个维度打 1-10 分,按统一文本 rubric
|
|
2972
|
+
每个维度打 1-10 分,按统一文本 rubric 与所读证据评估,不以“宁严勿宽”压分。重点关注:%s。""" % GLOBAL_REVIEW_FOCUS + """
|
|
2917
2973
|
若是全书最初的前 3 章,可观察视角人物、故事承诺、开篇兑现和读者理解成本;固定字数点、钩子/爽点密度仅作参考,不是扣分门槛。发现问题须给可定位原文引文,并区分文本事实、解释和建议。
|
|
2918
2974
|
major 问题必须给 quote(系统会逐条校验引文是否真在稿件中——编造的引文会被降档标记)。
|
|
2919
2975
|
|
|
@@ -3319,6 +3375,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3319
3375
|
# 签约专项维度作为额外硬性检查注入提示词与报告证据。
|
|
3320
3376
|
dims = task.get("rubric") or DEFAULT_RUBRIC
|
|
3321
3377
|
signing_dims = novel_quality.rubric_for({"type": "serial_novel"})
|
|
3378
|
+
# 作者面评审口径块(与评审面同源提取):起草/修订/打磨提示词都带
|
|
3379
|
+
rubric_block = author_review_rubric_block(dims, signing_dims)
|
|
3322
3380
|
threshold = task.get("threshold", 7.0)
|
|
3323
3381
|
threshold_ch = threshold - 0.5 if threshold >= 7.5 else threshold # 单章阈值略放宽 0.5 分
|
|
3324
3382
|
dimkey = ", ".join('"%s": 0' % d for d in dims)
|
|
@@ -3389,6 +3447,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3389
3447
|
inherit = run.get("inherit") or {}
|
|
3390
3448
|
done_set = set(inherit.get("done_chapters") or [])
|
|
3391
3449
|
inh_scores = {c.get("chapter"): c for c in (inherit.get("chapter_scores") or [])}
|
|
3450
|
+
# 未达标重写的原因账(store.retry_task 从 verdict.major_issues 下发):
|
|
3451
|
+
# 章节级+全局级 major,渲染进对应重写章的起草提示词;达标续跑为空表
|
|
3452
|
+
redo_issues = [x for x in (inherit.get("redo_issues") or []) if isinstance(x, dict)]
|
|
3392
3453
|
if inherit.get("outline"):
|
|
3393
3454
|
outline = inherit["outline"]
|
|
3394
3455
|
# 历史遗留:降级/模板大纲被继承时,真实任务宁可中止重生成,也不按空模板写全书
|
|
@@ -3817,6 +3878,26 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3817
3878
|
if os.path.exists(os.path.join(workdir, ch_file)):
|
|
3818
3879
|
stale_note = ("- 注意:目录里已有一版**未通过评审**的旧稿 `%s`——不要校验沿用、"
|
|
3819
3880
|
"不要增量修补,按本章任务直接覆盖重写。" % ch_file)
|
|
3881
|
+
# 上一轮未达标原因(「重写未达标章」链路):本章被点名的 major 与
|
|
3882
|
+
# 全书级 major 随起草提示词下发——原因只躺在报告里、作者盲写等
|
|
3883
|
+
# 修订拉回,白白多烧一轮评审(2026-10-10 实案)。
|
|
3884
|
+
def _issue_ch(x):
|
|
3885
|
+
try:
|
|
3886
|
+
return int(x.get("chapter"))
|
|
3887
|
+
except (TypeError, ValueError):
|
|
3888
|
+
return None
|
|
3889
|
+
|
|
3890
|
+
redo_lines = []
|
|
3891
|
+
mine = [x for x in redo_issues if _issue_ch(x) == i]
|
|
3892
|
+
book_lvl = [x for x in redo_issues if _issue_ch(x) is None]
|
|
3893
|
+
if mine:
|
|
3894
|
+
redo_lines.append("- 本章被上一轮评审点名的问题(重写必须逐条消解):")
|
|
3895
|
+
redo_lines += _major_lines(mine)
|
|
3896
|
+
if book_lvl:
|
|
3897
|
+
redo_lines.append("- 上一轮全书级 major 问题(本章重写须配合消解,不得相抵):")
|
|
3898
|
+
redo_lines += _major_lines(book_lvl)
|
|
3899
|
+
redo_note = ("## 上一轮评审未达标原因(必读)\n" + "\n".join(redo_lines) + "\n"
|
|
3900
|
+
) if redo_lines else ""
|
|
3820
3901
|
# stable_order:同一任务 8 个章节的技能块必须字节级一致(§07 T1.2' 前缀缓存)
|
|
3821
3902
|
# 三块分开留底:起草重试的分层降级(_serial_shrunk_block)要按
|
|
3822
3903
|
# 「经验库/圣经」各自的身份收缩,折成一整块就只剩 4K 硬截一层了
|
|
@@ -3855,6 +3936,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
3855
3936
|
.replace("__HOOK__", branch_hook or ch.get("hook") or "留下悬念")
|
|
3856
3937
|
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
|
|
3857
3938
|
.replace("__STALE_NOTE__", stale_note)
|
|
3939
|
+
.replace("__REDO_ISSUES__", redo_note)
|
|
3940
|
+
.replace("__REVIEW_RUBRIC__", rubric_block)
|
|
3858
3941
|
.replace("__HIGHLIGHT__", ch.get("highlight") or "按剧情要点自然铺设一处小冲突/小反转")
|
|
3859
3942
|
.replace("__STYLE__", style_tpl)
|
|
3860
3943
|
.replace("__QUALITY_GATE__", novel_quality.opening_requirements(i, wpc))
|
|
@@ -4008,7 +4091,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4008
4091
|
if not res["ok"]:
|
|
4009
4092
|
# 成品是文件不是退出码:CLI 超时但章稿已完整落盘(终章长文实测
|
|
4010
4093
|
# 反复出现——文件写完、收尾声明没等到)就送评审门把关,别整章作废
|
|
4011
|
-
live = (store.get_run(run_id).get("steps") or [])
|
|
4094
|
+
live = ((store.get_run(run_id) or {}).get("steps") or [])
|
|
4012
4095
|
if live:
|
|
4013
4096
|
store.finish_step(run_id, live[-1]["n"], "done",
|
|
4014
4097
|
summary="起草调用超时,但章稿已完整落盘(约 %d 字)——交评审门判质量"
|
|
@@ -4267,6 +4350,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4267
4350
|
.replace("__VOLUME__", vol_block_for(i))
|
|
4268
4351
|
.replace("__BEATS__", ch.get("beats") or "按大纲推进")
|
|
4269
4352
|
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
|
|
4353
|
+
.replace("__REVIEW_RUBRIC__", rubric_block)
|
|
4270
4354
|
.replace("__HIGHLIGHT__", ch.get("highlight")
|
|
4271
4355
|
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
4272
4356
|
.replace("__STYLE__", style_tpl)
|
|
@@ -4422,6 +4506,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4422
4506
|
.replace("__VOLUME__", vol_block_for(j))
|
|
4423
4507
|
.replace("__BEATS__", _wch.get("beats") or "按大纲推进")
|
|
4424
4508
|
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, j))
|
|
4509
|
+
.replace("__REVIEW_RUBRIC__", rubric_block)
|
|
4425
4510
|
.replace("__HIGHLIGHT__", _wch.get("highlight")
|
|
4426
4511
|
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
4427
4512
|
.replace("__STYLE__", style_tpl)
|
|
@@ -4633,6 +4718,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
|
|
|
4633
4718
|
.replace("__VOLUME__", vol_block_for(i))
|
|
4634
4719
|
.replace("__BEATS__", ch.get("beats") or "按大纲推进")
|
|
4635
4720
|
.replace("__LOCKED_CH__", serial_locked_chapter_block(workdir, i))
|
|
4721
|
+
.replace("__REVIEW_RUBRIC__", rubric_block)
|
|
4636
4722
|
.replace("__HIGHLIGHT__", ch.get("highlight")
|
|
4637
4723
|
or "按剧情要点自然铺设一处小冲突/小反转")
|
|
4638
4724
|
.replace("__CRITIQUE__", crit)
|
|
@@ -5382,11 +5468,11 @@ def _write_project_memory(task, workdir, lines, source_run=""):
|
|
|
5382
5468
|
return ""
|
|
5383
5469
|
|
|
5384
5470
|
|
|
5385
|
-
def _read_project_memory(workdir, cap=4000):
|
|
5471
|
+
def _read_project_memory(workdir, cap=4000, query=""):
|
|
5386
5472
|
"""Only inject approved, unexpired and fingerprint-verified memory."""
|
|
5387
5473
|
try:
|
|
5388
5474
|
from . import project_memory
|
|
5389
|
-
return project_memory.active_text(workdir, cap=cap)
|
|
5475
|
+
return project_memory.active_text(workdir, cap=cap, query=query)
|
|
5390
5476
|
except (OSError, ValueError, TypeError):
|
|
5391
5477
|
return ""
|
|
5392
5478
|
|
|
@@ -5917,8 +6003,23 @@ def execute_run(run_id):
|
|
|
5917
6003
|
status="cancelled", ended_at=_now())
|
|
5918
6004
|
except Exception as e:
|
|
5919
6005
|
import traceback
|
|
6006
|
+
# 崩溃现场随错误行下发(2026-10-10 Mac 实案:连载连烧两轮自动续跑都
|
|
6007
|
+
# 死于 AttributeError("'NoneType' object has no attribute 'get'"),
|
|
6008
|
+
# 错误行只有一行 repr 无从定位;traceback 全文虽落 run 目录 error.log,
|
|
6009
|
+
# 但 UI 上看不见)。尾帧 = 文件:行号:函数名,一行说清死在哪;同因止损
|
|
6010
|
+
# 的错误签名因此按崩溃位置区分——同位置才算同因,代码升级后行号漂移
|
|
6011
|
+
# 自动能多试一轮(新代码可能已修复),次数上限仍兜底。
|
|
6012
|
+
_tb_tail = ""
|
|
6013
|
+
try:
|
|
6014
|
+
_frames = traceback.extract_tb(e.__traceback__)
|
|
6015
|
+
if _frames:
|
|
6016
|
+
_f = _frames[-1]
|
|
6017
|
+
_tb_tail = "(崩溃于 %s:%s %s)" % (
|
|
6018
|
+
os.path.basename(_f.filename), _f.lineno, _f.name)
|
|
6019
|
+
except Exception:
|
|
6020
|
+
pass
|
|
5920
6021
|
store.update_run(run_id, expected_status="running", status="failed",
|
|
5921
|
-
error=repr(e)[:500], ended_at=_now())
|
|
6022
|
+
error=(repr(e)[:380] + _tb_tail)[:500], ended_at=_now())
|
|
5922
6023
|
try:
|
|
5923
6024
|
err_path = store.run_dir(run_id) / "error.log"
|
|
5924
6025
|
if _inside(str(store.run_dir(run_id).parent), str(err_path)):
|
package/app/core/plugins.py
CHANGED
|
@@ -371,14 +371,21 @@ def install(plugin_id, roots=None):
|
|
|
371
371
|
"skill_count": len(old.get("skill_pack_ids") or [])}, None
|
|
372
372
|
source = Path(desc["dir"]).resolve()
|
|
373
373
|
target = (_install_root() / plugin_id).resolve()
|
|
374
|
+
market_id = "plugin-" + plugin_id
|
|
375
|
+
# 回滚标记必须在 try 之前就绑定:_copy_plugin/load_manifest 在记账前
|
|
376
|
+
# 抛错时 except 要靠它决定是否清 market 包,未初始化会把人话错误
|
|
377
|
+
# 炸成 UnboundLocalError→500,还会跳过残留目录清理让重装永远撞车。
|
|
378
|
+
market_written = False
|
|
374
379
|
try:
|
|
375
380
|
target.relative_to(_install_root().resolve())
|
|
381
|
+
if target.exists() and target != source:
|
|
382
|
+
# 有登记记录的已装插件在上面 already 分支就返回了;走到这里
|
|
383
|
+
# 还存在目标目录只可能是上次安装中途失败的残缺副本,清掉重装。
|
|
384
|
+
shutil.rmtree(target, ignore_errors=True)
|
|
376
385
|
_copy_plugin(source, target)
|
|
377
386
|
installed_desc = load_manifest(target)
|
|
378
387
|
files = _skill_files(installed_desc)
|
|
379
|
-
market_id = "plugin-" + plugin_id
|
|
380
388
|
skill_result = None
|
|
381
|
-
market_written = False
|
|
382
389
|
if files:
|
|
383
390
|
skill_result, err = market.install_files(
|
|
384
391
|
market_id, installed_desc["display_name"], files,
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
|
+
import re
|
|
8
9
|
import stat
|
|
9
10
|
import time
|
|
10
11
|
from pathlib import Path
|
|
@@ -95,6 +96,8 @@ def list_entries(workdir, *, now=None):
|
|
|
95
96
|
raw = row.get("content") or ""
|
|
96
97
|
row["verified"] = hashlib.sha256(raw.encode("utf-8")).hexdigest() == row.get("content_sha256")
|
|
97
98
|
row["expired"] = float(row.get("expires_at") or 0) <= stamp
|
|
99
|
+
row["feedback_score"] = int(row.get("feedback_score") or 0)
|
|
100
|
+
row["feedback_count"] = int(row.get("feedback_count") or 0)
|
|
98
101
|
return sorted(rows, key=lambda x: (x.get("created_at", 0), x.get("id", "")), reverse=True)
|
|
99
102
|
|
|
100
103
|
|
|
@@ -126,14 +129,78 @@ def decide(workdir, entry_id, action, *, expected_version=None, now=None):
|
|
|
126
129
|
return row
|
|
127
130
|
|
|
128
131
|
|
|
129
|
-
def
|
|
132
|
+
def feedback(workdir, entry_id, *, useful, expected_version=None, now=None):
|
|
133
|
+
"""Record explicit retrieval feedback as a versioned append-only update."""
|
|
134
|
+
if not isinstance(useful, bool):
|
|
135
|
+
raise ValueError("useful 必须是布尔值")
|
|
136
|
+
path = _file(workdir)
|
|
137
|
+
row = _read(path).get(str(entry_id or ""))
|
|
138
|
+
if not row:
|
|
139
|
+
raise KeyError("记忆条目不存在")
|
|
140
|
+
if expected_version is not None and int(expected_version) != int(row.get("version") or 1):
|
|
141
|
+
raise ValueError("记忆条目版本已变化,请刷新后重试")
|
|
142
|
+
raw = row.get("content") or ""
|
|
143
|
+
if hashlib.sha256(raw.encode("utf-8")).hexdigest() != row.get("content_sha256"):
|
|
144
|
+
raise ValueError("记忆内容指纹校验失败")
|
|
145
|
+
if row.get("status") != "approved":
|
|
146
|
+
raise ValueError("只有已批准的记忆条目可以反馈")
|
|
130
147
|
stamp = float(now if now is not None else time.time())
|
|
148
|
+
row = dict(row, feedback_score=int(row.get("feedback_score") or 0) +
|
|
149
|
+
(1 if useful else -1),
|
|
150
|
+
feedback_count=int(row.get("feedback_count") or 0) + 1,
|
|
151
|
+
feedback_at=stamp, version=int(row.get("version") or 1) + 1,
|
|
152
|
+
updated_at=stamp)
|
|
153
|
+
_append(path, row)
|
|
154
|
+
return row
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _query_terms(query):
|
|
158
|
+
query = str(query or "").strip().lower()
|
|
159
|
+
words = re.findall(r"[a-z0-9_]+|[\u3400-\u9fff]+", query)
|
|
160
|
+
terms = set()
|
|
161
|
+
for word in words:
|
|
162
|
+
if len(word) > 2 and re.fullmatch(r"[\u3400-\u9fff]+", word):
|
|
163
|
+
terms.update(word[i:i + 2] for i in range(len(word) - 1))
|
|
164
|
+
else:
|
|
165
|
+
terms.add(word)
|
|
166
|
+
return terms
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _relevance(row, terms):
|
|
170
|
+
if not terms:
|
|
171
|
+
return 0
|
|
172
|
+
title = str(row.get("title") or "").lower()
|
|
173
|
+
content = str(row.get("content") or "").lower()
|
|
174
|
+
title_terms = _query_terms(title)
|
|
175
|
+
content_terms = _query_terms(content)
|
|
176
|
+
return 4 * len(terms & title_terms) + len(terms & content_terms)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def active_text(workdir, *, cap=4000, query="", now=None):
|
|
180
|
+
stamp = float(now if now is not None else time.time())
|
|
181
|
+
try:
|
|
182
|
+
cap = max(0, int(cap))
|
|
183
|
+
except (TypeError, ValueError):
|
|
184
|
+
cap = 4000
|
|
185
|
+
heading = "## 已批准项目记忆(内容已校验,过期条目不注入)\n\n"
|
|
186
|
+
cap = max(0, cap - len(heading) - 2)
|
|
187
|
+
terms = _query_terms(query)
|
|
131
188
|
items = []
|
|
132
189
|
for row in list_entries(workdir, now=stamp):
|
|
133
190
|
if row.get("status") != "approved" or row.get("expired") or not row.get("verified"):
|
|
134
191
|
continue
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
192
|
+
block = "### %s\n%s" % (row.get("title") or "项目记忆", row.get("content") or "")
|
|
193
|
+
items.append((row, block))
|
|
194
|
+
items.sort(key=lambda item: (_relevance(item[0], terms),
|
|
195
|
+
item[0].get("feedback_score", 0),
|
|
196
|
+
item[0].get("created_at", 0)), reverse=True)
|
|
197
|
+
selected = []
|
|
198
|
+
used = 0
|
|
199
|
+
for _row, block in items:
|
|
200
|
+
cost = len(block) + (2 if selected else 0)
|
|
201
|
+
if used + cost > cap:
|
|
202
|
+
continue
|
|
203
|
+
selected.append(block)
|
|
204
|
+
used += cost
|
|
205
|
+
text = "\n\n".join(selected)
|
|
206
|
+
return (heading + text + "\n\n") if text else ""
|
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/core/store.py
CHANGED
|
@@ -1723,12 +1723,27 @@ def retry_task(task_id):
|
|
|
1723
1723
|
# 结构——这正是「重写未达标章」按钮(前端 done+未达标态放行
|
|
1724
1724
|
# retry)区别于断点续跑的语义。
|
|
1725
1725
|
redo = set()
|
|
1726
|
+
redo_issues = []
|
|
1726
1727
|
if (prev.get("verdict") or {}).get("publishable") is False:
|
|
1727
1728
|
redo = {int(c["chapter"]) for c in v_scores
|
|
1728
1729
|
if not c.get("passed") and c.get("chapter") is not None}
|
|
1730
|
+
# 未达标原因账(verdict.major_issues,章节级+全局级)随继承
|
|
1731
|
+
# 下发:流水线把本章点名项与全书级项渲染进重写章起草提示词,
|
|
1732
|
+
# 作者第一遍就知道往哪改,不再盲写等修订拉回。被 major 点名
|
|
1733
|
+
# 的过线章一并回炉——总判定卡在 major 上时只重写未过线章
|
|
1734
|
+
# 消不掉账,「重写未达标章」会空转(2026-10-10 实案)。
|
|
1735
|
+
for x in (prev.get("verdict") or {}).get("major_issues") or []:
|
|
1736
|
+
if not isinstance(x, dict):
|
|
1737
|
+
continue
|
|
1738
|
+
redo_issues.append(x)
|
|
1739
|
+
try:
|
|
1740
|
+
redo.add(int(x.get("chapter")))
|
|
1741
|
+
except (TypeError, ValueError):
|
|
1742
|
+
pass
|
|
1729
1743
|
if redo:
|
|
1730
1744
|
done = [n for n in done if n not in redo]
|
|
1731
|
-
v_scores = [c for c in v_scores if c.get("passed")
|
|
1745
|
+
v_scores = [c for c in v_scores if c.get("passed")
|
|
1746
|
+
and c.get("chapter") not in redo]
|
|
1732
1747
|
if done or redo:
|
|
1733
1748
|
if redo:
|
|
1734
1749
|
scores = v_scores # 未达标重写:只带已过线章的分数
|
|
@@ -1743,6 +1758,8 @@ def retry_task(task_id):
|
|
|
1743
1758
|
# 落账的 chapter_scores——否则多轮失败恢复会把全部
|
|
1744
1759
|
# 已过线章节重新评审(实测一晚白烧数百万 token)
|
|
1745
1760
|
"chapter_scores": scores,
|
|
1761
|
+
# 未达标重写的原因账(达标续跑时为空表)
|
|
1762
|
+
"redo_issues": redo_issues,
|
|
1746
1763
|
}
|
|
1747
1764
|
break
|
|
1748
1765
|
else:
|
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"))
|
|
@@ -3480,6 +3546,10 @@ def main():
|
|
|
3480
3546
|
print("[CodeBee] v%s" % (_ver or "?"), flush=True)
|
|
3481
3547
|
_step("正在准备数据目录…")
|
|
3482
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()
|
|
3483
3553
|
from core import attachments
|
|
3484
3554
|
n_pc = attachments.cleanup_stale() # 待提交附件残留清理(崩溃/弃单不堆积)
|
|
3485
3555
|
if n_pc:
|