codebee 0.1.23 → 0.1.25

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.
@@ -19,7 +19,7 @@ import re
19
19
  import threading
20
20
  import time
21
21
 
22
- from . import aiflavor, catalog, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage
22
+ from . import aiflavor, attachments, catalog, dispatch_log, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, task_compile, usage
23
23
  from . import builtin_agent
24
24
  from . import diagnostics
25
25
  from . import paths as paths_mod
@@ -53,11 +53,7 @@ def _inside(dirpath, target):
53
53
 
54
54
  def _task_images(task, workdir):
55
55
  """任务的图片附件绝对路径(仅 codex 原生 -i 用)。无附件/异常返回空列表。"""
56
- try:
57
- from . import attachments as att_mod
58
- return att_mod.image_paths(task, workdir)
59
- except Exception:
60
- return []
56
+ return attachments.image_paths(task, workdir)
61
57
 
62
58
 
63
59
  def _ms_name(raw):
@@ -252,25 +248,8 @@ def _drain_directives(run_id, workdir, role=None, step_n=None):
252
248
  if _is_review_role(role):
253
249
  lines.append("本步为评审步骤:请把上述用户意见作为评分依据之一,"
254
250
  "在相应维度的分数与 issues 中明确体现(引用用户原话)。")
255
- imgs = []
256
- for m in msgs:
257
- stamp = m.get("created_at") or ""
258
- sender = m.get("sender") or "用户"
259
- text = (m.get("text") or "").strip()
260
- lines.append("- [%s %s] %s" % (stamp, sender, text) if text
261
- else "- [%s %s](附件指令,见下方文件)" % (stamp, sender))
262
- for rel in (m.get("attachments") or []):
263
- rel = str(rel)
264
- low = rel.lower()
265
- if low.endswith((".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp")):
266
- ap = os.path.join(workdir or "", rel) if workdir else rel
267
- if workdir and os.path.isfile(ap):
268
- imgs.append(ap)
269
- lines.append(" · 图片附件:%s(请查看图片内容)" % rel)
270
- else:
271
- lines.append(" · 图片附件:%s" % rel)
272
- else:
273
- lines.append(" · 文件附件:%s(位于工作目录,可直接读取)" % rel)
251
+ msg_lines, imgs = attachments.directive_lines(msgs, workdir)
252
+ lines.extend(msg_lines)
274
253
  return "\n".join(lines), imgs
275
254
 
276
255
 
@@ -609,6 +588,9 @@ def _record_usage(run_id, role, agent, res, source="pipeline", step=0):
609
588
  try:
610
589
  run = store.get_run(run_id) or {}
611
590
  task = store.get_task(run.get("task_id")) if run.get("task_id") else None
591
+ provider = res.get("provider") or agent.get("provider") or {}
592
+ provider_id = provider.get("id") if isinstance(provider, dict) else ""
593
+ provider_name = provider.get("name") if isinstance(provider, dict) else ""
612
594
  usage.record(
613
595
  source=source, run_id=run_id, step=step,
614
596
  task_id=run.get("task_id") or "",
@@ -617,13 +599,14 @@ def _record_usage(run_id, role, agent, res, source="pipeline", step=0):
617
599
  agent_label=agent.get("label", ""),
618
600
  tool=agent.get("kind", ""),
619
601
  model=res.get("model") or "",
602
+ provider=res.get("provider_id") or provider_id or provider_name or "",
620
603
  ok=bool(res.get("ok")),
621
604
  duration_s=float(res.get("raw", {}).get("duration") or 0.0),
622
605
  cost_usd=float(res.get("cost_usd") or 0.0),
623
606
  usage=res.get("usage"))
624
607
  # 告警模块:CLI 调用成功/失败上报(provider 名与 usage 台账一致)
625
608
  from . import health
626
- prov = agent.get("provider") or {}
609
+ prov = res.get("provider") or agent.get("provider") or {}
627
610
  prov_name = (prov.get("name") if isinstance(prov, dict) else "") or ""
628
611
  if prov_name:
629
612
  if res.get("ok"):
@@ -747,6 +730,8 @@ def _run_review(run_id, task, workdir, reviewer, ev):
747
730
  .replace("__GOAL__", task["goal"])
748
731
  .replace("__VERIFY__", task.get("verify_command") or "(未配置)")
749
732
  .replace("__DIFF__", diff or "(无法获取 git diff,请综合任务目标谨慎评审)"))
733
+ if task.get("context"):
734
+ prompt += "\n\n## 原始背景与附件要求\n" + task["context"]
750
735
  res = _run_step(run_id, "review", reviewer, prompt, workdir, readonly=True, ev=ev,
751
736
  images=_task_images(task, workdir))
752
737
  if reviewer.get("mode") == "mock":
@@ -904,6 +889,31 @@ def _record_actual_route(run_id, task, agents, stats, implementer,
904
889
  store.update_run(run_id, route_plan={
905
890
  "task": spec, "implement": impl_plan, "review": review_plan,
906
891
  })
892
+ task_id = task.get("id") or (store.get_run(run_id) or {}).get("task_id") or ""
893
+ difficulty = (task.get("difficulty") or "auto")
894
+ for plan in (impl_plan, review_plan):
895
+ dispatch_log.record_event(
896
+ run_id=run_id, task_id=task_id, task_type=spec.get("type") or "",
897
+ difficulty=difficulty, role=plan.get("role") or "",
898
+ phase="selected", selected=plan.get("selected") or "",
899
+ participants=plan.get("participants") or (),
900
+ candidates=plan.get("candidates") or (),
901
+ fallback=plan.get("fallback") or (),
902
+ selection_reason=plan.get("selection_reason") or "")
903
+
904
+
905
+ def _record_dispatch_completed(run_id, task, result, verify_pass=None, review_pass=None):
906
+ """记录运行终态,供调度回放与线上指标复盘使用。"""
907
+ try:
908
+ spec = task.get("_compiled_spec") or task_compile.compile_task(task)
909
+ dispatch_log.record_event(
910
+ run_id=run_id,
911
+ task_id=task.get("id") or (store.get_run(run_id) or {}).get("task_id") or "",
912
+ task_type=spec.get("type") or "", difficulty=task.get("difficulty") or "auto",
913
+ role="", phase="completed", result=result,
914
+ verify_pass=verify_pass, review_pass=review_pass)
915
+ except Exception:
916
+ pass
907
917
 
908
918
 
909
919
  def _run_code(run, task, agents, ev, stats, mode):
@@ -941,7 +951,7 @@ def _run_code(run, task, agents, ev, stats, mode):
941
951
  # 架构事实,让规划器不再对代码库一无所知
942
952
  _pm = _read_project_memory(workdir)
943
953
  if _pm:
944
- task = dict(task, context=((task.get("context") or "") + "\n\n" + _pm)[:8000])
954
+ task = dict(task, context=(task.get("context") or "") + "\n\n" + _pm)
945
955
  plan_step, plan_log = store.add_step(run_id, "plan", impl["id"], impl.get("label"),
946
956
  note=route.get("implementer", ""))
947
957
  plan = planner.make_code_plan(_steered_task(run_id, task),
@@ -1080,8 +1090,9 @@ def _run_code(run, task, agents, ev, stats, mode):
1080
1090
  return False
1081
1091
 
1082
1092
  def review_and_score():
1083
- review_json = _run_review(run_id, task, workdir, modelhub.bind_agent(reviewer, difficulty), ev)
1084
1093
  verify_pass, verify_ran = _run_verify(run_id, task, workdir, ev)
1094
+ # 先让确定性验证落盘,再执行模型评审;终态判断会同时使用两份证据。
1095
+ review_json = _run_review(run_id, task, workdir, modelhub.bind_agent(reviewer, difficulty), ev)
1085
1096
  return review_json, verify_pass, verify_ran
1086
1097
 
1087
1098
  attempt_note = route.get("implementer", "") if mode == "auto" else ""
@@ -1100,6 +1111,7 @@ def _run_code(run, task, agents, ev, stats, mode):
1100
1111
  .replace("__GOAL__", task["goal"])
1101
1112
  .replace("__ISSUES__", issues_txt)
1102
1113
  .replace("__VERIFY_HINT__", _verify_hint(task)))
1114
+ prompt = attachments.append_task_context(prompt, task)
1103
1115
  res = _run_step(run_id, "fix-r%d" % round_no, modelhub.bind_agent(impl, difficulty),
1104
1116
  prompt, workdir, readonly=False, ev=ev,
1105
1117
  note="自动修复第 %d 轮" % round_no,
@@ -1218,6 +1230,7 @@ __GOAL__
1218
1230
  __CONTEXT__
1219
1231
 
1220
1232
  ## 要求
1233
+ - 背景与上下文里若有「附件材料」,先逐个读取再处理——附件是必要输入,不读附件就作答视为未完成。
1221
1234
  - 能改直接改、能写直接写(限本工作目录内),产出文件一律 UTF-8 编码(PowerShell 写文件显式 -Encoding UTF8)。
1222
1235
  - 正文直接交代结果与答案:不要写「本轮做了什么」这类开场总结,也不要「回复:」这类引导词。
1223
1236
  - 回复的最后一行单独输出一行交代结果:
@@ -1247,6 +1260,7 @@ __GOAL__
1247
1260
  __CONTEXT__
1248
1261
 
1249
1262
  ## 要求
1263
+ - 背景与上下文里若有「附件材料」,先逐个读取再处理——附件是必要输入,不读附件就作答视为未完成。
1250
1264
  - 能改直接改、能写直接写(用工具,限本工作目录内),产出文件一律 UTF-8 编码。
1251
1265
  - 完成后直接给用户结论与答案,需要时顺带交代产出/修改了哪些文件;不要写「本轮做了什么」这类开场白。
1252
1266
  __FOLLOWUPS__"""
@@ -1418,6 +1432,8 @@ def _run_direct(run, task, agents, ev, stats, mode):
1418
1432
  prompt = (DIRECT_PROMPT
1419
1433
  .replace("__GOAL__", task["goal"])
1420
1434
  .replace("__CONTEXT__", task.get("context") or "(无)"))
1435
+ if task.get("attachments") and "codebee-attachments:start" not in prompt:
1436
+ prompt += "\n\n## 用户背景与附件\n" + (task.get("context") or "")
1421
1437
  note = route.get("implementer", "")
1422
1438
  images = _task_images(task, workdir)
1423
1439
  else:
@@ -1533,6 +1549,9 @@ CONTENT_DELIVERY_CONTRACTS = {
1533
1549
  "doc": ("技术文档编辑", [
1534
1550
  "先明确读者、目的和前置条件,再按可执行步骤组织正文。",
1535
1551
  "命令、参数、示例与限制必须一致;无法确认的内容明确标注。",
1552
+ # sepia 分场合规则(工单/文档体裁):标题=结果、验收可测试、链接不重复
1553
+ "标题写结果或结论(「如何迁移 X」优于「X 说明」),正文链接原文不整段复述。",
1554
+ "涉及需求或变更时给出可测试的验收标准(能被逐条勾选判定通过/不通过)。",
1536
1555
  ]),
1537
1556
  "translation": ("专业译者与审校", [
1538
1557
  "忠实保留原文含义、语气、数字、专名、占位符、链接和 Markdown 结构,不增译或漏译。",
@@ -1549,14 +1568,24 @@ CONTENT_DELIVERY_CONTRACTS = {
1549
1568
  "weekly_report": ("业务汇报顾问", [
1550
1569
  "按成果与影响、关键数据、问题阻塞、下步行动(负责人/时间)组织内容。",
1551
1570
  "只使用用户提供或可核验的数据;缺失数字保留待补项,不虚构业绩。",
1571
+ # sepia 分场合规则(postmortem 体裁):先给结论;对机制严格不指名甩锅
1572
+ "第一段先给本期最重要的结论或结果,再展开支撑细节,不按时间流水铺陈。",
1573
+ "问题与阻塞直说机制原因,不带情绪也不指名甩锅;行动项必须落到负责人与时间。",
1552
1574
  ]),
1553
1575
  "email": ("商务沟通顾问", [
1554
1576
  "包含明确主题、称呼、来意、必要背景、请求/下一步和得体落款。",
1555
1577
  "语气匹配双方关系;日期、承诺、附件与联系人不得凭空补造。",
1578
+ # sepia 分场合规则(PR 回复体裁):先答再铺陈;篇幅与利害成正比
1579
+ "第一句/第一段先给结论或答复(对方要做什么、答应还是不答应),再给必要背景。",
1580
+ "请求具体到动作与截止时间;篇幅与事情轻重成正比,删掉礼节性空话与自我表扬。",
1556
1581
  ]),
1557
1582
  "tech_proposal": ("解决方案架构师", [
1558
1583
  "覆盖现状与目标、约束、候选方案对比、推荐架构、实施阶段、风险与回滚、验收指标。",
1559
1584
  "区分已知事实、假设和待验证项;成本收益给出计算口径而非虚构数字。",
1585
+ # sepia 分场合规则(技术文章体裁):从问题开场/真实死胡同/明确观点/带条件数字
1586
+ "从要解决的问题开场(不是从背景科普铺陈),让读者第一段就知道为什么非做不可。",
1587
+ "候选对比里至少保留一个真实分析过又被否决的方向,写清否决理由,不搞陪衬方案。",
1588
+ "必须有明确表态的推荐意见和取舍逻辑;关键数字一律带适用条件与计算口径。",
1560
1589
  ]),
1561
1590
  "resume": ("招聘与简历顾问", [
1562
1591
  "围绕目标岗位提炼真实经历,用行动、结果和技能关键词表达岗位匹配度。",
@@ -1959,6 +1988,8 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1959
1988
  # 优先级最高的约束放最前面,写作者先读原则再读设定
1960
1989
  if _RUN_CONSTITUTION:
1961
1990
  bible = _RUN_CONSTITUTION + (bible or "")
1991
+ if task.get("context"):
1992
+ bible = task["context"] + ("\n\n" + bible if bible else "")
1962
1993
 
1963
1994
 
1964
1995
  def crit_prompt_for(text, note=""):
@@ -2498,6 +2529,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2498
2529
  .replace("__GOAL__", task["goal"])
2499
2530
  .replace("__CRITIQUE__", "\n".join(crit_lines))
2500
2531
  .replace("__WORDS__", str(wpc)))
2532
+ prompt = attachments.append_task_context(prompt, task)
2501
2533
  _run_step(run_id, "revise-c%d" % i, modelhub.bind_agent(impl, difficulty), prompt,
2502
2534
  step_wd, readonly=False, ev=ev, timeout=2400,
2503
2535
  resume=resume_ctx["session"] if resume_ctx else draft_sid)
@@ -2629,6 +2661,7 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2629
2661
  .replace("__GOAL__", task["goal"])
2630
2662
  .replace("__CRITIQUE__", crit)
2631
2663
  .replace("__WORDS__", str(wpc)))
2664
+ prompt = attachments.append_task_context(prompt, task)
2632
2665
  res = _run_step(run_id, "polish-c%d" % i, modelhub.bind_agent(impl, difficulty),
2633
2666
  prompt, workdir, readonly=False, ev=ev, timeout=2400,
2634
2667
  resume=resume_ctx["session"] if resume_ctx else None)
@@ -3010,6 +3043,8 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3010
3043
  _tpl(task, "critique_prompt", NOVEL_CRITIQUE_PROMPT))
3011
3044
  .replace("__DIMKEYS__", dimkey)
3012
3045
  .replace("__MANUSCRIPT__", manuscript or "(稿件为空!)"))
3046
+ if task.get("context"):
3047
+ crit_prompt += "\n\n## 原始任务背景与附件参考\n" + task["context"]
3013
3048
  # AI 味确定性检测(借鉴 oh-story 去AI味):客观参考线随评审下发,
3014
3049
  # 命中才追加——评审官结合上下文判断是否真问题,脚本不直接扣分
3015
3050
  _aiflavor_line = aiflavor.report_line(manuscript)
@@ -3078,6 +3113,7 @@ def _run_content_review(run, task, agents, ev, stats, mode):
3078
3113
  .replace("__GOAL__", task["goal"])
3079
3114
  .replace("__CRITIQUE__", "\n".join(crit_lines)))
3080
3115
  prompt += _content_contract(task)
3116
+ prompt = attachments.append_task_context(prompt, task)
3081
3117
  _run_step(run_id, "revise-r%d" % r, modelhub.bind_agent(impl, difficulty), prompt,
3082
3118
  workdir, readonly=False, ev=ev,
3083
3119
  resume=resume_ctx["session"] if resume_ctx else None)
@@ -3405,6 +3441,7 @@ def execute_run(run_id):
3405
3441
  task_spec_summary=task_compile.summary(task_spec),
3406
3442
  difficulty=task_spec["difficulty"])
3407
3443
  task = dict(task)
3444
+ task = attachments.refresh_task(task, task.get("workdir") or "")
3408
3445
  task["_compiled_spec"] = task_spec
3409
3446
  # 运行内统一使用编译后的难度;store 中历史任务常带 difficulty=auto,
3410
3447
  # 不能让这个兼容值覆盖 easy/default/hard 的模型调度决策。
@@ -3515,6 +3552,33 @@ def execute_run(run_id):
3515
3552
  except Exception:
3516
3553
  pass
3517
3554
  finally:
3555
+ # 全类型统一写调度终态与质量反馈。调用成功只代表传输可靠;真正用于
3556
+ # 在线推荐的成功率以 verify/review/publishable 等验收结果为准。
3557
+ try:
3558
+ final_run = store.get_run(run_id) or {}
3559
+ final_status = final_run.get("status") or ""
3560
+ if final_status in ("done", "failed"):
3561
+ final_verdict = final_run.get("verdict") or {}
3562
+ if "pass" in final_verdict:
3563
+ quality_ok = bool(final_verdict.get("pass"))
3564
+ elif "publishable" in final_verdict:
3565
+ quality_ok = bool(final_verdict.get("publishable"))
3566
+ else:
3567
+ quality_ok = final_status == "done"
3568
+ _record_dispatch_completed(
3569
+ run_id, task, "passed" if quality_ok else "failed",
3570
+ verify_pass=final_verdict.get("verify_pass"),
3571
+ review_pass=final_verdict.get(
3572
+ "review_pass", final_verdict.get("publishable")))
3573
+ final_agent = (((final_run.get("route_plan") or {})
3574
+ .get("implement") or {}).get("selected") or "")
3575
+ if final_agent.startswith("builtin:"):
3576
+ final_agent = "builtin"
3577
+ usage.record_quality_for_run(run_id, quality_ok, agent=final_agent)
3578
+ elif final_status == "cancelled":
3579
+ _record_dispatch_completed(run_id, task, "cancelled")
3580
+ except Exception:
3581
+ pass
3518
3582
  # 任务分支收尾(git_rev 隔离链的第二半):先只读快照本 run 的全部变更
3519
3583
  # 落 run 记录供人审,再把产物提交到 tutti/<task-id> 并切回原分支。
3520
3584
  # 放 finally:done/failed/cancelled/异常一律保存现场;收尾自身绝不抛错,
@@ -0,0 +1,111 @@
1
+ # -*- coding: utf-8 -*-
2
+ """启动端口清场(用户拍板 2026-09-21):端口被占时先清场再启动。
3
+
4
+ - **自家旧实例**(命令行含本包 main.py 完整路径,或 npm 包内 app/main.py)
5
+ → 杀树(TerminateProcess 直杀)——升级/重启最常见的占用者就是没退干净
6
+ 的 CodeBee 自己;控制台进程对温和信号(WM_CLOSE)无反应,温和关不掉
7
+ 正是用户「旧进程太难杀」的痛点,所以自家实例直接强杀。
8
+ - **别人的进程** → 只报告占用者,不发送任何信号;启动流程不得替用户
9
+ 关闭可能正在开发中的服务。
10
+ - 系统/自身进程拒关(portscan 内置护栏)。
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ import os
16
+ import shlex
17
+ import subprocess
18
+
19
+ from . import portscan, runner
20
+
21
+ log = logging.getLogger(__name__)
22
+
23
+ PS_EXE = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
24
+
25
+
26
+ def proc_cmdline(pid):
27
+ """读进程命令行;失败返回空串。仅用于确认旧 CodeBee 实例身份。"""
28
+ try:
29
+ pid = int(pid)
30
+ except (TypeError, ValueError):
31
+ return ""
32
+ if pid <= 0:
33
+ return ""
34
+ if os.name != "nt":
35
+ try:
36
+ with open("/proc/%d/cmdline" % pid, "rb") as fh:
37
+ return fh.read().decode("utf-8", "replace").replace("\x00", " ").strip()
38
+ except Exception:
39
+ return ""
40
+ try:
41
+ r = subprocess.run(
42
+ [PS_EXE, "-NoProfile", "-Command",
43
+ "(Get-CimInstance Win32_Process -Filter 'ProcessId = %d').CommandLine" % pid],
44
+ capture_output=True, timeout=10)
45
+ return r.stdout.decode("utf-8", "replace").strip() if r.returncode == 0 else ""
46
+ except Exception:
47
+ return ""
48
+
49
+
50
+ def _command_tokens(cmdline):
51
+ try:
52
+ return [token.strip().strip('"').strip("'")
53
+ for token in shlex.split(str(cmdline or ""), posix=False)]
54
+ except (TypeError, ValueError):
55
+ return []
56
+
57
+
58
+ def is_own_instance(cmdline, main_script, port=None):
59
+ """按启动器、脚本独立参数和端口精确确认 CodeBee 实例。"""
60
+ tokens = _command_tokens(cmdline)
61
+ if len(tokens) < 2:
62
+ return False
63
+ launcher = os.path.splitext(os.path.basename(tokens[0]))[0].lower()
64
+ if launcher not in ("python", "python3", "py"):
65
+ return False
66
+ script = ""
67
+ for token in tokens[1:]:
68
+ if token.startswith("-"):
69
+ continue
70
+ script = token
71
+ break
72
+ if not script or not os.path.isabs(script):
73
+ return False
74
+ actual = os.path.normcase(os.path.normpath(os.path.abspath(script)))
75
+ expected = os.path.normcase(os.path.normpath(os.path.abspath(str(main_script))))
76
+ packaged = actual.replace("\\", "/").lower().endswith(
77
+ "/node_modules/codebee/app/main.py")
78
+ if actual != expected and not packaged:
79
+ return False
80
+ if port is None:
81
+ return True
82
+ try:
83
+ wanted = str(int(port))
84
+ except (TypeError, ValueError):
85
+ return False
86
+ explicit_port = None
87
+ for i, token in enumerate(tokens):
88
+ if token == "--port" and i + 1 < len(tokens):
89
+ explicit_port = tokens[i + 1]
90
+ elif token.startswith("--port="):
91
+ explicit_port = token.split("=", 1)[1]
92
+ # argparse 默认端口可不出现在旧实例参数中;非默认端口必须显式相符。
93
+ return ((explicit_port == wanted) if explicit_port is not None
94
+ else wanted == "8765")
95
+
96
+
97
+ def clear_stale_port(port, main_script):
98
+ """清掉占用端口的进程。返回 (是否清掉, 人话说明)。"""
99
+ holders = [p for p in portscan.listening_ports() if p.get("port") == int(port)]
100
+ if not holders:
101
+ return True, ""
102
+ pid = int(holders[0].get("pid") or 0)
103
+ if pid <= 4 or pid == os.getpid():
104
+ return False, "占用者是系统进程/自身,拒绝清理"
105
+ if is_own_instance(proc_cmdline(pid), main_script, port=port):
106
+ try:
107
+ runner._kill_tree(pid)
108
+ return True, "已结束旧实例 PID %d" % pid
109
+ except Exception as e:
110
+ return False, "旧实例 PID %d 清理失败 %s" % (pid, str(e)[:80])
111
+ return False, "占用者非 CodeBee(PID %d),未自动关闭" % pid