codebee 0.1.17 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
4
4
  `<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
5
5
  README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
6
6
 
7
+ ## v0.1.18(2026-09-20)
8
+
9
+ - 知识库:运行产出自动整理成可复用知识——编排者从产出材料(调研报告/文档)提炼知识条目(草稿态),人工「转正」后按任务类型+相关性自动注入同类任务的规划与评审提示词;与经验库分工(教训记「别这么做」,知识记「已知是这样」);设置页独立「知识库」子页,搜索/状态/标签过滤+手动新建(即个人知识管理库);事实带账龄,as_of 超 90 天注入时自动标注「可能过期」
10
+ - 封面生成:智谱 CogView 接线(CODEBEE_IMAGE_WATERMARK=1 才保留供应商水印);封面落运行目录,专用通道内嵌缩略图
11
+ - 七猫发布纠偏:目标读者/分类旧数据不自洽时按官方目录反查,不再死在流程中段
12
+ - 队列补队治理:CLI 安装/升级等管理操作也纳入启动补队与巡检(selfupgrade 除外),排队不再无人接案
13
+
7
14
  ## v0.1.17(2026-09-20)
8
15
 
9
16
  - 任务宪章:工作目录 .codebee/constitution.md 一次定义质量原则(代码规范/文风/测试要求),每次运行自动注入所有智能体——注入体系五层就位(宪章>圣经>模块库>经验>记忆)
package/README.md CHANGED
@@ -21,10 +21,10 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
21
21
  不会把你的任务内容交给任何第三方。
22
22
 
23
23
  <!-- relnotes:start -->
24
- ### 最新版更新内容(v0.1.16
24
+ ### 最新版更新内容(v0.1.18
25
25
 
26
- - **帮助中心**:首启欢迎引导升级为多章帮助中心——快速上手 / 模型接入与绑定 / 连载创作流程 / 发布上架 / 代码任务与版本 / 自动化与技能市场 / 功能一览 / 常见问题八章全就绪;设置各子页与蜂巢、Git 工作台、作品信息等十二处「?」就地问号直达对应章节;**F1** 或设置导航、关于页、Ctrl+K 随时可重开
27
- - 禅道集成、扫榜选材(双源)、可分享报告页、引擎调参卡见 v0.1.15
26
+ - 新增「知识库」:产出材料自动提炼成知识条目,转正后按任务类型自动注入规划与评审——你的个人知识管理库
27
+ - 新增封面生成(智谱 CogView);七猫发布分类纠偏;管理操作排队补队治理
28
28
  <!-- relnotes:end -->
29
29
 
30
30
  ---
@@ -0,0 +1,371 @@
1
+ # -*- coding: utf-8 -*-
2
+ """知识库(Knowledge):运行产出自动整理形成的可复用知识 + 个人知识管理入口。
3
+
4
+ 与经验库(skills.py)的分工边界:
5
+ - 教训(lessons)记「别这么做」——负面规则,来源=评审暴露的问题(verdict/issues);
6
+ - 知识(knowledge)记「已知是这样」——领域事实/平台规则/结论/方法论,
7
+ 来源=run 产出材料(调研报告/文档)。两者输入源不同,提炼互不双写。
8
+
9
+ 质量闸门:自动提炼的条目一律落 draft(草稿态),人工「转正」后才参与注入——
10
+ 垃圾知识进了提示词比没有知识更糟;手动新建即视为已确认(直接 approved)。
11
+
12
+ 注入纪律(与教训反着来):教训是全量小注(top-8),知识默认不注入——
13
+ scope 命中且库里有 approved 条目才成块,按与任务目标的相关性取 top,
14
+ 独立预算截断(KNOWLEDGE_BUDGET),绝不挤占经验包/圣经的空间。
15
+ 选择依据在单次注入内恒定(goal 固定、id 唯一),同一任务字节稳定,
16
+ 不碎供应商前缀缓存。
17
+
18
+ 事实会过期:竞品/平台类知识每条带 as_of(事实采集日),超过 STALE_DAYS
19
+ 注入时自动标注「可能过期」,提示模型自行核实时效。
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import json
25
+ import re
26
+ import threading
27
+ import time
28
+ from pathlib import Path
29
+
30
+ from . import paths, skills
31
+
32
+ _LOCK = threading.RLock()
33
+ _FILE = paths.DATA_DIR / "knowledge.json"
34
+
35
+ KNOWLEDGE_BUDGET = 3000 # 注入块字符预算(独立于经验包的 MAX_INJECT_CHARS)
36
+ KNOWLEDGE_MAX_INJECT = 6 # 单次注入条数上限
37
+ STALE_DAYS = 90 # as_of 超过该天数标注「可能过期」
38
+
39
+
40
+ def _now():
41
+ return time.strftime("%Y-%m-%d %H:%M:%S")
42
+
43
+
44
+ def _load():
45
+ try:
46
+ return json.loads(_FILE.read_text(encoding="utf-8"))
47
+ except Exception:
48
+ return {}
49
+
50
+
51
+ def _save(data):
52
+ try:
53
+ _FILE.parent.mkdir(parents=True, exist_ok=True)
54
+ _FILE.write_text(json.dumps(data, ensure_ascii=False, indent=1),
55
+ encoding="utf-8")
56
+ except Exception:
57
+ pass
58
+
59
+
60
+ def _entry_id(scope, title):
61
+ """去重指纹:scope + 归一化标题(沿用教训库的 _norm_title 标点剥离)。"""
62
+ h = hashlib.sha256(("%s|%s" % (scope, skills._norm_title(title)))
63
+ .encode("utf-8")).hexdigest()[:12]
64
+ return "kb-" + h
65
+
66
+
67
+ def list_entries(scope=None, status=None, tag=None, only_enabled=False):
68
+ with _LOCK:
69
+ items = list(_load().get("entries") or [])
70
+ if scope:
71
+ items = [x for x in items if x.get("scope") in (scope, "*")]
72
+ if status:
73
+ items = [x for x in items if (x.get("status") or "draft") == status]
74
+ if tag:
75
+ items = [x for x in items if tag in (x.get("tags") or [])]
76
+ if only_enabled:
77
+ items = [x for x in items if x.get("enabled", True)]
78
+ items.sort(key=lambda x: (x.get("updated_at") or x.get("created_at") or ""),
79
+ reverse=True)
80
+ return items
81
+
82
+
83
+ def upsert_entry(scope, title, body, tags=None, source="", source_file="",
84
+ as_of=None, status="draft"):
85
+ """写入/合并一条知识。同 scope 同标题视为同一条(指纹去重):
86
+ - 已有条目是 approved 且新条是 draft:不覆盖正文(人工确认过的内容优先),
87
+ 新内容记入 revisions 作为修订候选,seen+1;
88
+ - 其余情况(已有是 draft,或新条是 approved):正文取新。
89
+ 返回条目;title/body 为空返回 None。"""
90
+ title = str(title or "").strip()[:80]
91
+ body = str(body or "").strip()[:1500]
92
+ if not title or not body:
93
+ return None
94
+ tags = [str(t).strip()[:20] for t in (tags or []) if str(t).strip()][:6]
95
+ status = status if status in ("draft", "approved") else "draft"
96
+ scope = str(scope or "*").strip() or "*"
97
+ kid = _entry_id(scope, title)
98
+ with _LOCK:
99
+ data = _load()
100
+ items = data.setdefault("entries", [])
101
+ for it in items:
102
+ if it.get("id") != kid:
103
+ continue
104
+ it["seen"] = int(it.get("seen") or 1) + 1
105
+ if status == "approved" or it.get("status") != "approved":
106
+ it["body"] = body
107
+ if status == "approved":
108
+ it["status"] = "approved"
109
+ else:
110
+ it.setdefault("revisions", []).append(
111
+ {"at": _now(), "body": body, "source": source})
112
+ it["revisions"] = it["revisions"][-5:] # 最多留 5 条修订候选
113
+ for tg in tags:
114
+ if tg not in (it.get("tags") or []):
115
+ it["tags"] = (it.get("tags") or []) + [tg]
116
+ it["tags"] = (it.get("tags") or [])[:8]
117
+ if as_of:
118
+ it["as_of"] = str(as_of)[:10]
119
+ if source:
120
+ it["source"] = source
121
+ if source_file:
122
+ it["source_file"] = source_file
123
+ it["updated_at"] = _now()
124
+ _save(data)
125
+ return it
126
+ it = {"id": kid, "scope": scope, "title": title, "body": body,
127
+ "tags": tags, "status": status, "enabled": True,
128
+ "source": source, "source_file": source_file,
129
+ "as_of": str(as_of or _now()[:10])[:10],
130
+ "revisions": [], "hits": 0, "seen": 1,
131
+ "created_at": _now(), "updated_at": _now(), "kind": "knowledge"}
132
+ items.append(it)
133
+ _save(data)
134
+ return it
135
+
136
+
137
+ def entry_op(entry_id, op, fields=None):
138
+ """新建/编辑/转正/启停/删除。返回错误或 None。
139
+
140
+ create:手动新建直接 approved(人工录入即确认);edit:改标题/正文/标签/
141
+ 范围/账龄,不改状态——转正必须显式走 approve,保持人工闸门。
142
+ """
143
+ if op not in ("create", "edit", "approve", "enable", "disable", "delete"):
144
+ return "未知操作 " + str(op)
145
+ if op == "create":
146
+ f = fields or {}
147
+ it = upsert_entry(f.get("scope") or "*", f.get("title") or "",
148
+ f.get("body") or "", tags=f.get("tags"),
149
+ as_of=(f.get("as_of") or "").strip() or None,
150
+ status="approved")
151
+ return None if it else "标题与正文不能为空"
152
+ with _LOCK:
153
+ data = _load()
154
+ items = data.get("entries") or []
155
+ hit = next((x for x in items if x.get("id") == entry_id), None)
156
+ if not hit:
157
+ return "知识条目不存在"
158
+ if op == "delete":
159
+ data["entries"] = [x for x in items if x.get("id") != entry_id]
160
+ elif op == "approve":
161
+ hit["status"] = "approved"
162
+ hit["updated_at"] = _now()
163
+ elif op == "edit":
164
+ f = fields or {}
165
+ if str(f.get("title") or "").strip():
166
+ hit["title"] = str(f["title"]).strip()[:80]
167
+ if str(f.get("body") or "").strip():
168
+ hit["body"] = str(f["body"]).strip()[:1500]
169
+ if "tags" in f:
170
+ hit["tags"] = [str(t).strip()[:20]
171
+ for t in (f.get("tags") or []) if str(t).strip()][:8]
172
+ if str(f.get("scope") or "").strip():
173
+ hit["scope"] = str(f["scope"]).strip()[:30]
174
+ if str(f.get("as_of") or "").strip():
175
+ hit["as_of"] = str(f["as_of"]).strip()[:10]
176
+ hit["updated_at"] = _now()
177
+ else:
178
+ hit["enabled"] = (op == "enable")
179
+ _save(data)
180
+ return None
181
+
182
+
183
+ def _is_stale(as_of):
184
+ try:
185
+ d = time.strptime(str(as_of or "")[:10], "%Y-%m-%d")
186
+ return (time.mktime(time.localtime()) - time.mktime(d)) / 86400.0 > STALE_DAYS
187
+ except Exception:
188
+ return False
189
+
190
+
191
+ def block_for(task):
192
+ """生成注入提示词的知识块。默认不注入:scope 命中且存在 approved 条目才成块,
193
+ 按与任务目标/上下文的相关性取 top(复用教训库的 bigram 检索),独立预算截断。
194
+ 无命中返回 ""——字节稳定,不碎供应商前缀缓存。"""
195
+ scope = (task or {}).get("type") or "*"
196
+ entries = [x for x in list_entries(scope, only_enabled=True)
197
+ if (x.get("status") or "draft") == "approved"]
198
+ if not entries:
199
+ return ""
200
+ probe = (skills._text_bigrams((task or {}).get("goal"))
201
+ | skills._text_bigrams((task or {}).get("context"))
202
+ | skills._text_bigrams((task or {}).get("title")))
203
+ if probe:
204
+ def rank(x):
205
+ grams = (skills._text_bigrams(x.get("title"))
206
+ | skills._text_bigrams(x.get("body"))
207
+ | set(x.get("tags") or []))
208
+ return (-len(probe & grams), x.get("id") or "")
209
+ entries = sorted(entries, key=rank)
210
+ else:
211
+ entries.sort(key=lambda x: x.get("id") or "")
212
+ entries = entries[:KNOWLEDGE_MAX_INJECT]
213
+
214
+ lines, used = [], []
215
+ for x in entries:
216
+ stale = _is_stale(x.get("as_of"))
217
+ mark = ("(事实截至 %s,可能过期,请自行核实时效)" % x.get("as_of") if stale
218
+ else ("(事实截至 %s)" % x.get("as_of") if x.get("as_of") else ""))
219
+ lines.append("- **%s**%s:%s" % (x["title"], mark, x["body"]))
220
+ used.append(x["id"])
221
+ text = "## 知识库(已确认的领域知识,供参考)\n\n" + "\n".join(lines)
222
+ if len(text) > KNOWLEDGE_BUDGET:
223
+ text = text[:KNOWLEDGE_BUDGET] + "\n…(已截断)"
224
+ if used:
225
+ _bump_hits(used)
226
+ return text
227
+
228
+
229
+ def _bump_hits(ids):
230
+ with _LOCK:
231
+ data = _load()
232
+ dirty = False
233
+ for it in (data.get("entries") or []):
234
+ if it.get("id") in ids:
235
+ it["hits"] = int(it.get("hits") or 0) + 1
236
+ dirty = True
237
+ if dirty:
238
+ _save(data)
239
+
240
+
241
+ # ---------------------------------------------------------------- 自动整理
242
+
243
+ KNOWLEDGE_PROMPT = """你是编排系统的知识管理员。下面是一次任务的目标与它的产出材料(调研报告/文档等)。
244
+ 请从产出中提炼**可长期复用的知识条目**:领域事实、平台规则、结论、方法论。
245
+ 注意:只提炼事实性/结论性内容;「下次要避免什么」这类负面教训由另一个复盘流程负责,你不要写。
246
+ 只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
247
+ {"entries": [{"title": "≤20 字的知识标题", "body": "具体结论(≤200 字,自含上下文,脱离本次任务也能看懂)", "tags": ["1-3 个检索标签"], "as_of": "YYYY-MM-DD(事实采集日)"}]}
248
+ 最多 3 条,只保留有明确复用价值的;产出里没有值得沉淀的就返回空数组。
249
+
250
+ ## 任务类型
251
+ __TYPE__
252
+
253
+ ## 任务目标
254
+ __GOAL__
255
+
256
+ ## 产出材料(节选)
257
+ __MATERIAL__"""
258
+
259
+
260
+ def _read_text(p):
261
+ """产物读取:utf-8 严格优先,GBK 兜底(codex/pwsh 落盘编码不一)。"""
262
+ try:
263
+ return p.read_text(encoding="utf-8")
264
+ except Exception:
265
+ try:
266
+ return p.read_text(encoding="gbk", errors="replace")
267
+ except Exception:
268
+ return ""
269
+
270
+
271
+ def _pick_material(run):
272
+ """挑本次 run 的产出材料喂给编排者:最近的 .md/.txt 文档各截 4000 字,
273
+ 外加运行目录里的评审报告。总量封顶 10000 字。"""
274
+ from . import store
275
+ try:
276
+ wd, files = store.run_artifacts(run.get("id"))
277
+ except Exception:
278
+ return ""
279
+ chunks = []
280
+ try:
281
+ rp = paths.RUNS_DIR / (run.get("id") or "") / "report.md"
282
+ if rp.is_file():
283
+ t = _read_text(rp)
284
+ if t.strip():
285
+ chunks.append("### 评审报告(节选)\n" + t[:4000])
286
+ except Exception:
287
+ pass
288
+ docs = [f for f in (files or [])
289
+ if str(f.get("name", "")).lower().endswith((".md", ".txt"))
290
+ and int(f.get("size") or 0) < 500_000]
291
+ for f in docs[:3]:
292
+ t = _read_text(Path(wd) / f["name"])
293
+ if t.strip():
294
+ chunks.append("### " + f["name"] + "\n" + t[:4000])
295
+ return "\n\n".join(chunks)[:10000]
296
+
297
+
298
+ def learn_from_run(run_id):
299
+ """运行结束后由编排者从产出材料提炼知识条目(草稿态)。返回写入条数。
300
+
301
+ 知识没有便宜的兜底路径:无编排者/无产出/非真实运行(mock)一律静默跳过,
302
+ 宁缺毋滥——教训库的规则兜底搬到这里只会制造垃圾知识。
303
+ """
304
+ from . import store
305
+ run = store.get_run(run_id)
306
+ if not run:
307
+ return 0
308
+ task = store.get_task(run.get("task_id")) if run.get("task_id") else None
309
+ if not task:
310
+ return 0
311
+ if all((s.get("agent") or "").startswith("mock") for s in (run.get("steps") or [])):
312
+ return 0
313
+ material = _pick_material(run)
314
+ if not material:
315
+ return 0
316
+ n = 0
317
+ try:
318
+ from . import modelhub, runner
319
+ orch = modelhub.resolve_orchestrator()
320
+ if not orch:
321
+ return 0
322
+ prov, model = orch
323
+ prompt = (KNOWLEDGE_PROMPT
324
+ .replace("__TYPE__", str(task.get("type")))
325
+ .replace("__GOAL__", (task.get("goal") or "")[:600])
326
+ .replace("__MATERIAL__", material))
327
+ res = modelhub.chat(prov["id"], model, prompt, max_tokens=4000, timeout=300)
328
+ if not res.get("ok"):
329
+ return 0
330
+ data = runner.extract_json(res.get("text") or "")
331
+ raw = (data or {}).get("entries") if isinstance(data, dict) else None
332
+ if not isinstance(raw, list):
333
+ return 0
334
+ for x in raw[:3]:
335
+ if not (isinstance(x, dict) and x.get("title") and x.get("body")):
336
+ continue
337
+ as_of = str(x.get("as_of") or "").strip()
338
+ if not re.match(r"^\d{4}-\d{2}-\d{2}$", as_of):
339
+ as_of = _now()[:10]
340
+ if upsert_entry(task.get("type") or "*", x["title"], x["body"],
341
+ tags=x.get("tags"), source=run_id,
342
+ as_of=as_of, status="draft"):
343
+ n += 1
344
+ except Exception:
345
+ return n
346
+ return n
347
+
348
+
349
+ def learn_async(run_id):
350
+ """异步提炼(不阻塞任务收尾,与教训沉淀同款语义)。"""
351
+ def _run():
352
+ try:
353
+ learn_from_run(run_id)
354
+ except Exception:
355
+ pass
356
+ threading.Thread(target=_run, name="kb-learn", daemon=True).start()
357
+
358
+
359
+ def view():
360
+ """知识库总览(给 UI/API):条目 + 标签聚合 + 草稿数 + 账龄标记。"""
361
+ entries = list_entries()
362
+ tags, drafts = {}, 0
363
+ for x in entries:
364
+ for tg in (x.get("tags") or []):
365
+ tags[tg] = tags.get(tg, 0) + 1
366
+ if (x.get("status") or "draft") != "approved":
367
+ drafts += 1
368
+ x["stale"] = _is_stale(x.get("as_of"))
369
+ return {"entries": entries,
370
+ "tags": sorted(tags, key=lambda t: (-tags[t], t)),
371
+ "tag_counts": tags, "drafts": drafts, "total": len(entries)}
@@ -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
@@ -1777,6 +1777,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1777
1777
  tpl = tpl.replace("## 待评审稿件", "%s\n\n## 待评审稿件" % sk, 1)
1778
1778
  if bible:
1779
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)
1780
1783
  return tpl.replace("__DIMKEYS__", dimkey).replace(
1781
1784
  "__MANUSCRIPT__", text or "(稿件为空!)")
1782
1785
 
@@ -1974,6 +1977,9 @@ def _run_serial_review(run, task, agents, ev, stats, mode, critics, impl, route,
1974
1977
  sk_block, _ = skills.block_for(task, stable_order=True)
1975
1978
  if bible:
1976
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
1977
1983
  scope = ("本章 = 大纲第 %d 章" % i) if start == 1 else (
1978
1984
  "本批为第 %d–%d 章,下列按全书章号列出各章要点" % (start, end))
1979
1985
 
@@ -3190,3 +3196,8 @@ def execute_run(run_id):
3190
3196
  skills.learn_async(run_id)
3191
3197
  except Exception:
3192
3198
  pass
3199
+ # 知识库闭环:从产出材料提炼可复用知识条目(草稿态,人工转正后参与注入)
3200
+ try:
3201
+ knowledge.learn_async(run_id)
3202
+ except Exception:
3203
+ pass
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
@@ -7522,6 +7522,126 @@ async function skillLessonOp(id, op) {
7522
7522
  loadSkills();
7523
7523
  }
7524
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
+
7525
7645
  /* ---------------------------------------------------------- 自动化(定时任务) */
7526
7646
  /* 进页拉一次 + 每次操作后重拉 + 页面可见时每 8s 轮询(离页停表)。
7527
7647
  * 后端 /api/automation → {tasks, templates};任务/模板操作走子路径。 */
@@ -8928,7 +9048,7 @@ async function suStartupCheck() {
8928
9048
  }
8929
9049
 
8930
9050
  /* ---------------------------------------------------------- 页签 & 初始化 */
8931
- 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: "关于与更新" };
8932
9052
  const SET_TABS = new Set(Object.keys(TAB_TITLES)); // 设置导航里的子页(__phone 是弹框,不算)
8933
9053
 
8934
9054
  function tabTitle(name) {
@@ -9590,6 +9710,7 @@ function switchTab(name) {
9590
9710
  if (name === "agents") autoCheckUpdates(); // 进目录页自动查各 CLI 新版本
9591
9711
  if (name === "orch") { loadOrchestrator(); loadSettings(); } // 进编排设置页拉取配置
9592
9712
  if (name === "skills") loadSkills(); // 进经验库页拉取沉淀
9713
+ if (name === "knowledge") loadKnowledge(); // 进知识库页拉取条目
9593
9714
  if (name === "automation") { loadAutomation(); startAutoPoll(); } // 进自动化页:拉取 + 页面可见时每 8s 轮询
9594
9715
  else stopAutoPoll(); // 离开自动化页(或切到别的子页)即停表
9595
9716
  if (name === "zentao") loadZentao(); // 进禅道页:拉配置与修复记录回填表单
@@ -10158,6 +10279,7 @@ function cmdkOps() {
10158
10279
  { icon: "i-calendar-days", label: t("自动化"), run: () => switchTab("automation") },
10159
10280
  { icon: "i-blocks", label: t("插件市场"), run: () => switchTab("market") },
10160
10281
  { icon: "i-book", label: t("经验库"), run: () => switchTab("skills") },
10282
+ { icon: "i-sigma", label: t("知识库"), run: () => switchTab("knowledge") },
10161
10283
  { icon: "i-cpu", label: t("智能体管理"), run: () => switchTab("agents") },
10162
10284
  { icon: "i-chart", label: t("用量统计"), run: () => switchTab("usage") },
10163
10285
  { icon: "i-gear", label: t("设置"), run: () => enterSettings() },
package/app/ui/i18n.js CHANGED
@@ -429,6 +429,36 @@
429
429
  "CLI 绑定": "CLI bindings",
430
430
  "编排设置": "Orchestrator",
431
431
  "经验库": "Skill library",
432
+ "知识库": "Knowledge base",
433
+ "新建条目": "New entry",
434
+ "运行产出自动整理成的可复用知识:": "Reusable knowledge distilled from run outputs:",
435
+ "自动提炼(草稿)": "auto-distilled (draft)",
436
+ "——每次任务结束由编排者从产出材料里提炼知识条目,你确认「转正」后才参与注入;": "— after each run the orchestrator distills knowledge entries from output material; they join prompt injection only after you approve them;",
437
+ "手动新建(直接转正)": "manually created (approved)",
438
+ "——也是你自己的知识管理库。转正条目按任务类型与相关性自动注入同类任务的规划与评审提示词。": "— doubles as your personal knowledge base. Approved entries are injected by task type and relevance into planning/review prompts of similar tasks.",
439
+ "搜索标题 / 正文 / 标签": "Search title / body / tags",
440
+ "知识条目": "Knowledge entries",
441
+ "全部标签": "All tags",
442
+ "草稿": "Draft",
443
+ "已转正": "Approved",
444
+ "转正": "Approve",
445
+ "可能过期": "Possibly stale",
446
+ "事实较旧,注入时会标注可能过期": "Fact is old; injection marks it as possibly stale",
447
+ "只看该标签": "Filter by tag",
448
+ "来源运行": "Source run",
449
+ "事实截至 ": "As of ",
450
+ "事实截至": "Fact as of",
451
+ "删除这条知识?转正条目不再注入,删除后不可恢复。": "Delete this entry? Approved entries stop being injected; deletion is permanent.",
452
+ "新建知识条目": "New knowledge entry",
453
+ "编辑知识条目": "Edit knowledge entry",
454
+ "标题": "Title",
455
+ "正文": "Body",
456
+ "标签(逗号分隔)": "Tags (comma-separated)",
457
+ "例:番茄, 签约": "e.g. fanqie, signing",
458
+ "适用范围(任务类型,* 为通用)": "Scope (task type; * = general)",
459
+ "标题与正文不能为空": "Title and body are required",
460
+ "还没有知识条目——完成一次产出型任务后自动提炼(草稿态待转正),也可以点右上角手动新建。": "No entries yet — finish an output-producing task and entries are auto-distilled (drafts awaiting approval), or create one manually.",
461
+ "(暂无)": "(none)",
432
462
  "皮肤": "Skins",
433
463
  "手机连接": "Phone link",
434
464
  "设置": "Settings",
package/app/ui/index.html CHANGED
@@ -185,6 +185,7 @@
185
185
  <button class="set-item" data-sub="models" data-i18n="模型接入"><svg class="ico" aria-hidden="true"><use href="#i-layers"></use></svg>模型接入</button>
186
186
  <button class="set-item" data-sub="bindings" data-i18n="CLI 绑定"><svg class="ico" aria-hidden="true"><use href="#i-link"></use></svg>CLI 绑定</button>
187
187
  <button class="set-item" data-sub="skills" data-i18n="经验库"><svg class="ico" aria-hidden="true"><use href="#i-book"></use></svg>经验库</button>
188
+ <button class="set-item" data-sub="knowledge" data-i18n="知识库"><svg class="ico" aria-hidden="true"><use href="#i-sigma"></use></svg>知识库</button>
188
189
  <button class="set-item" data-sub="market" data-i18n="插件市场"><svg class="ico" aria-hidden="true"><use href="#i-blocks"></use></svg>插件市场</button>
189
190
  <button class="set-item" data-sub="orch" data-i18n="编排设置"><svg class="ico" aria-hidden="true"><use href="#i-workflow"></use></svg>编排设置</button>
190
191
  <div data-i18n="外观" class="set-group">外观</div>
@@ -671,6 +672,22 @@
671
672
  </div>
672
673
  </div>
673
674
 
675
+ <!-- 知识库:运行产出自动整理的知识条目 + 个人知识管理 -->
676
+ <div id="sub-knowledge" class="subpage hidden">
677
+ <div class="panel wide">
678
+ <div class="panel-head"><h2 data-i18n="知识库">知识库</h2>
679
+ <button class="ghost small" onclick="kbFormOpen(null)"><svg class="ico" aria-hidden="true"><use href="#i-plus"></use></svg><span data-i18n="新建条目">新建条目</span></button></div>
680
+ <p class="hint"><span data-i18n="运行产出自动整理成的可复用知识:">运行产出自动整理成的可复用知识:</span><b data-i18n="自动提炼(草稿)">自动提炼(草稿)</b><span data-i18n="——每次任务结束由编排者从产出材料里提炼知识条目,你确认「转正」后才参与注入;">——每次任务结束由编排者从产出材料里提炼知识条目,你确认「转正」后才参与注入;</span><b data-i18n="手动新建(直接转正)">手动新建(直接转正)</b><span data-i18n="——也是你自己的知识管理库。转正条目按任务类型与相关性自动注入同类任务的规划与评审提示词。">——也是你自己的知识管理库。转正条目按任务类型与相关性自动注入同类任务的规划与评审提示词。</span></p>
681
+ <div class="kb-toolbar">
682
+ <input id="kb-search" data-i18n-ph="搜索标题 / 正文 / 标签" placeholder="搜索标题 / 正文 / 标签" oninput="kbSearch(this.value)">
683
+ <div class="cat-chips" id="kb-status-chips"></div>
684
+ </div>
685
+ <div class="cat-chips" id="kb-tag-chips"></div>
686
+ <h3 class="sec-title"><span data-i18n="知识条目">知识条目</span> <span class="tag" id="kb-count"></span></h3>
687
+ <div id="kb-cards" class="cards"></div>
688
+ </div>
689
+ </div>
690
+
674
691
  <!-- 插件市场:经验包一键安装 + 外部生态目录 -->
675
692
  <div id="sub-market" class="subpage hidden">
676
693
  <div class="panel wide">
package/app/ui/style.css CHANGED
@@ -5232,3 +5232,7 @@ body.welcome-open { overflow: hidden; }
5232
5232
  .hhelp:hover { color: var(--accent); border-color: var(--accent); }
5233
5233
  .hhelp .ico { width: 12px; height: 12px; }
5234
5234
  .empty a { color: var(--accent); }
5235
+
5236
+ /* —— 知识库:工具行(搜索框 + 状态过滤 chips 同排)—— */
5237
+ .kb-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin: 10px 0 2px; }
5238
+ .kb-toolbar input { flex: 1 1 220px; min-width: 180px; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codebee",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "CodeBee · 多智能体编排台 — 统一编排本机 AI 编码 CLI(Codex / Claude Code / QwenCode / OpenCode / Aider…):目标→拆解→路由→执行→验证→跨厂商评审→修复→报告。纯 Python 标准库实现,本地 Web UI。",
5
5
  "license": "MIT",
6
6
  "bin": {