codebee 0.1.16 → 0.1.18

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, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, usage
22
+ from . import aiflavor, catalog, history, jobs, knowledge, manager, modelhub, mocks, paihang, planner, registry, router, runner, skills, store, usage
23
23
  from . import builtin_agent
24
24
  from . import diagnostics
25
25
  from . import paths as paths_mod
@@ -82,6 +82,9 @@ def _agents():
82
82
  # 本轮运行的智能体池:execute_run 入口快照,_run_step 死链补位时扫描。
83
83
  # 直接调 _run_step 的场景(单测/内部工具)池为空 → 补位不触发,闸门语义不变。
84
84
  _CURRENT_AGENTS: list = []
85
+ # 本次 run 的任务宪章注入块(execute_run 起跑时从 .codebee/constitution.md 读入;
86
+ # 步骤函数在各提示词组装点引用,空串=未配置零噪音)
87
+ _RUN_CONSTITUTION = ""
85
88
 
86
89
 
87
90
  def _dead_binding_substitute(dead_id, resume=None):
@@ -660,6 +663,11 @@ CODE_REVIEW_PROMPT = """你是代码评审员(不要修改任何文件)。
660
663
  "summary": "一句话结论"
661
664
  }
662
665
 
666
+ ## 评审要求(findings 锚定证据)
667
+ 每个 issue 的 detail 必须给出「文件名:行号」(从 diff 的 hunk 头 @@ -a,b +c,d @@ 与上下文推算),
668
+ 并引用该处一行关键代码作为依据——没有证据定位的问题不要报,宁可少报不报猜测。
669
+ (借鉴 pr-af:findings grounded in code evidence 是评审可信度的根。)
670
+
663
671
  ## 任务目标
664
672
  __GOAL__
665
673
 
@@ -792,7 +800,7 @@ def _code_bestof(run, task, impl, difficulty, ev):
792
800
  agt_b = modelhub.bind_agent(impl, difficulty)
793
801
  res = None
794
802
  for i, sub in enumerate(subtasks):
795
- prompt = (CODE_IMPL_PROMPT
803
+ prompt = (_RUN_CONSTITUTION + CODE_IMPL_PROMPT
796
804
  .replace("__GOAL__", task["goal"])
797
805
  .replace("__SUBTASK__", sub["detail"] if sub["detail"] else sub["title"])
798
806
  .replace("__CONTEXT__", task.get("context") or "(无)")
@@ -941,7 +949,7 @@ def _run_code(run, task, agents, ev, stats, mode):
941
949
  (s.get("title") or "")[:40] for s in subtasks[:i]) or "(无)"
942
950
  prog = ("\n\n## 计划进度(第 %d/%d 项)\n已完成:%s。当前接着做下面这一项,"
943
951
  "不要重做已完成的。" % (i + 1, len(subtasks), done_titles))
944
- prompt = (CODE_IMPL_PROMPT
952
+ prompt = (_RUN_CONSTITUTION + CODE_IMPL_PROMPT
945
953
  .replace("__GOAL__", task["goal"])
946
954
  .replace("__SUBTASK__",
947
955
  sub["detail"] if sub["detail"] else sub["title"])
@@ -1396,10 +1404,25 @@ __GOAL__
1396
1404
  ## 背景与上下文
1397
1405
  __CONTEXT__
1398
1406
 
1407
+ ## 评审维度(写前自查)
1408
+ __RUBRIC__
1409
+ (按这些维度组织内容重点——评审会按它们打分)
1410
+
1399
1411
  ## 要求
1400
1412
  - 只修改 `__FILE__` 这一个文件;保持 Markdown 结构。
1401
1413
  - 完成后用 3 句话说明本轮写了什么。"""
1402
1414
 
1415
+ # 调研报告类稿件的追加要求(借鉴 gpt-researcher 迭代深研):有网络/读文件工具时
1416
+ # 多源交叉验证,单源结论降权——调研的可信度来自证据链而非文采
1417
+ RESEARCH_APPENDIX = """
1418
+
1419
+ ## 调研要求(证据链)
1420
+ - 有联网/检索工具就先搜集资料再写:同一关键结论至少两个独立来源交叉验证,
1421
+ 单源信息要标注「仅单一来源」。
1422
+ - 引用来源在文中用行内链接或脚注标明(域名即可,不编造 URL)。
1423
+ - 区分「事实」与「观点」:数据/时间/版本号给来源,预测/评价标明是分析。
1424
+ - 结构建议:结论先行 → 分层论证 → 风险与局限(说明哪些结论证据不足)。"""
1425
+
1403
1426
  NOVEL_REVISE_PROMPT = """你是一名专业作者。请根据下方汇总评审意见修订稿件文件:`__FILE__`(直接写入该文件)。文件必须以 UTF-8 编码保存(PowerShell 写文件显式加 -Encoding UTF8,禁止依赖默认编码)。
1404
1427
 
1405
1428
  ## 原始写作任务
@@ -1465,7 +1488,10 @@ def _story_bible(workdir):
1465
1488
  if not txt:
1466
1489
  return ""
1467
1490
  return ("## 故事圣经(story-bible.md:人物/世界观/伏笔台账,本书一切写作与评审以此为准,"
1468
- "与其冲突处以圣经为准)\n\n" + txt)
1491
+ "与其冲突处以圣经为准)\n\n"
1492
+ "**连续性锁**(借鉴 drama-skills):人物外貌/性格口头禅/物品/能力等需要跨章一致"
1493
+ "的设定,写作时原样沿用圣经中的表述(可整句贴入正文),不要同义改写——每章用词"
1494
+ "一致读者才不会出戏。\n\n" + txt)
1469
1495
 
1470
1496
 
1471
1497
  def _plot_modules(workdir):
@@ -1733,6 +1759,10 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1733
1759
  mods = _plot_modules(workdir)
1734
1760
  if mods:
1735
1761
  bible = (bible + "\n\n" + mods) if bible else mods
1762
+ # 任务宪章(spec-kit constitution):作者定的质量原则拼在注入块最前——
1763
+ # 优先级最高的约束放最前面,写作者先读原则再读设定
1764
+ if _RUN_CONSTITUTION:
1765
+ bible = _RUN_CONSTITUTION + (bible or "")
1736
1766
 
1737
1767
 
1738
1768
  def crit_prompt_for(text, note=""):
@@ -1747,6 +1777,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1747
1777
  tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % sk, 1)
1748
1778
  if bible:
1749
1779
  tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % bible, 1)
1780
+ kb = knowledge.block_for(task)
1781
+ if kb:
1782
+ tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % kb, 1)
1750
1783
  return tpl.replace("__DIMKEYS__", dimkey).replace(
1751
1784
  "__MANUSCRIPT__", text or "(稿件为空!)")
1752
1785
 
@@ -1944,6 +1977,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1944
1977
  sk_block, _ = skills.block_for(task, stable_order=True)
1945
1978
  if bible:
1946
1979
  sk_block = (sk_block + "\n\n" + bible) if sk_block else bible
1980
+ kb_block = knowledge.block_for(task)
1981
+ if kb_block:
1982
+ sk_block = (sk_block + "\n\n" + kb_block) if sk_block else kb_block
1947
1983
  scope = ("本章 = 大纲第 %d 章" % i) if start == 1 else (
1948
1984
  "本批为第 %d–%d 章,下列按全书章号列出各章要点" % (start, end))
1949
1985
 
@@ -2397,10 +2433,25 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
2397
2433
  fixed.append(i)
2398
2434
  store.update_run(run_id, chapter_scores=chapter_scores)
2399
2435
  _check_cancel(ev)
2436
+ if not fixed:
2437
+ # 最弱章重改全部失败(供应商拥堵/流断等)→ 章稿没有任何变化,
2438
+ # 继续跑全书重评只会白烧评审链,组长步骤一挂「工作中」就是几十
2439
+ # 分钟(2026-09-19 实案:polish-c18/c11 双败后仍进全书重评,
2440
+ # 单个评审 35 分钟,用户侧只见打磨 2/3 久卡不动)。直接收尾。
2441
+ store.finish_step(run_id, pstep["n"], "failed",
2442
+ summary="重改未成功(%s 全部失败),本轮打磨中止;"
2443
+ "已落盘章稿不受影响"
2444
+ % "、".join("第 %d 章" % c["chapter"] for c in weak))
2445
+ break
2400
2446
  # 重评全书一致性(同一评审闭包;本轮全挂则保留上一轮结论——评审链挂了
2401
2447
  # 不代表书变差,不能拿「无法评审」覆盖真实分数)
2402
- full_text = "\n\n".join(_read_chapter(workdir, i2) for i2 in range(1, end + 1))
2403
- gscored2, gmeans_acc2 = run_global_round(critics)
2448
+ try:
2449
+ full_text = "\n\n".join(_read_chapter(workdir, i2) for i2 in range(1, end + 1))
2450
+ gscored2, gmeans_acc2 = run_global_round(critics)
2451
+ except BaseException:
2452
+ store.finish_step(run_id, pstep["n"], "failed",
2453
+ summary="打磨后全书重评异常中止,已落盘章稿不受影响")
2454
+ raise
2404
2455
  if gscored2:
2405
2456
  global_means = {d: round(sum(xs) / len(xs), 1) for d, xs in gmeans_acc2.items()}
2406
2457
  global_pass = _all_ge(global_means, threshold)
@@ -2659,13 +2710,19 @@ def _run_content_review(run, task, agents, ev, stats, mode):
2659
2710
  except Exception:
2660
2711
  pass
2661
2712
  else:
2713
+ is_research = task.get("type") == "research"
2714
+
2662
2715
  def _draft_prompt_for(vfile):
2663
2716
  p = (_tpl(task, "draft_prompt", NOVEL_DRAFT_PROMPT).replace("__FILE__", vfile)
2664
2717
  .replace("__GOAL__", task["goal"])
2665
- .replace("__CONTEXT__", task.get("context") or "(无)"))
2718
+ .replace("__CONTEXT__", task.get("context") or "(无)")
2719
+ .replace("__RUBRIC__", "、".join(dims) if dims else "(按流程默认维度)"))
2666
2720
  if outline:
2667
2721
  p += "\n\n## 编排者大纲(按要点组织稿件)\n" + \
2668
2722
  "\n".join("- " + i for i in outline["items"])
2723
+ if is_research:
2724
+ # 调研报告追加证据链要求(gpt-researcher 借鉴)
2725
+ p += RESEARCH_APPENDIX
2669
2726
  return p
2670
2727
 
2671
2728
  best_of = max(1, min(3, int(task.get("best_of") or 1)))
@@ -2871,6 +2928,23 @@ def _run_serial_qa(run, task, agents, ev):
2871
2928
  error="答疑失败(执行/评审链不可用)——" + ";".join(errors[-3:]))
2872
2929
 
2873
2930
 
2931
+ def _read_constitution(workdir):
2932
+ """任务宪章(借鉴 spec-kit constitution):工作目录 .codebee/constitution.md
2933
+ (作者手工维护的质量原则——代码规范/文风/测试要求)。存在且非空时返回注入块,
2934
+ 每次 run 的所有智能体提示词都会带上;否则返回 ""(零配置零噪音)。"""
2935
+ p = os.path.abspath(os.path.join(str(workdir or ""), ".codebee", "constitution.md"))
2936
+ if not _inside(workdir, p) or not os.path.isfile(p):
2937
+ return ""
2938
+ try:
2939
+ txt = _read_text_any_enc(p)[:6000].strip()
2940
+ except OSError:
2941
+ return ""
2942
+ if not txt:
2943
+ return ""
2944
+ return ("## 项目宪章(constitution.md:本项目一切产出的质量原则,优先级最高,"
2945
+ "与其他要求冲突时以宪章为准)\n\n" + txt + "\n\n")
2946
+
2947
+
2874
2948
  def _write_task_spec(task, workdir):
2875
2949
  """任务规格落盘 .codebee/spec.md(借鉴 agent-orchestrator 的 .spec/PROMPT.md 与
2876
2950
  planning-with-files 的文件化计划):任务定义随工作目录留存、随任务分支版本化,
@@ -3028,6 +3102,11 @@ def execute_run(run_id):
3028
3102
  # 任务规格文件化(借鉴 planning-with-files/agent-orchestrator):任何任务都在
3029
3103
  # 工作目录留一份 .codebee/spec.md——原始意图可见、随任务分支版本化
3030
3104
  _write_task_spec(task, task["workdir"])
3105
+ # 任务宪章(借鉴 spec-kit constitution):工作目录 .codebee/constitution.md
3106
+ # 是作者定下的质量原则(代码规范/文风/测试要求),每次 run 注入所有智能体
3107
+ # 提示词——代码/文章/翻译全类型通用,一次定义持续生效。缺省零噪音。
3108
+ global _RUN_CONSTITUTION
3109
+ _RUN_CONSTITUTION = _read_constitution(task["workdir"])
3031
3110
  agents = _agents()
3032
3111
  global _CURRENT_AGENTS
3033
3112
  _CURRENT_AGENTS = agents
@@ -3117,3 +3196,8 @@ def execute_run(run_id):
3117
3196
  skills.learn_async(run_id)
3118
3197
  except Exception:
3119
3198
  pass
3199
+ # 知识库闭环:从产出材料提炼可复用知识条目(草稿态,人工转正后参与注入)
3200
+ try:
3201
+ knowledge.learn_async(run_id)
3202
+ except Exception:
3203
+ pass
package/app/core/store.py CHANGED
@@ -621,11 +621,23 @@ def update_run(run_id, expected_status=None, **fields):
621
621
  if expected_status is not None and run.get("status") != expected_status:
622
622
  return None
623
623
  run.update(fields)
624
+ # 终态收尸:run 已结束却还挂 queued/running 的步骤统一落 cancelled,
625
+ # 语义与 recover_orphaned_runs 的启动清扫对齐(那套清跨进程遗留,
626
+ # 这套在落终态瞬间即时收口——打磨组长被取消/异常打断时 UI 不闪
627
+ # 假「工作中」)。run 终态后不可能还有步骤在真跑。
628
+ st = fields.get("status")
629
+ if st in ("done", "failed", "cancelled"):
630
+ end_s = time.strftime("%H:%M:%S")
631
+ for s in run["steps"]:
632
+ if s.get("status") in ("queued", "running"):
633
+ s["status"] = "cancelled"
634
+ s["ended_at"] = s.get("ended_at") or end_s
635
+ if not s.get("summary"):
636
+ s["summary"] = "步骤未正常收尾(终态自动恢复)"
624
637
  _save_json(paths.RUNS_DIR / run_id / "run.json", run)
625
638
  # 状态回填:起跑与结束都同步任务状态。只回填终态的话,run 在跑、
626
639
  # 任务永远显示「排队中」(2026-09-18 实案:「# 重写·续」run 已 running
627
640
  # 写了一小时,任务卡 queued,用户以为卡死连点重试)。
628
- st = fields.get("status")
629
641
  if st in ("running", "done", "failed", "cancelled"):
630
642
  tid = run.get("task_id")
631
643
  task = _TASKS.get(tid) if tid else None
package/app/main.py CHANGED
@@ -233,6 +233,9 @@ class Handler(BaseHTTPRequestHandler):
233
233
  if path == "/api/skills":
234
234
  from core import skills
235
235
  return self._json(200, skills.view())
236
+ if path == "/api/knowledge":
237
+ from core import knowledge
238
+ return self._json(200, knowledge.view())
236
239
  if path == "/api/settings":
237
240
  return self._json(200, dict(settings.load(), **jobs.workers_info()))
238
241
  if path == "/api/settings-v2":
@@ -835,6 +838,16 @@ class Handler(BaseHTTPRequestHandler):
835
838
  from core import skills
836
839
  n = skills.learn_from_run((self._body().get("run_id") or ""))
837
840
  return self._json(200, {"ok": True, "learned": n})
841
+ if path == "/api/knowledge/op":
842
+ from core import knowledge
843
+ body = self._body()
844
+ err = knowledge.entry_op(body.get("id") or "", body.get("op") or "",
845
+ fields=body.get("fields"))
846
+ return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
847
+ if path == "/api/knowledge/learn":
848
+ from core import knowledge
849
+ n = knowledge.learn_from_run((self._body().get("run_id") or ""))
850
+ return self._json(200, {"ok": True, "learned": n})
838
851
  m = re.match(r"^/api/flows/([^/]+)/reset$", path)
839
852
  if m:
840
853
  err = flows.reset_flow(m.group(1))
package/app/ui/app.js CHANGED
@@ -1679,7 +1679,7 @@ async function openImportDialog() {
1679
1679
  '<input type="checkbox" value="' + esc(s.id) + '"' + (ok ? " checked" : " disabled") +
1680
1680
  (ok ? ' onchange="updateImportSelHint()"' : "") + ">" +
1681
1681
  '<div class="src-main">' +
1682
- '<div class="src-name">' + esc(s.name) + status + "</div>" +
1682
+ '<div class="src-name">' + esc(t(s.name)) + status + "</div>" +
1683
1683
  '<div class="src-desc">' + esc(t(s.desc)) + "</div>" +
1684
1684
  '<div class="src-path">' + esc((s.paths || []).join(" · ")) + "</div>" +
1685
1685
  (s.note ? '<div class="src-note">' + esc(t.apply(null, [s.note].concat(s.note_args || []))) + "</div>" : "") +
@@ -3846,6 +3846,7 @@ function renderBookMetaPanel(task) {
3846
3846
  let html = '<div class="bm-head"><svg class="ico" aria-hidden="true"><use href="#i-idcard"/></svg>' +
3847
3847
  '<span class="sec-title">' + t("作品信息") + "</span>" +
3848
3848
  '<span class="hint">' + esc(t("按发布平台生成建书表单资料,逐字段复制过去")) + "</span>" +
3849
+ '<button class="hhelp" data-help-topic="publish" title="' + esc(t("这是什么?点开看帮助")) + '" aria-label="' + esc(t("帮助")) + '"><svg class="ico" aria-hidden="true"><use href="#i-help"></use></svg></button>' +
3849
3850
  '<span class="flex1"></span>';
3850
3851
  if (taskBusy) html += '<span class="bm-warn">' + esc(t("任务运行中,生成将在本轮结束后可用")) + "</span>";
3851
3852
  html += "</div>";
@@ -4284,6 +4285,7 @@ function _renderGitSnapshot(run, task) {
4284
4285
  let html =
4285
4286
  '<div class="git-head"><svg class="ico" aria-hidden="true"><use href="#i-git-branch"></use></svg>' +
4286
4287
  '<span class="sec-title">' + t("代码版本隔离") + '</span>' +
4288
+ '<button class="hhelp" data-help-topic="code" title="' + esc(t("这是什么?点开看帮助")) + '" aria-label="' + esc(t("帮助")) + '"><svg class="ico" aria-hidden="true"><use href="#i-help"></use></svg></button>' +
4287
4289
  '<code class="git-branch" title="' + esc(t("任务分支:产物提交在此,原分支未受影响")) + '">' + esc(g.branch) + "</code>";
4288
4290
  const addT = ch.add_total, delT = ch.del_total;
4289
4291
  if (addT != null && delT != null) {
@@ -4379,7 +4381,8 @@ function renderGitWbMain(d, task) {
4379
4381
  sel += "</select>";
4380
4382
  let html =
4381
4383
  '<div class="git-head"><svg class="ico" aria-hidden="true"><use href="#i-git-branch"></use></svg>' +
4382
- '<span class="sec-title">' + t("Git 工作台") + "</span>" + sel +
4384
+ '<span class="sec-title">' + t("Git 工作台") + "</span>" +
4385
+ '<button class="hhelp" data-help-topic="code" title="' + esc(t("这是什么?点开看帮助")) + '" aria-label="' + esc(t("帮助")) + '"><svg class="ico" aria-hidden="true"><use href="#i-help"></use></svg></button>' + sel +
4383
4386
  '<code class="git-branch" title="HEAD">' + esc(d.head || "") + "</code>" +
4384
4387
  (d.upstream ? '<span class="gm" title="' + esc(d.upstream) + '">↑' + (d.ahead || 0) + " ↓" + (d.behind || 0) + "</span>" : "") +
4385
4388
  '<span class="flex1"></span>' +
@@ -7519,6 +7522,126 @@ async function skillLessonOp(id, op) {
7519
7522
  loadSkills();
7520
7523
  }
7521
7524
 
7525
+ /* ---------------------------------------------------------- 知识库 */
7526
+ /* 运行产出自动提炼(草稿→人工转正)+ 手动新建的个人知识库;
7527
+ * 进页拉一次,操作后重拉,搜索/状态/标签过滤全在客户端。 */
7528
+ let KB = { data: null, q: "", tag: "", status: "" };
7529
+
7530
+ async function loadKnowledge() {
7531
+ try { KB.data = await api("/api/knowledge"); } catch (e) { KB.data = null; }
7532
+ renderKnowledge();
7533
+ }
7534
+
7535
+ function renderKnowledge() {
7536
+ const box = $("kb-cards");
7537
+ if (!box || !KB.data) return;
7538
+ const entries = KB.data.entries || [];
7539
+ // 状态 chips:全部 / 草稿 / 已转正(转正=人工确认过、参与注入)
7540
+ const stBox = $("kb-status-chips");
7541
+ if (stBox) {
7542
+ const cnt = (s) => s ? entries.filter((x) => (x.status || "draft") === s).length : entries.length;
7543
+ stBox.innerHTML = [["", "全部"], ["draft", "草稿"], ["approved", "已转正"]].map((p) =>
7544
+ '<button class="cat-chip' + (KB.status === p[0] ? " active" : "") + '" onclick="kbSetFilter(\'status\',\'' + p[0] + '\')">' +
7545
+ esc(t(p[1])) + "<b>" + cnt(p[0]) + "</b></button>").join("");
7546
+ }
7547
+ const tags = KB.data.tags || [];
7548
+ const tagBox = $("kb-tag-chips");
7549
+ if (tagBox) {
7550
+ tagBox.innerHTML = tags.length
7551
+ ? '<button class="cat-chip' + (!KB.tag ? " active" : "") + '" onclick="kbSetFilter(\'tag\',\'\')">' + esc(t("全部标签")) + "</button>" +
7552
+ tags.map((tg) => '<button class="cat-chip' + (KB.tag === tg ? " active" : "") +
7553
+ '" onclick="kbSetFilter(\'tag\',\'' + esc(tg) + '\')">' + esc(tg) + "<b>" + (KB.data.tag_counts[tg] || 0) + "</b></button>").join("")
7554
+ : "";
7555
+ }
7556
+ let shown = entries;
7557
+ if (KB.status) shown = shown.filter((x) => (x.status || "draft") === KB.status);
7558
+ if (KB.tag) shown = shown.filter((x) => (x.tags || []).includes(KB.tag));
7559
+ if (KB.q) shown = shown.filter((x) =>
7560
+ (x.title || "").toLowerCase().includes(KB.q) || (x.body || "").toLowerCase().includes(KB.q) ||
7561
+ (x.tags || []).some((tg) => tg.toLowerCase().includes(KB.q)));
7562
+ const cnt = $("kb-count");
7563
+ if (cnt) cnt.textContent = shown.length ? shown.length + t(" / 共 ") + entries.length + t(" 条") : t("(暂无)");
7564
+ box.innerHTML = shown.map(kbCard).join("") ||
7565
+ '<div class="empty">' + t("还没有知识条目——完成一次产出型任务后自动提炼(草稿态待转正),也可以点右上角手动新建。") + "</div>";
7566
+ }
7567
+
7568
+ function kbCard(x) {
7569
+ const draft = (x.status || "draft") !== "approved";
7570
+ return '<div class="card' + (x.enabled === false ? " off" : "") + '" data-kb-id="' + esc(x.id) + '"><div class="head">' +
7571
+ '<span class="name">' + esc(x.title) + "</span>" +
7572
+ (draft ? '<span class="tag warn">' + esc(t("草稿")) + "</span>" : '<span class="tag ok">' + esc(t("已转正")) + "</span>") +
7573
+ (x.stale ? '<span class="tag" title="' + esc(t("事实较旧,注入时会标注可能过期")) + '">' + esc(t("可能过期")) + "</span>" : "") +
7574
+ '<span class="tag">' + esc(x.scope === "*" ? t("通用") : x.scope) + "</span>" +
7575
+ (x.tags || []).map((tg) => '<span class="tag" onclick="kbSetFilter(\'tag\',\'' + esc(tg) + '\')" title="' + esc(t("只看该标签")) + '">' + esc(tg) + "</span>").join("") +
7576
+ (x.seen > 1 ? '<span class="tag">' + t("出现 ") + x.seen + t(" 次") + "</span>" : "") +
7577
+ (x.hits ? '<span class="tag">' + t("已注入 ") + x.hits + t(" 次") + "</span>" : "") +
7578
+ (x.enabled === false ? '<span class="tag">' + t("已停用") + "</span>" : "") + "</div>" +
7579
+ '<div class="note">' + esc(x.body) + "</div>" +
7580
+ '<div class="facts">' + (x.as_of ? t("事实截至 ") + esc(x.as_of) : "") +
7581
+ (x.source ? (x.as_of ? " · " : "") + '<a href="#" onclick="openRun(\'' + esc(x.source) + '\');return false">' + t("来源运行") + "</a>" : "") + "</div>" +
7582
+ '<div class="ops">' +
7583
+ (draft ? '<button class="ghost small" onclick="kbOp(\'' + esc(x.id) + '\', \'approve\')">' + t("转正") + "</button>" : "") +
7584
+ '<button class="ghost small" onclick="kbEditOpen(\'' + esc(x.id) + '\')">' + t("编辑") + "</button>" +
7585
+ '<button class="ghost small" onclick="kbOp(\'' + esc(x.id) + '\', \'' + (x.enabled === false ? "enable" : "disable") + '\')">' + (x.enabled === false ? t("启用") : t("停用")) + "</button>" +
7586
+ '<button class="danger small" onclick="kbOp(\'' + esc(x.id) + '\', \'delete\')">' + t("删除") + "</button>" +
7587
+ "</div></div>";
7588
+ }
7589
+
7590
+ function kbSetFilter(k, v) {
7591
+ KB[k] = v || "";
7592
+ renderKnowledge(); // 客户端过滤,无需重拉
7593
+ }
7594
+
7595
+ function kbSearch(v) {
7596
+ KB.q = String(v || "").trim().toLowerCase();
7597
+ renderKnowledge();
7598
+ }
7599
+
7600
+ async function kbOp(id, op) {
7601
+ if (op === "delete" && !await uiConfirm(t("删除这条知识?转正条目不再注入,删除后不可恢复。"), { ok: t("删除"), danger: true })) return;
7602
+ try { await api("/api/knowledge/op", { method: "POST", body: JSON.stringify({ id, op }) }); }
7603
+ catch (e) { toast(e.message, true); return; }
7604
+ loadKnowledge();
7605
+ }
7606
+
7607
+ function kbFormOpen(x) { // x 为空 = 新建(保存即转正),否则编辑(不改状态)
7608
+ const isNew = !x;
7609
+ openModal(isNew ? t("新建知识条目") : t("编辑知识条目"),
7610
+ '<div class="form">' +
7611
+ '<div class="field"><label>' + t("标题") + ' <span class="req">*</span></label><input id="kb-f-title" value="' + esc(x ? x.title || "" : "") + '"></div>' +
7612
+ '<div class="field"><label>' + t("正文") + ' <span class="req">*</span></label><textarea id="kb-f-body" rows="5">' + esc(x ? x.body || "" : "") + "</textarea></div>" +
7613
+ '<div class="grid-2">' +
7614
+ '<div class="field"><label>' + t("标签(逗号分隔)") + '</label><input id="kb-f-tags" value="' + esc((x ? x.tags || [] : []).join(", ")) + '" data-i18n-ph="例:番茄, 签约" placeholder="例:番茄, 签约"></div>' +
7615
+ '<div class="field"><label>' + t("事实截至") + '</label><input id="kb-f-asof" value="' + esc(x ? x.as_of || "" : "") + '" placeholder="YYYY-MM-DD"></div>' +
7616
+ "</div>" +
7617
+ '<div class="field"><label>' + t("适用范围(任务类型,* 为通用)") + '</label><input id="kb-f-scope" value="' + esc(x ? x.scope || "*" : "*") + '" placeholder="novel / serial_novel / code / *"></div>' +
7618
+ "</div>",
7619
+ '<button class="ghost" onclick="closeModal()">' + t("取消") + "</button>" +
7620
+ '<button onclick="kbFormSave(' + (isNew ? "" : "'" + esc(x.id) + "'") + ')">' + t("保存") + "</button>");
7621
+ }
7622
+
7623
+ async function kbFormSave(id) {
7624
+ const fields = {
7625
+ title: $("kb-f-title").value.trim(),
7626
+ body: $("kb-f-body").value.trim(),
7627
+ tags: $("kb-f-tags").value.split(/[,,]/).map((s) => s.trim()).filter(Boolean),
7628
+ scope: $("kb-f-scope").value.trim() || "*",
7629
+ as_of: $("kb-f-asof").value.trim(),
7630
+ };
7631
+ if (!fields.title || !fields.body) { toast(t("标题与正文不能为空"), true); return; }
7632
+ try {
7633
+ await api("/api/knowledge/op", { method: "POST", body: JSON.stringify(id
7634
+ ? { id, op: "edit", fields }
7635
+ : { op: "create", fields }) });
7636
+ } catch (e) { toast(e.message, true); return; }
7637
+ closeModal();
7638
+ loadKnowledge();
7639
+ }
7640
+
7641
+ function kbEditOpen(id) {
7642
+ kbFormOpen((KB.data && KB.data.entries || []).find((x) => x.id === id));
7643
+ }
7644
+
7522
7645
  /* ---------------------------------------------------------- 自动化(定时任务) */
7523
7646
  /* 进页拉一次 + 每次操作后重拉 + 页面可见时每 8s 轮询(离页停表)。
7524
7647
  * 后端 /api/automation → {tasks, templates};任务/模板操作走子路径。 */
@@ -8191,7 +8314,7 @@ function mkrCardHtml(p) {
8191
8314
  const meta = [p.author, p.version ? "v" + p.version : ""].filter(Boolean).join(" · ");
8192
8315
  return '<div class="card mk-card' + (p.compat === "blocked" ? " blocked" : "") + '"><div class="head">' +
8193
8316
  '<span class="name">' + esc(t(p.title || p.name)) + "</span>" +
8194
- '<span class="tag mk-src">' + esc(p.source_name || "") + "</span>" +
8317
+ '<span class="tag mk-src">' + esc(t(p.source_name || "")) + "</span>" +
8195
8318
  (p.category ? '<span class="tag">' + esc(p.category) + "</span>" : "") +
8196
8319
  "</div>" +
8197
8320
  '<div class="note">' + esc(t(p.desc || "")) + "</div>" +
@@ -8214,7 +8337,7 @@ function renderMarketRemote() {
8214
8337
  if (srcSel && !srcSel.options.length) {
8215
8338
  srcSel.innerHTML = '<option value="">' + t("全部来源") + "</option>" +
8216
8339
  (data.sources || []).map((s) =>
8217
- '<option value="' + esc(s.id) + '">' + esc(s.name) + t("(") + s.count + t(")") + "</option>").join("");
8340
+ '<option value="' + esc(s.id) + '">' + esc(t(s.name)) + t("(") + s.count + t(")") + "</option>").join("");
8218
8341
  }
8219
8342
  const times = (data.sources || []).map((s) => s.fetched_at).filter(Boolean);
8220
8343
  const meta = $("mkr-meta");
@@ -8269,7 +8392,7 @@ async function mkrRefresh() {
8269
8392
  const keep = srcSel.value;
8270
8393
  srcSel.innerHTML = '<option value="">' + t("全部来源") + "</option>" +
8271
8394
  (r.sources || []).map((s) =>
8272
- '<option value="' + esc(s.id) + '">' + esc(s.name) + t("(") + s.count + t(")") + "</option>").join("");
8395
+ '<option value="' + esc(s.id) + '">' + esc(t(s.name)) + t("(") + s.count + t(")") + "</option>").join("");
8273
8396
  srcSel.value = keep;
8274
8397
  }
8275
8398
  mkrLoadPage(true);
@@ -8389,7 +8512,7 @@ async function loadSettingsV2() {
8389
8512
  inp = '<input id="' + iid + '" type="text" value="' + esc(String(cur)) + '">';
8390
8513
  }
8391
8514
  html += '<div class="set-v2-row">' + inp +
8392
- '<span class="set-v2-lb" title="' + esc(f.description || f.path) + '">' + esc(f.description || f.path) + "</span></div>";
8515
+ '<span class="set-v2-lb" title="' + esc(t(f.description || f.path)) + '">' + esc(t(f.description || f.path)) + "</span></div>";
8393
8516
  }
8394
8517
  html += '<div class="input-row" style="margin-top:6px"><button class="ghost small" onclick="saveSettingsV2(\'' + esc(ns) + '\')">' + t("保存") + "</button>" +
8395
8518
  '<span class="set-v2-msg msg"></span></div></div>';
@@ -8577,6 +8700,7 @@ var HELP_CHAPTERS = [
8577
8700
  { id: "features", icon: "i-bee", label: "功能一览" },
8578
8701
  { id: "serial", icon: "i-book-open", label: "连载创作流程" },
8579
8702
  { id: "publish", icon: "i-clapper", label: "发布上架" },
8703
+ { id: "code", icon: "i-git-branch", label: "代码任务与版本" },
8580
8704
  { id: "auto", icon: "i-calendar-days", label: "自动化与技能市场" },
8581
8705
  { id: "faq", icon: "i-search", label: "常见问题" },
8582
8706
  ];
@@ -8620,6 +8744,7 @@ function helpChapterBody(id) {
8620
8744
  '<li><svg class="ico" aria-hidden="true"><use href="#i-clapper"></use></svg><div><b>' + t("发布上架") + '</b><span>' + t("一键发布到番茄、七猫,含封面与作品资料") + '</span></div></li>' +
8621
8745
  '<li><svg class="ico" aria-hidden="true"><use href="#i-git-branch"></use></svg><div><b>' + t("版本工作台") + '</b><span>' + t("任务分支隔离,改动可审可合") + '</span></div></li>' +
8622
8746
  '<li><svg class="ico" aria-hidden="true"><use href="#i-calendar-days"></use></svg><div><b>' + t("自动化") + '</b><span>' + t("定时任务,无人值守") + '</span></div></li>' +
8747
+ '<li><svg class="ico" aria-hidden="true"><use href="#i-library"></use></svg><div><b>' + t("经验库") + '</b><span>' + t("内置规范包 + 自动沉淀教训,越用越顺手") + '</span></div></li>' +
8623
8748
  '<li><svg class="ico" aria-hidden="true"><use href="#i-blocks"></use></svg><div><b>' + t("插件市场") + '</b><span>' + t("600+ 技能,一键安装") + '</span></div></li>' +
8624
8749
  '</ul>';
8625
8750
  }
@@ -8643,13 +8768,25 @@ function helpChapterBody(id) {
8643
8768
  '<h3 class="help-h3">' + t("发布失败") + '</h3>' +
8644
8769
  '<p class="help-p">' + t("平台改版或登录态过期会导致失败,失败信息会写明原因。重新登录后再点重试即可;反复失败可以到 GitHub 提 Issue 附上失败截图。") + '</p>';
8645
8770
  }
8771
+ if (id === "code") {
8772
+ return '<h3 class="help-h3">' + t("代码任务怎么跑") + '</h3>' +
8773
+ '<p class="help-p">' + t("新建任务选开发类类型,目标写清楚要做什么(可以附需求文档、截图,附件会进入上下文)。蜂群先出计划再实现:实现 → 客观验证 → 跨厂商评审 → 修复,单路实现失败还有多候选赛马——各候选在隔离的 worktree 里各写各的,择优采纳。跑的过程中可以在详情页递话(文字/截图),下一轮生效。") + '</p>' +
8774
+ '<h3 class="help-h3">' + t("任务分支隔离:你的工作区不受影响") + '</h3>' +
8775
+ '<p class="help-p">' + t("启用代码版本隔离的任务会检出独立的任务分支(codebee/任务ID),所有改动只落在那条分支上,你当前的分支和未提交的改动完全不动——开跑前工作区有脏改动会先帮你收起(stash),跑完自动还原。") + '</p>' +
8776
+ '<h3 class="help-h3">' + t("版本裁决:合并还是丢弃") + '</h3>' +
8777
+ '<p class="help-p">' + t("任务跑完后,「版本」页签会亮起「待裁决」等你拍板:先看变更清单,双击文件能看 diff;满意点「合并」,任务分支合回基线分支;不满意点「丢弃」,分支和工作现场自动清场。中途停下的任务也会留好提交,可以继续跑完再裁决。合并与丢弃绝不静默替你决定。") + '</p>' +
8778
+ '<h3 class="help-h3">' + t("哪里看细节") + '</h3>' +
8779
+ '<p class="help-p">' + t("「蜂巢」页签看每个智能体实时在干什么(点格子看实时日志);「步骤」页签看任务拆解、每步实际用的模型、耗时与失败原因,评审分数与打磨记录也在这里。") + '</p>';
8780
+ }
8646
8781
  if (id === "auto") {
8647
8782
  return '<h3 class="help-h3">' + t("定时任务") + '</h3>' +
8648
8783
  '<p class="help-p">' + t("设置 → 自动化:把任意任务类型挂上时间表(每天/每周固定时刻),到点自动按真实运行链开跑,结果照常落盘。配合群机器人(钉钉/飞书/企微自动适配)可以把完成状态推到群里。") + '</p>' +
8784
+ '<h3 class="help-h3">' + t("禅道 Bug 自动修复") + '</h3>' +
8785
+ '<p class="help-p">' + t("设置 → 禅道:配好地址与产品档案后,定时扫描激活 Bug,按模块路由 / AI 排查定责(前端/后端/双端/非我方);我方端的 Bug 自动创建代码修复任务,修完自动合并、resolve 并回写报告到群里;测试指错人的也会按排查结论改派。合并失败不 resolve 不转派,留人工兜底。") + '</p>' +
8649
8786
  '<h3 class="help-h3">' + t("技能市场") + '</h3>' +
8650
8787
  '<p class="help-p">' + t("设置 → 插件市场:600+ 技能一键安装,来源包括内置库和 ZCode、Anthropic 等多个外部目录。装上的技能会注入智能体能力,按任务类型生效。") + '</p>' +
8651
8788
  '<h3 class="help-h3">' + t("用量台账") + '</h3>' +
8652
- '<p class="help-p">' + t("设置 → 用量:每次调用的模型、token 与费用按多个维度聚合展示,历史运行会在启动时自动回填——每个任务花了多少,一目了然。") + '</p>';
8789
+ '<p class="help-p">' + t("设置 → 用量统计:每次调用的模型、token 与费用按多个维度聚合展示,历史运行会在启动时自动回填——每个任务花了多少,一目了然。") + '</p>';
8653
8790
  }
8654
8791
  if (id === "faq") {
8655
8792
  return '<div class="help-qa"><b>' + t("点「获取模型列表」报 404?") + '</b><p>' + t("部分网关不提供模型列表接口,属正常现象;只要能正常对话就不用管,模型名手工填即可。") + '</p></div>' +
@@ -8690,6 +8827,12 @@ function setHelpChapter(id) {
8690
8827
  renderHelp();
8691
8828
  }
8692
8829
  function helpGo(id) { setHelpChapter(id); }
8830
+ function paintHelpChrome() {
8831
+ const badge = $("welcome-badge");
8832
+ if (badge) badge.textContent = t(helpFirst ? "首次启动" : "帮助中心");
8833
+ const title = $("welcome-title");
8834
+ if (title) title.textContent = t(helpFirst ? "欢迎使用 CodeBee" : "帮助中心");
8835
+ }
8693
8836
  function welcomeOpen(opts) {
8694
8837
  const w = $("welcome");
8695
8838
  if (!w) return;
@@ -8697,8 +8840,7 @@ function welcomeOpen(opts) {
8697
8840
  helpFirst = !!(opts && opts.firstRun);
8698
8841
  if (helpFirst) helpCur = "quickstart";
8699
8842
  else if (opts && opts.topic && HELP_CHAPTERS.some((c) => c.id === opts.topic)) helpCur = opts.topic;
8700
- const badge = $("welcome-badge");
8701
- if (badge) badge.textContent = t(helpFirst ? "首次启动" : "帮助中心");
8843
+ paintHelpChrome();
8702
8844
  renderHelp();
8703
8845
  w.classList.remove("hidden");
8704
8846
  document.body.classList.add("welcome-open");
@@ -8906,7 +9048,7 @@ async function suStartupCheck() {
8906
9048
  }
8907
9049
 
8908
9050
  /* ---------------------------------------------------------- 页签 & 初始化 */
8909
- const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "智能体管理", models: "模型接入", bindings: "CLI 绑定", skills: "经验库", market: "插件市场", orch: "编排设置", appearance: "皮肤", about: "关于与更新" };
9051
+ const TAB_TITLES = { tasks: "任务", runs: "运行记录", automation: "自动化", zentao: "禅道 Bug 自动修复", usage: "用量统计", agents: "智能体管理", models: "模型接入", bindings: "CLI 绑定", skills: "经验库", knowledge: "知识库", market: "插件市场", orch: "编排设置", appearance: "皮肤", about: "关于与更新" };
8910
9052
  const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
8911
9053
 
8912
9054
  function tabTitle(name) {
@@ -9070,10 +9212,9 @@ function setLangBtn(lang) {
9070
9212
  paintArchToggle();
9071
9213
  syncSideExpandBtn();
9072
9214
  renderCodePreviews(); // 预览徽章是动态文案:语言切换时若停在皮肤页要跟着换
9073
- // 帮助中心若开着:徽章/目录/正文跟着换语言重画
9215
+ // 帮助中心若开着:徽章/标题/目录/正文跟着换语言重画
9074
9216
  if (!$("welcome").classList.contains("hidden")) {
9075
- const wbadge = $("welcome-badge");
9076
- if (wbadge) wbadge.textContent = t(helpFirst ? "首次启动" : "帮助中心");
9217
+ paintHelpChrome();
9077
9218
  renderHelp();
9078
9219
  }
9079
9220
  // 重画还在缓存里的页面标题
@@ -9554,7 +9695,7 @@ function collapseDrawerIfMobile() {
9554
9695
 
9555
9696
  function switchTab(name) {
9556
9697
  if (name === "__phone") { openPhoneConnect(); return; } // 手机连接是弹框,不切页
9557
- if (name === "__guide") { welcomeOpen(); return; } // 使用引导是弹层,不切页(设置导航「软件」组)
9698
+ if (name === "__guide") { welcomeOpen(); return; } // 帮助中心是弹层,不切页(设置导航「软件」组)
9558
9699
  // 导航收进「设置」:进设置后左栏整体换成设置导航,内容铺满
9559
9700
  S.tab = name;
9560
9701
  if (SET_TABS.has(name)) localStorage.setItem("orch.setTab", name);
@@ -9569,6 +9710,7 @@ function switchTab(name) {
9569
9710
  if (name === "agents") autoCheckUpdates(); // 进目录页自动查各 CLI 新版本
9570
9711
  if (name === "orch") { loadOrchestrator(); loadSettings(); } // 进编排设置页拉取配置
9571
9712
  if (name === "skills") loadSkills(); // 进经验库页拉取沉淀
9713
+ if (name === "knowledge") loadKnowledge(); // 进知识库页拉取条目
9572
9714
  if (name === "automation") { loadAutomation(); startAutoPoll(); } // 进自动化页:拉取 + 页面可见时每 8s 轮询
9573
9715
  else stopAutoPoll(); // 离开自动化页(或切到别的子页)即停表
9574
9716
  if (name === "zentao") loadZentao(); // 进禅道页:拉配置与修复记录回填表单
@@ -10041,6 +10183,7 @@ document.addEventListener("DOMContentLoaded", () => {
10041
10183
  && e.target.tagName !== "TEXTAREA") { _askClose(true); return; }
10042
10184
  // 文件内容弹窗(240) 夹在 ask(250) 与 modal(200) 之间,Esc 同样逐层关
10043
10185
  if (e.key === "Escape" && filePopIsOpen()) { window.filePopClose(); return; }
10186
+ if (e.key === "F1") { e.preventDefault(); welcomeOpen(); return; } // F1 = 帮助中心(浏览器帮助被拦下)
10044
10187
  if (e.key === "Escape" && !$("welcome").classList.contains("hidden")) { welcomeClose(); return; }
10045
10188
  if (e.key === "Escape" && !$("modal").classList.contains("hidden")) closeModal();
10046
10189
  // 日志抽屉:Esc 收起(最底层,放在弹窗之后)
@@ -10136,10 +10279,11 @@ function cmdkOps() {
10136
10279
  { icon: "i-calendar-days", label: t("自动化"), run: () => switchTab("automation") },
10137
10280
  { icon: "i-blocks", label: t("插件市场"), run: () => switchTab("market") },
10138
10281
  { icon: "i-book", label: t("经验库"), run: () => switchTab("skills") },
10282
+ { icon: "i-sigma", label: t("知识库"), run: () => switchTab("knowledge") },
10139
10283
  { icon: "i-cpu", label: t("智能体管理"), run: () => switchTab("agents") },
10140
10284
  { icon: "i-chart", label: t("用量统计"), run: () => switchTab("usage") },
10141
10285
  { icon: "i-gear", label: t("设置"), run: () => enterSettings() },
10142
- { icon: "i-bee", label: t("使用引导"), run: () => welcomeOpen() },
10286
+ { icon: "i-bee", label: t("帮助中心"), run: () => welcomeOpen() },
10143
10287
  { icon: "i-phone", label: t("手机连接"), run: () => switchTab("__phone") },
10144
10288
  ];
10145
10289
  }