codebee 0.1.17 → 0.1.19

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/app/main.py CHANGED
@@ -24,6 +24,7 @@ from core import automation, catalog, flows, jobs, manager, market, market_remot
24
24
  from core import paths
25
25
  from core import health
26
26
  import pick_dialog
27
+ import pet
27
28
 
28
29
  log = logging.getLogger(__name__)
29
30
 
@@ -233,6 +234,9 @@ class Handler(BaseHTTPRequestHandler):
233
234
  if path == "/api/skills":
234
235
  from core import skills
235
236
  return self._json(200, skills.view())
237
+ if path == "/api/knowledge":
238
+ from core import knowledge
239
+ return self._json(200, knowledge.view())
236
240
  if path == "/api/settings":
237
241
  return self._json(200, dict(settings.load(), **jobs.workers_info()))
238
242
  if path == "/api/settings-v2":
@@ -261,6 +265,8 @@ class Handler(BaseHTTPRequestHandler):
261
265
  return self._json(200, {"ok": True, "catalog": manager.catalog_view()})
262
266
  if path == "/api/runs":
263
267
  return self._json(200, {"runs": store.list_runs()})
268
+ if path == "/api/pet_state":
269
+ return self._api_pet_state()
264
270
  if path == "/api/usage":
265
271
  from core import usage
266
272
  q = parse_qs(urlparse(self.path).query)
@@ -462,12 +468,27 @@ class Handler(BaseHTTPRequestHandler):
462
468
  if path == "/api/zentao":
463
469
  from core import zentao
464
470
  return self._json(200, zentao.view())
471
+ if path == "/api/wxdigest":
472
+ from core import wxdigest
473
+ try:
474
+ return self._json(200, wxdigest.view())
475
+ except Exception:
476
+ log.exception("wxdigest: view 失败")
477
+ return self._json(500, {"error": "群摘要视图生成失败"})
465
478
  m = re.match(r"^/api/automation/([^/]+)$", path)
466
479
  if m:
467
480
  t = automation.get_task(m.group(1))
468
481
  return self._json(200, {"task": t}) if t else self._json(404, {"error": "not found"})
469
482
  if path == "/api/market":
470
483
  return self._json(200, market.view())
484
+ if path == "/api/cleanup":
485
+ # 数据与备份页:清理配置 + 上次清理状态 + 当前可清理预估
486
+ from core import cleanup
487
+ try:
488
+ return self._json(200, cleanup.status())
489
+ except Exception:
490
+ log.exception("cleanup: status 失败")
491
+ return self._json(500, {"error": "清理状态生成失败"})
471
492
  if path == "/api/market/remote":
472
493
  q = parse_qs(urlparse(self.path).query)
473
494
  return self._json(200, market_remote.view(
@@ -622,6 +643,9 @@ class Handler(BaseHTTPRequestHandler):
622
643
  m = re.match(r"^/api/publish/task/([^/]+)/(create-book|chapter|auto-publish)$", path)
623
644
  if m:
624
645
  return self._api_publish_task_op(m.group(1), m.group(2))
646
+ m = re.match(r"^/api/publish/task/([^/]+)/register-book$", path)
647
+ if m:
648
+ return self._api_publish_register_book(m.group(1))
625
649
  m = re.match(r"^/api/publish/task/([^/]+)/publish-all$", path)
626
650
  if m:
627
651
  return self._api_publish_auto(m.group(1))
@@ -835,6 +859,16 @@ class Handler(BaseHTTPRequestHandler):
835
859
  from core import skills
836
860
  n = skills.learn_from_run((self._body().get("run_id") or ""))
837
861
  return self._json(200, {"ok": True, "learned": n})
862
+ if path == "/api/knowledge/op":
863
+ from core import knowledge
864
+ body = self._body()
865
+ err = knowledge.entry_op(body.get("id") or "", body.get("op") or "",
866
+ fields=body.get("fields"))
867
+ return self._json(400, {"error": err}) if err else self._json(200, {"ok": True})
868
+ if path == "/api/knowledge/learn":
869
+ from core import knowledge
870
+ n = knowledge.learn_from_run((self._body().get("run_id") or ""))
871
+ return self._json(200, {"ok": True, "learned": n})
838
872
  m = re.match(r"^/api/flows/([^/]+)/reset$", path)
839
873
  if m:
840
874
  err = flows.reset_flow(m.group(1))
@@ -991,6 +1025,89 @@ class Handler(BaseHTTPRequestHandler):
991
1025
  except ValueError as e:
992
1026
  return self._json(400, {"error": str(e)})
993
1027
  return self._json(200, {"ok": True, "config": cfg})
1028
+ if path == "/api/wxdigest/config":
1029
+ from core import wxdigest
1030
+ try:
1031
+ cfg = wxdigest.save_config(self._body())
1032
+ except ValueError as e:
1033
+ return self._json(400, {"error": str(e)})
1034
+ return self._json(200, {"ok": True, "config": cfg})
1035
+ if path == "/api/wxdigest/scan":
1036
+ from core import wxdigest
1037
+ res = wxdigest.scan_now() # 含模型调用,同步做(ThreadingHTTPServer 不堵别的请求)
1038
+ return self._json(200, dict(res, ok=bool(res.get("ok"))))
1039
+ if path == "/api/wxdigest/seen":
1040
+ from core import wxdigest
1041
+ return self._json(200, {"ok": True, "view": wxdigest.seen_clear()})
1042
+ if path == "/api/wxdigest/groups":
1043
+ from core import wxdigest
1044
+ try:
1045
+ groups = wxdigest.refresh_groups() # sidecar 调用,秒级到几十秒(ThreadingHTTPServer 不堵)
1046
+ except Exception as e:
1047
+ return self._json(500, {"error": str(e)})
1048
+ return self._json(200, {"ok": True, "groups": groups})
1049
+ if path == "/api/wxdigest/to-knowledge":
1050
+ from core import wxdigest
1051
+ body = self._body()
1052
+ try:
1053
+ entry = wxdigest.digest_to_knowledge(str(body.get("id") or ""))
1054
+ except Exception as e:
1055
+ return self._json(400, {"error": str(e)})
1056
+ return self._json(200, {"ok": True, "entry": entry})
1057
+ if path == "/api/cleanup":
1058
+ # op=run 立即清理(include_profiles=true 连浏览器缓存一起清,
1059
+ # 平台登录态会丢需重新扫码);否则只重扫预估不落盘
1060
+ from core import cleanup
1061
+ body = self._body()
1062
+ try:
1063
+ if body.get("op") == "run":
1064
+ r = cleanup.run_cleanup(
1065
+ include_profiles=bool(body.get("include_profiles")))
1066
+ return self._json(200, {"ok": True, "result": r})
1067
+ return self._json(200, {"ok": True,
1068
+ "plan": cleanup.plan(
1069
+ include_profiles=bool(
1070
+ body.get("include_profiles")))})
1071
+ except Exception as e:
1072
+ log.exception("cleanup: 执行失败")
1073
+ return self._json(500, {"error": "清理执行失败:%s" % e})
1074
+ if path == "/api/data/export":
1075
+ # 整机备份导出:数据目录 + 任务工作目录打包 zip(含密钥,提示用户妥善保管)
1076
+ from core import backup
1077
+ body = self._body()
1078
+ try:
1079
+ info = backup.export_data(
1080
+ target_dir=str(body.get("target_dir") or ""),
1081
+ include_logs=body.get("include_logs", True),
1082
+ include_workspace=body.get("include_workspace", True))
1083
+ except ValueError as e:
1084
+ return self._json(400, {"error": str(e)})
1085
+ except Exception as e:
1086
+ log.exception("backup: 导出失败")
1087
+ return self._json(500, {"error": "导出失败:%s" % e})
1088
+ return self._json(200, {"ok": True, **info})
1089
+ if path == "/api/data/import":
1090
+ # op=inspect 预览(不落盘);op=apply 应用(merge/replace + 路径重映射)
1091
+ from core import backup
1092
+ body = self._body()
1093
+ try:
1094
+ if body.get("op") == "apply":
1095
+ r = backup.apply_import(
1096
+ str(body.get("path") or ""),
1097
+ mode=str(body.get("mode") or "merge"),
1098
+ remap=body.get("remap", True),
1099
+ include_workspace=body.get("include_workspace", True))
1100
+ return self._json(200, {"ok": True, **r})
1101
+ return self._json(200, {"ok": True,
1102
+ "preview": backup.inspect_backup(
1103
+ str(body.get("path") or ""))})
1104
+ except ValueError as e:
1105
+ return self._json(400, {"error": str(e)})
1106
+ except Exception as e:
1107
+ log.exception("backup: 导入失败")
1108
+ return self._json(500, {"error": "导入失败:%s" % e})
1109
+ if path == "/api/pick_file":
1110
+ return self._api_pick_file()
994
1111
  if path == "/api/zentao/test":
995
1112
  from core import zentao
996
1113
  body = self._body()
@@ -998,7 +1115,14 @@ class Handler(BaseHTTPRequestHandler):
998
1115
  ok, msg = zentao.test_connection(
999
1116
  base_url=body.get("base_url"), account=body.get("account"),
1000
1117
  password=pw or None) # 空 = 用已保存密码
1001
- return self._json(200, {"ok": ok, "message": msg})
1118
+ res = {"ok": ok, "message": msg}
1119
+ if ok:
1120
+ # 子路径部署被自动探测补全时回传有效地基,前端回填表单提醒保存
1121
+ eff = zentao.resolved_base_url(body.get("base_url")) or \
1122
+ zentao.resolved_base_url(zentao.view()["config"].get("base_url"))
1123
+ if eff:
1124
+ res["base_url"] = eff
1125
+ return self._json(200, res)
1002
1126
  if path == "/api/zentao/scan":
1003
1127
  from core import zentao
1004
1128
  res = zentao.scan_now() # 网络操作同步做(ThreadingHTTPServer 不堵别的请求)
@@ -1008,6 +1132,12 @@ class Handler(BaseHTTPRequestHandler):
1008
1132
  pid = (self._body() or {}).get("product")
1009
1133
  res = zentao.fetch_modules(pid)
1010
1134
  return self._json(200, dict(res, ok=bool(res.get("ok"))))
1135
+ if path == "/api/zentao/products":
1136
+ from core import zentao
1137
+ return self._json(200, zentao.fetch_products())
1138
+ if path == "/api/zentao/users":
1139
+ from core import zentao
1140
+ return self._json(200, zentao.fetch_users())
1011
1141
  # 外部目录路由必须在通配的 market/<id>/(install|remove) 之前——
1012
1142
  # 否则 "remote" 会被当成包名吞掉
1013
1143
  if path == "/api/market/remote/refresh":
@@ -1175,6 +1305,20 @@ class Handler(BaseHTTPRequestHandler):
1175
1305
  return self._json(400, {"error": err or "操作失败"})
1176
1306
  return self._json(200, {"ok": True, "started": True})
1177
1307
 
1308
+ def _api_publish_register_book(self, task_id):
1309
+ """人工登记平台已有作品(建书流程没走通/手工建书后的补账口)。"""
1310
+ from core import store
1311
+ from core.publish import manager as pub
1312
+ if not store.get_task(task_id):
1313
+ return self._json(404, {"error": "任务不存在"})
1314
+ body = self._body() or {}
1315
+ ok, err = pub.register_book(task_id, (body.get("platform") or "").strip(),
1316
+ str(body.get("title") or ""),
1317
+ book_id=str(body.get("book_id") or ""))
1318
+ if not ok:
1319
+ return self._json(400, {"error": err or "登记失败"})
1320
+ return self._json(200, {"ok": True})
1321
+
1178
1322
  def _api_publish_auto(self, task_id):
1179
1323
  """批量发布全部待发章节(publish/auto.py)。
1180
1324
 
@@ -1273,6 +1417,54 @@ class Handler(BaseHTTPRequestHandler):
1273
1417
  parent = "" if p.parent == p else str(p.parent)
1274
1418
  return self._json(200, {"path": str(p), "parent": parent, "dirs": dirs})
1275
1419
 
1420
+ def _api_pet_state(self):
1421
+ """桌面蜜蜂(app/pet.py 子进程)的喂食端点:任务近况最小集 + 相关设置。
1422
+
1423
+ 数据与 /api/runs 同源(latest_run_by_task 全量扫描,内存操作),但只裁
1424
+ 出桌宠需要的字段;每任务一条(封顶 200),蜜蜂在进程内自行差分出
1425
+ 「刚完工/刚出事」。设置随响应回传,蜜蜂轮询到 pet_enabled=false 自行
1426
+ 退出——关闭桌宠不需要专门的信号通道。
1427
+ """
1428
+ st = settings.load()
1429
+ rows = []
1430
+ n_run = n_q = 0
1431
+ latest = store.latest_run_by_task()
1432
+ for tid in sorted(latest.keys(), reverse=True)[:200]:
1433
+ run = latest[tid]
1434
+ task = store.get_task(tid) or {}
1435
+ rs = str(run.get("status") or "")
1436
+ if rs == "running":
1437
+ n_run += 1
1438
+ elif rs == "queued":
1439
+ n_q += 1
1440
+ steps = run.get("steps") or []
1441
+ cur = next((s for s in steps if s.get("status") == "running"), None)
1442
+ rows.append({
1443
+ "id": tid,
1444
+ "title": str(task.get("title") or tid),
1445
+ "type": str(task.get("type") or ""),
1446
+ "run_status": rs,
1447
+ "steps_done": sum(1 for s in steps if s.get("status") == "done"),
1448
+ "steps_total": len(steps),
1449
+ "step_current": str((cur.get("summary") or cur.get("agent_label")
1450
+ or "") if cur else ""),
1451
+ "error": str(run.get("error") or "")[:160],
1452
+ })
1453
+ try:
1454
+ from core import wxdigest as _wxd # 群摘要喂食:未读数+最新一条(失败兜底零值)
1455
+ dg = _wxd.pet_digest()
1456
+ except Exception:
1457
+ dg = {"unseen": 0, "latest": None}
1458
+ return self._json(200, {
1459
+ "ok": True, "ts": int(time.time()), "port": PORT,
1460
+ "settings": {"pet_enabled": bool(st.get("pet_enabled", True)),
1461
+ "pet_mode": str(st.get("pet_mode") or "always"),
1462
+ "pet_skin": str(st.get("pet_skin") or "plush")},
1463
+ "workers": {"running": n_run, "queued": n_q},
1464
+ "tasks": rows,
1465
+ "digest": dg,
1466
+ })
1467
+
1276
1468
  def _api_pick_folder(self):
1277
1469
  """系统原生「选择文件夹」对话框(工作目录「选择…」/点输入框用)。仅限本机:
1278
1470
  对话框弹在服务所在机器上,远端触发等于替别人开窗。pick_dialog.ask_directory
@@ -1293,6 +1485,24 @@ class Handler(BaseHTTPRequestHandler):
1293
1485
  return self._json(200, {"path": "", "fallback": fb, "error": err})
1294
1486
  return self._json(200, {"path": path or ""})
1295
1487
 
1488
+ def _api_pick_file(self):
1489
+ """系统原生「选择文件」对话框(导入备份包用)。仅限本机,同 _api_pick_folder;
1490
+ 没有 tkinter 时 fallback=true(本场景没有网页回落,前端提示手输路径)。"""
1491
+ ip, fw = self._forwarded_ip()
1492
+ if ip not in ("127.0.0.1", "::1") or fw:
1493
+ return self._json(403, {"error": "文件选择仅限本机使用,请手动输入路径"})
1494
+ body = self._body()
1495
+ if not _PICK_LOCK.acquire(blocking=False):
1496
+ return self._json(200, {"path": "", "busy": True})
1497
+ try:
1498
+ path, err, fb = pick_dialog.ask_file(str(body.get("initial") or ""),
1499
+ str(body.get("title") or "选择文件"))
1500
+ finally:
1501
+ _PICK_LOCK.release()
1502
+ if err:
1503
+ return self._json(200, {"path": "", "fallback": fb, "error": err})
1504
+ return self._json(200, {"path": path or ""})
1505
+
1296
1506
  def _api_dir_scan(self):
1297
1507
  """侧栏文件夹「查看文件」:递归列出该工作目录下的全部文件(左侧文件页树形展示)。
1298
1508
 
@@ -1970,6 +2180,56 @@ class ThreadedServer(ThreadingHTTPServer):
1970
2180
  super().server_bind()
1971
2181
 
1972
2182
 
2183
+ # ---------------------------------------------------------------- 桌面蜜蜂
2184
+
2185
+ _PET_PROC = None # 蜜蜂子进程句柄(看护线程专用,别的线程别碰)
2186
+
2187
+
2188
+ def _spawn_pet(port):
2189
+ """拉起桌面蜜蜂子进程(app/pet.py:tkinter 自绘,死活不影响服务)。"""
2190
+ global _PET_PROC
2191
+ pet_py = Path(__file__).resolve().parent / "pet.py"
2192
+ if not pet_py.exists():
2193
+ return
2194
+ env = dict(os.environ, PYTHONIOENCODING="utf-8")
2195
+ # CREATE_NO_WINDOW 同 pick_dialog:控制台宿主下不给蜜蜂弹黑框
2196
+ kw = {"creationflags": 0x08000000} if os.name == "nt" else {}
2197
+ try:
2198
+ _PET_PROC = subprocess.Popen(
2199
+ [sys.executable, str(pet_py), "--port", str(port),
2200
+ "--data-dir", str(paths.DATA_DIR)], env=env, **kw)
2201
+ print("[CodeBee] 桌面蜜蜂已起飞(右键蜜蜂可设置;设置页可关闭)", flush=True)
2202
+ except Exception as e:
2203
+ print("[CodeBee] 桌面蜜蜂未能起飞(不影响其他功能):%s" % e, flush=True)
2204
+
2205
+
2206
+ def _start_pet_keeper(port):
2207
+ """蜜蜂看护线程:按设置自愈拉起(崩溃/被杀 5s 内补位)。
2208
+
2209
+ TUTTI_PET_DISABLED=1 完全静默(测试/无桌面环境);关闭走设置
2210
+ pet_enabled=false——蜜蜂轮询到后自离,看护线程也不再拉起,不存在
2211
+ 「关了又复活」。子进程与服务的生命周期解耦:服务被杀蜜蜂 1 分钟内
2212
+ 自灭(MAX_MISS),不留孤儿窗口。
2213
+ """
2214
+ if os.environ.get("TUTTI_PET_DISABLED") == "1":
2215
+ return
2216
+
2217
+ def _loop():
2218
+ while True:
2219
+ try:
2220
+ if settings.load().get("pet_enabled", True):
2221
+ if _PET_PROC is None or _PET_PROC.poll() is not None:
2222
+ # 全机唯一蜜蜂:别的服务实例(不同数据目录)已养蜂时
2223
+ # 不再拉起,否则桌面会出现多只(用户实测踩坑)
2224
+ if not pet.global_lock_held():
2225
+ _spawn_pet(port)
2226
+ except Exception:
2227
+ pass
2228
+ time.sleep(5)
2229
+
2230
+ threading.Thread(target=_loop, name="pet-keeper", daemon=True).start()
2231
+
2232
+
1973
2233
  def main():
1974
2234
  parser = argparse.ArgumentParser(description="CodeBee 多智能体编排台")
1975
2235
  parser.add_argument("--host", default="0.0.0.0",
@@ -2034,6 +2294,13 @@ def main():
2034
2294
  n_lc = skills.migrate_lesson_categories() # 分类字段上线前的教训按关键词回填(幂等,带备份)
2035
2295
  if n_lc:
2036
2296
  print("[CodeBee] 经验库:%d 条历史教训已自动归类" % n_lc)
2297
+ try:
2298
+ from core import knowledge
2299
+ n_kd = knowledge.migrate_drafts_approved() # 草稿闸门退役:历史草稿一次性转正(幂等)
2300
+ if n_kd:
2301
+ print("[CodeBee] 知识库:%d 条历史草稿已转正(默认直接参与注入)" % n_kd)
2302
+ except Exception:
2303
+ pass
2037
2304
  from core import usage
2038
2305
  _step("正在回填用量台账…")
2039
2306
  n_bf = usage.backfill_from_runs() # 历史运行 token 回填台账(幂等,仅补缺失步骤)
@@ -2107,6 +2374,13 @@ def main():
2107
2374
  print("[CodeBee] 禅道集成:%d 条认领记录已加载" % n_zt)
2108
2375
  except Exception:
2109
2376
  pass
2377
+ try:
2378
+ from core import wxdigest
2379
+ n_wx = wxdigest.start() # 群摘要:加载监控配置与游标(调度挂在 automation tick)
2380
+ if n_wx:
2381
+ print("[CodeBee] 群摘要:%d 个群游标已加载" % n_wx)
2382
+ except Exception:
2383
+ pass
2110
2384
  tok = remote.token()
2111
2385
  import atexit
2112
2386
  import os as _os
@@ -2147,6 +2421,7 @@ def main():
2147
2421
  faulthandler.cancel_dump_traceback_later() # 启动完成,看门狗退役(否则运行期每 20s 误报堆栈)
2148
2422
  except Exception:
2149
2423
  pass
2424
+ _start_pet_keeper(args.port) # 桌面蜜蜂:端口就绪后放出(设置可关;测试可禁)
2150
2425
  if remote.PUBLIC_URL:
2151
2426
  print("[CodeBee] 公网 %s/?token=%s ← 任何网络可访问(反代回源已强制校验令牌)"
2152
2427
  % (remote.PUBLIC_URL, tok))