codebee 0.1.68 → 0.1.70

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.
Files changed (133) hide show
  1. package/CHANGELOG.md +580 -543
  2. package/LICENSE +21 -21
  3. package/README.md +11 -0
  4. package/app/browser-final-1440.png +0 -0
  5. package/app/browser-final-390.png +0 -0
  6. package/app/browser-google-fallback.png +0 -0
  7. package/app/browser-google-test.png +0 -0
  8. package/app/browser-workbench-1440.png +0 -0
  9. package/app/browser-workbench-390.png +0 -0
  10. package/app/browser-workspace-google.png +0 -0
  11. package/app/browser-workspace-smoke.png +0 -0
  12. package/app/browser-workspace.png +0 -0
  13. package/app/core/acceptance.py +100 -100
  14. package/app/core/aiflavor.py +197 -197
  15. package/app/core/artifacts.py +148 -148
  16. package/app/core/assets.py +110 -110
  17. package/app/core/attachments.py +623 -623
  18. package/app/core/audit.py +30 -0
  19. package/app/core/automation.py +853 -758
  20. package/app/core/backup.py +470 -470
  21. package/app/core/beekeeper.py +183 -183
  22. package/app/core/benchstore.py +95 -0
  23. package/app/core/board.py +209 -209
  24. package/app/core/bookmeta.py +935 -930
  25. package/app/core/bookmeta_catalog.py +180 -180
  26. package/app/core/branching.py +137 -137
  27. package/app/core/builtin_agent.py +1673 -1234
  28. package/app/core/capability.py +144 -144
  29. package/app/core/catalog.py +7 -1
  30. package/app/core/chaptersafe.py +32 -32
  31. package/app/core/cleanup.py +303 -303
  32. package/app/core/compaction.py +201 -201
  33. package/app/core/covergen.py +356 -356
  34. package/app/core/defectretro.py +43 -43
  35. package/app/core/diagnostics.py +114 -114
  36. package/app/core/dispatch.py +190 -173
  37. package/app/core/dispatch_log.py +128 -128
  38. package/app/core/env_scrub.py +85 -85
  39. package/app/core/error_codes.py +33 -24
  40. package/app/core/errorlog.py +137 -137
  41. package/app/core/evalbench.py +720 -0
  42. package/app/core/evaluation.py +174 -174
  43. package/app/core/failure_notify.py +290 -290
  44. package/app/core/flows.py +228 -3
  45. package/app/core/goal_service.py +167 -167
  46. package/app/core/health.py +374 -374
  47. package/app/core/history.py +38 -38
  48. package/app/core/hooks.py +162 -162
  49. package/app/core/jobs.py +51 -30
  50. package/app/core/manager.py +2143 -2122
  51. package/app/core/market.py +362 -362
  52. package/app/core/market_remote.py +10 -0
  53. package/app/core/mcp_client.py +287 -0
  54. package/app/core/mocks.py +64 -64
  55. package/app/core/modelhub.py +193 -10
  56. package/app/core/notify.py +201 -106
  57. package/app/core/novel_quality.py +189 -0
  58. package/app/core/operations.py +228 -228
  59. package/app/core/paihang.py +138 -138
  60. package/app/core/paths.py +6 -2
  61. package/app/core/pipeline.py +426 -28
  62. package/app/core/planner.py +243 -22
  63. package/app/core/plugins.py +525 -0
  64. package/app/core/portguard.py +111 -111
  65. package/app/core/preview.py +293 -293
  66. package/app/core/publish/__init__.py +7 -7
  67. package/app/core/publish/auto.py +367 -367
  68. package/app/core/publish/browser.py +655 -539
  69. package/app/core/publish/fanqie.py +165 -159
  70. package/app/core/publish/flow.py +589 -440
  71. package/app/core/publish/flows-fanqie-calibrated.json +336 -332
  72. package/app/core/publish/ledger.py +229 -203
  73. package/app/core/publish/manager.py +824 -692
  74. package/app/core/publish/qimao.py +146 -141
  75. package/app/core/publish/ws.py +139 -139
  76. package/app/core/redact.py +38 -38
  77. package/app/core/remotesync.py +121 -0
  78. package/app/core/repeat_guard.py +129 -129
  79. package/app/core/router.py +398 -398
  80. package/app/core/runner.py +2142 -2067
  81. package/app/core/selfupdate.py +384 -384
  82. package/app/core/session_log.py +161 -161
  83. package/app/core/sessions.py +312 -312
  84. package/app/core/settings.py +186 -149
  85. package/app/core/settings_schema.py +270 -262
  86. package/app/core/share_page.py +114 -114
  87. package/app/core/skill_scan.py +122 -122
  88. package/app/core/skillpacks/fanqie-novel.md +80 -80
  89. package/app/core/skillpacks/market/character-bible.md +66 -66
  90. package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
  91. package/app/core/skillpacks/market/git-workflow.md +57 -57
  92. package/app/core/skillpacks/market/release-notes.md +72 -72
  93. package/app/core/skillpacks/market/weekly-report.md +71 -71
  94. package/app/core/skillpacks/market/worldview-consistency.md +70 -70
  95. package/app/core/step_runner.py +132 -132
  96. package/app/core/store.py +104 -82
  97. package/app/core/task_compile.py +199 -199
  98. package/app/core/telemetry.py +292 -292
  99. package/app/core/tlsctx.py +64 -64
  100. package/app/core/token_meter.py +147 -147
  101. package/app/core/tracing.py +144 -144
  102. package/app/core/upstream.py +41 -41
  103. package/app/core/usage.py +1035 -862
  104. package/app/core/volumes.py +389 -389
  105. package/app/core/wxdigest.py +858 -858
  106. package/app/core/zentao.py +2614 -2465
  107. package/app/current-browser.png +0 -0
  108. package/app/main.py +3245 -2981
  109. package/app/mcp_server.py +175 -0
  110. package/app/pet.py +82 -21
  111. package/app/pick_dialog.py +110 -110
  112. package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
  113. package/app/plugins/code-review-helper/skills/review.md +16 -0
  114. package/app/ui/app.js +15706 -14399
  115. package/app/ui/artdiff.js +74 -74
  116. package/app/ui/board.html +162 -162
  117. package/app/ui/board.js +305 -305
  118. package/app/ui/i18n.js +3012 -2770
  119. package/app/ui/icons/bee.svg +79 -79
  120. package/app/ui/index.html +277 -13
  121. package/app/ui/manifest.json +16 -16
  122. package/app/ui/qrcode.js +2297 -2297
  123. package/app/ui/style.css +802 -16
  124. package/bin/tutti.js +147 -147
  125. package/package.json +47 -46
  126. package/tools/wx_reader.py +261 -261
  127. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
  128. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
  129. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  130. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
  131. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  132. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  133. package/app/.playwright-cli/run-detail-after.png +0 -0
@@ -0,0 +1,175 @@
1
+ # -*- coding: utf-8 -*-
2
+ """CodeBee 的 MCP 服务器形态:其他 AI 客户端(Claude Code / ZCode / Cursor)
3
+ 把这里当工具服务器挂载,即可直接创建任务、查状态、看用量与评测榜单——
4
+ CodeBee 从编排工具变成可以被别的智能体调度的生态节点。
5
+
6
+ stdio JSON-RPC 2.0(按行分帧,与 core/mcp_client 同一传输约定)。启动:
7
+ python -X utf8 <CodeBee目录>/app/mcp_server.py
8
+ 客户端配置示例(Claude Code / 兼容客户端):
9
+ {"mcpServers": {"codebee": {"command": "python",
10
+ "args": ["-X", "utf8", "<CodeBee目录>/app/mcp_server.py"]}}}
11
+ 仅本机 stdio,无网络监听,凭据不经过此通道;create_task 会真实调度智能体
12
+ 执行(与界面上建任务同一条链)。
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import sys
18
+
19
+ PROTOCOL_VERSION = "2024-11-05"
20
+ SERVER_INFO = {"name": "codebee", "version": "1.0"}
21
+
22
+ TOOLS = [
23
+ {"name": "create_task", "description": "创建并启动一个 CodeBee 编排任务(真实调度智能体执行,与界面建任务同链)。返回 task_id 与 run_id。",
24
+ "inputSchema": {"type": "object", "properties": {
25
+ "goal": {"type": "string", "description": "任务目标(一句话说清要做什么)"},
26
+ "type": {"type": "string", "description": "任务类型 id(如 code/novel/doc/direct),默认 doc"},
27
+ "workdir": {"type": "string", "description": "工作目录绝对路径;留空用 CodeBee 默认保存路径"},
28
+ "title": {"type": "string", "description": "任务标题;留空取目标前 30 字"}},
29
+ "required": ["goal"]}},
30
+ {"name": "get_status", "description": "查任务与最近一次运行的状态:进度、当前步骤、评分、错误。",
31
+ "inputSchema": {"type": "object", "properties": {
32
+ "task_id": {"type": "string", "description": "任务 ID"}}, "required": ["task_id"]}},
33
+ {"name": "list_recent", "description": "列出最近的运行(id/标题/状态/时间)。",
34
+ "inputSchema": {"type": "object", "properties": {
35
+ "limit": {"type": "integer", "description": "条数,默认 10,最大 30"}}}},
36
+ {"name": "usage_summary", "description": "用量台账汇总:今日/本月花费(¥)与 token 总量。",
37
+ "inputSchema": {"type": "object", "properties": {}}},
38
+ {"name": "bench_leaderboard", "description": "模型评测基准台能力榜(综合分 Top N)。",
39
+ "inputSchema": {"type": "object", "properties": {
40
+ "top": {"type": "integer", "description": "取前 N,默认 5"}}}},
41
+ ]
42
+
43
+
44
+ def _enqueue(job):
45
+ """thin wrapper:便于单测 mock(真链会拉起 worker 执行)。"""
46
+ from core import jobs
47
+ jobs.enqueue(job)
48
+
49
+
50
+ def _tool_create_task(args):
51
+ from core import settings as settings_mod, store
52
+ goal = str(args.get("goal") or "").strip()
53
+ if not goal:
54
+ return {"isError": True, "content": "goal 不能为空"}
55
+ payload = {"type": str(args.get("type") or "doc").strip()[:40] or "doc",
56
+ "title": (str(args.get("title") or "").strip() or goal[:30])[:40],
57
+ "goal": goal[:4000],
58
+ "workdir": str(args.get("workdir") or "").strip()
59
+ or settings_mod.default_workdir()}
60
+ task = store.create_task(payload)
61
+ run = store.create_run("orchestration", task["title"], task_id=task["id"])
62
+ store.update_task_status(task["id"], "queued")
63
+ _enqueue({"kind": "orchestration", "run_id": run["id"], "task_id": task["id"]})
64
+ return {"content": "已创建并排队:task_id=%s run_id=%s(可用 get_status 查进度)"
65
+ % (task["id"], run["id"])}
66
+
67
+
68
+ def _tool_get_status(args):
69
+ from core import store
70
+ task = store.get_task(str(args.get("task_id") or "").strip())
71
+ if not task:
72
+ return {"isError": True, "content": "任务不存在"}
73
+ lines = ["任务:%s(%s)" % (task.get("title"), task.get("status"))]
74
+ run = store.latest_run_by_task().get(task["id"])
75
+ if run:
76
+ lines.append("最近运行:%s(%s)" % (run.get("id"), run.get("status")))
77
+ steps = [s for s in (run.get("steps") or []) if isinstance(s, dict)]
78
+ if steps:
79
+ cur = steps[-1]
80
+ lines.append("步骤 %d/%d:%s(%s)" % (len(steps), run.get("total_steps") or len(steps),
81
+ cur.get("title"), cur.get("status")))
82
+ v = run.get("verdict") or {}
83
+ if v.get("overall") is not None:
84
+ lines.append("评分 %.1f(%s)" % (float(v["overall"]),
85
+ "达标" if v.get("publishable") else "未达标"))
86
+ if run.get("error"):
87
+ lines.append("错误:%s" % str(run["error"])[:200])
88
+ if task.get("git_state") == "isolated":
89
+ lines.append("⚠ 任务分支待裁决(合并/丢弃)")
90
+ return {"content": "\n".join(lines)}
91
+
92
+
93
+ def _tool_list_recent(args):
94
+ from core import store
95
+ try:
96
+ limit = max(1, min(30, int(args.get("limit") or 10)))
97
+ except (TypeError, ValueError):
98
+ limit = 10
99
+ rows = [{"id": r.get("id"), "title": r.get("title"), "status": r.get("status"),
100
+ "ts": r.get("started_at") or ""} for r in store.list_runs(limit=limit)]
101
+ return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
102
+
103
+
104
+ def _tool_usage_summary(args):
105
+ from core import usage
106
+ today, month = usage.cost_snapshot()
107
+ tokens = sum(int(r.get("total") or 0) for r in usage._iter_records(31))
108
+ return {"content": "今日花费 ¥%.2f\n本月花费 ¥%.2f\n近 31 天 tokens:%d(未标定单价的调用不计钱)"
109
+ % (today, month, tokens)}
110
+
111
+
112
+ def _tool_bench(args):
113
+ from core import evalbench
114
+ try:
115
+ top = max(1, min(10, int(args.get("top") or 5)))
116
+ except (TypeError, ValueError):
117
+ top = 5
118
+ board = evalbench.state().get("leaderboard") or []
119
+ rows = [{"rank": r["rank"], "model": r["model"], "overall": r["overall"],
120
+ "provider": r.get("provider_name")} for r in board[:top]]
121
+ return {"content": json.dumps(rows, ensure_ascii=False, indent=1)}
122
+
123
+
124
+ _TOOL_IMPL = {"create_task": _tool_create_task, "get_status": _tool_get_status,
125
+ "list_recent": _tool_list_recent, "usage_summary": _tool_usage_summary,
126
+ "bench_leaderboard": _tool_bench}
127
+
128
+
129
+ def handle_request(msg):
130
+ """处理一条客户端请求,返回响应 dict(通知返回 None)。"""
131
+ if not isinstance(msg, dict) or msg.get("method") is None:
132
+ return None
133
+ rid = msg.get("id")
134
+ method = str(msg["method"])
135
+ if method == "initialize":
136
+ return {"jsonrpc": "2.0", "id": rid, "result": {
137
+ "protocolVersion": PROTOCOL_VERSION, "capabilities": {"tools": {}},
138
+ "serverInfo": SERVER_INFO}}
139
+ if method.startswith("notifications/"):
140
+ return None
141
+ if method == "tools/list":
142
+ return {"jsonrpc": "2.0", "id": rid, "result": {"tools": TOOLS}}
143
+ if method == "tools/call":
144
+ params = msg.get("params") or {}
145
+ name = str(params.get("name") or "")
146
+ fn = _TOOL_IMPL.get(name)
147
+ if fn is None:
148
+ return {"jsonrpc": "2.0", "id": rid, "result": {
149
+ "isError": True, "content": "未知工具: %s" % name}}
150
+ try:
151
+ out = fn(params.get("arguments") or {})
152
+ except Exception as e:
153
+ out = {"isError": True, "content": "工具执行失败: %s" % e}
154
+ return {"jsonrpc": "2.0", "id": rid, "result": out}
155
+ return {"jsonrpc": "2.0", "id": rid,
156
+ "error": {"code": -32601, "message": "no such method: %s" % method}}
157
+
158
+
159
+ def main():
160
+ for raw in sys.stdin:
161
+ line = raw.strip()
162
+ if not line.startswith("{"):
163
+ continue
164
+ try:
165
+ msg = json.loads(line)
166
+ except Exception:
167
+ continue
168
+ resp = handle_request(msg)
169
+ if resp is not None:
170
+ sys.stdout.write(json.dumps(resp, ensure_ascii=False) + "\n")
171
+ sys.stdout.flush()
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()
package/app/pet.py CHANGED
@@ -11,6 +11,10 @@ CodeBee 蜜蜂。实现要点:
11
11
  庆祝=原地旋转、睡觉=调暗降饱和、失联=灰化);机器没有 Pillow 或素材缺失
12
12
  时回落「手绘模式」——canvas 矢量卡通蜜蜂(零依赖兜底,npm 分发用户可能
13
13
  装不了 PIL)。
14
+ - 底板:Windows 用 -transparentcolor 键色逐像素透明,蜜蜂真浮空;macOS 的
15
+ Tk 没有逐像素透明(键色是 Windows 专属),矩形底板必然整个露出来还带
16
+ 系统边框——非 Windows 把底板画成深色卡片(「桌面小组件」形态),状态
17
+ 滤镜只作用于蜂体、不碰底板色(2026-09-25 Mac 实拍白板翻车)。
14
18
  - 状态机:sleep(打盹)/ work(振翅)/ cheer(翻滚庆祝)/ alert(警示抖动),
15
19
  由「上一轮活跃任务集合 vs 本轮结果」的差分推出,不需要后端记事件流。
16
20
  - 悬停任务清单走指针轮询而不是 Enter/Leave:-transparentcolor 的透明像素在
@@ -310,6 +314,72 @@ def _http_json(port, path, body=None):
310
314
  # 用罕见深色,蜜蜂配色不会撞上。
311
315
  KEY = "#010203"
312
316
  KEY_RGB = (1, 2, 3)
317
+ # 非 Windows 底板:macOS 的 Tk 没有逐像素透明(-transparentcolor 是
318
+ # Windows 专属),矩形底板必然整个露出来还带系统边框——索性把底板画成
319
+ # 深色卡片(对齐悬停清单面板色),桌宠呈「桌面小组件」形态,而不是
320
+ # 一块突兀的白板(2026-09-25 Mac 实拍翻车)。
321
+ CARD_BG = "#1B2430"
322
+ CARD_RGB = (27, 36, 48)
323
+
324
+
325
+ def plate_for(os_name):
326
+ """底板色:Windows=键色(配 -transparentcolor 逐像素透明);其余=卡片色。"""
327
+ return KEY if os_name == "nt" else CARD_BG
328
+
329
+
330
+ def plate_rgb_for(os_name):
331
+ """底板色的 RGB 元组(精灵帧合成/噪点归底用),与 plate_for 同源。"""
332
+ return KEY_RGB if os_name == "nt" else CARD_RGB
333
+
334
+
335
+ PLATE = plate_for(os.name)
336
+ PLATE_RGB = plate_rgb_for(os.name)
337
+
338
+
339
+ def composite_plate(img, plate_rgb):
340
+ """RGBA 精灵合成到不透明底色(纯像素逻辑,不碰 Tk,可测试)。"""
341
+ from PIL import Image
342
+ bg = Image.new("RGBA", img.size, tuple(plate_rgb) + (255,))
343
+ bg.alpha_composite(img)
344
+ return bg.convert("RGB")
345
+
346
+
347
+ def knock_to_plate(img, plate_rgb):
348
+ """近底色噪点(旋转/缩放的 AA 碎点)归底色,原地修改并返回。"""
349
+ pr, pg, pb = plate_rgb
350
+ px = img.load()
351
+ w, h = img.size
352
+ for yy in range(h):
353
+ for xx in range(w):
354
+ r, g, b = px[xx, yy]
355
+ if abs(r - pr) + abs(g - pg) + abs(b - pb) < 12:
356
+ px[xx, yy] = plate_rgb
357
+ return img
358
+
359
+
360
+ def mood_rgba(img, color=1.0, bright=1.0):
361
+ """状态滤镜(降饱和/调暗/灰化)只作用于有内容的像素,底板色不动。
362
+
363
+ ImageEnhance 系列对整图混色,会把透明洞当黑色往里掺;而本文件的做法
364
+ 是先合成到底板再出帧,底板跟着变暗后卡片上就浮出一块更暗的小框
365
+ (Windows 靠近键噪点归键兜回来,Mac 卡片底兜不回来)——故按 alpha
366
+ 手做:透明像素原样,蜂体像素做 luma 降饱和 + 调暗。不修改入参图。
367
+ """
368
+ img = img.copy()
369
+ px = img.load()
370
+ w, h = img.size
371
+ for yy in range(h):
372
+ for xx in range(w):
373
+ r, g, b, a = px[xx, yy]
374
+ if not a:
375
+ continue
376
+ lum = int(r * 0.299 + g * 0.587 + b * 0.114)
377
+ px[xx, yy] = (
378
+ min(255, max(0, int((lum * (1 - color) + r * color) * bright))),
379
+ min(255, max(0, int((lum * (1 - color) + g * color) * bright))),
380
+ min(255, max(0, int((lum * (1 - color) + b * color) * bright))),
381
+ a)
382
+ return img
313
383
  WIN_W, WIN_H = 170, 150 # 手绘回落模式的窗口尺寸
314
384
  SPRITE_DIR = Path(__file__).resolve().parent
315
385
  SPRITE_DISP_H = 112 # 精灵显示高度(宽等比)
@@ -496,7 +566,7 @@ class PetApp:
496
566
  self.root.attributes("-transparentcolor", KEY)
497
567
  except Exception:
498
568
  pass
499
- self.root.configure(bg=KEY)
569
+ self.root.configure(bg=PLATE)
500
570
 
501
571
  # 形象:本地 cfg 优先(换形象要立刻见效,不等服务端轮询),缺省 plush
502
572
  self.skin = str(cfg.get("skin") or DEFAULT_SKIN)
@@ -515,7 +585,7 @@ class PetApp:
515
585
  self.win_w, self.win_h = WIN_W, WIN_H
516
586
 
517
587
  self.cv = tk.Canvas(self.root, width=self.win_w, height=self.win_h,
518
- bg=KEY, highlightthickness=0, bd=0)
588
+ bg=PLATE, highlightthickness=0, bd=0)
519
589
  self.cv.pack()
520
590
  self.spr = None
521
591
 
@@ -628,7 +698,7 @@ class PetApp:
628
698
  expand 放画布,窗口会比蜂体大一圈,点缀画件全悬空(真机截图翻车)。
629
699
  """
630
700
  try:
631
- from PIL import Image, ImageEnhance, ImageTk
701
+ from PIL import Image, ImageTk
632
702
  except Exception:
633
703
  # 无 Pillow:用 Tk 原生 PhotoImage 直接贴 PNG(Tk 8.6+ 原生解码,
634
704
  # 零依赖)——此前直接回落手绘蜂,普通用户 npm 装完没有 Pillow,
@@ -659,30 +729,21 @@ class PetApp:
659
729
  base = base0.resize((disp_w, disp_h), Image.LANCZOS)
660
730
 
661
731
  def flat(img):
662
- """合成到键色底:羽化边缘自然过渡,键色即透明,无需真 alpha。"""
663
- bg = Image.new("RGBA", img.size, KEY_RGB + (255,))
664
- bg.alpha_composite(img)
665
- return bg.convert("RGB")
732
+ """合成到底板色:Windows 键色待逐像素透明,Mac 卡片底;
733
+ 羽化边缘自然过渡,无需真 alpha。"""
734
+ return composite_plate(img, PLATE_RGB)
666
735
 
667
736
  def bake(img):
668
- """RGB → PhotoImage;近键色噪点(旋转/缩放的 AA 碎点)归键。"""
669
- px = img.load()
670
- w, h = img.size
671
- for yy in range(h):
672
- for xx in range(w):
673
- r, g, b = px[xx, yy]
674
- if abs(r - 1) + abs(g - 2) + abs(b - 3) < 12:
675
- px[xx, yy] = KEY_RGB
676
- return ImageTk.PhotoImage(img)
737
+ """RGB → PhotoImage;近底色噪点(旋转/缩放的 AA 碎点)归底。"""
738
+ return ImageTk.PhotoImage(knock_to_plate(img, PLATE_RGB))
677
739
 
678
740
  rot = lambda a: flat(base.rotate(a, resample=Image.BICUBIC))
679
741
  frames = {
680
742
  "alert": [bake(flat(base))],
681
- "sleep": [bake(ImageEnhance.Brightness(
682
- ImageEnhance.Color(flat(base)).enhance(0.7)
683
- ).enhance(0.55))],
684
- "dead": [bake(ImageEnhance.Brightness(
685
- flat(base).convert("L").convert("RGB")).enhance(0.6))],
743
+ # 睡觉降饱和调暗 / 失联灰化:滤镜只碰蜂体不碰底板
744
+ # (mood_rgba 按 alpha 手做,底板色恒定,卡片不浮暗框)
745
+ "sleep": [bake(flat(mood_rgba(base, color=0.7, bright=0.55)))],
746
+ "dead": [bake(flat(mood_rgba(base, color=0.0, bright=0.6)))],
686
747
  "work": [bake(rot(a)) for a in (-8, -4, 0, 4, 8)],
687
748
  # 欢腾摇摆:左倾-回正-右倾-回正,像跳舞不像旋转木马
688
749
  "cheer": [bake(rot(a)) for a in (0, 10, 20, 10, 0, -10,
@@ -1,110 +1,110 @@
1
- #!/usr/bin/env python3
2
- """原生「选择文件夹」对话框(main.py 的 /api/pick_folder 用)。
3
-
4
- Tk 必须活在自家进程的主线程里:HTTP 请求线程里建 root 在 macOS 上会崩,
5
- Windows 上反复建/销毁也不稳,所以隔离成独立进程。ask_directory() 是父端
6
- 入口,把本文件拉起为子进程,请求参数(JSON:initial 起始目录 / title 标题)
7
- 走 stdin,选中目录以 JSON({"path": ...})走 stdout,用户取消回空串;
8
- 对话框部分是文件尾的 main()。任何一步失败都以非零码退出,父端据此回
9
- fallback=true 让前端回落网页目录弹框。"""
10
- import json
11
- import os
12
- import subprocess
13
- import sys
14
- from pathlib import Path
15
-
16
- # 非 Windows 置 0:POSIX 的 Popen 对非零 creationflags 抛 ValueError(manager 同款守卫)
17
- CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
18
- _SELF = str(Path(__file__).resolve())
19
-
20
-
21
- def ask_directory(initial="", title="选择文件夹"):
22
- """父端入口:拉起子进程弹原生对话框,返回 (path, error, fallback)。
23
-
24
- 用户取消时 path 为空串;子进程起不来(机器无 tkinter 等)时 error
25
- 非空且 fallback=True,调用方原样转给前端回落网页目录弹框。"""
26
- if not Path(__file__).exists():
27
- return "", "pick_dialog.py 缺失", True
28
- req = json.dumps({"initial": initial, "title": title}).encode("utf-8")
29
- env = dict(os.environ, PYTHONIOENCODING="utf-8")
30
- try:
31
- if os.name == "nt":
32
- cp = subprocess.run([sys.executable, _SELF], input=req,
33
- capture_output=True, timeout=600, env=env,
34
- creationflags=CREATE_NO_WINDOW)
35
- else:
36
- cp = subprocess.run([sys.executable, _SELF], input=req,
37
- capture_output=True, timeout=600, env=env)
38
- except Exception as e:
39
- return "", str(e), True
40
- if cp.returncode != 0:
41
- err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
42
- return "", (err[-1] if err else "native picker unavailable"), True
43
- try:
44
- data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
45
- except Exception:
46
- return "", "对话框输出无法解析", True
47
- return str(data.get("path") or ""), "", False
48
-
49
-
50
- def ask_file(initial="", title="选择文件"):
51
- """父端入口:拉起子进程弹原生「选择文件」对话框(导入备份包用)。
52
- 返回 (path, error, fallback),语义与 ask_directory 一致。"""
53
- if not Path(__file__).exists():
54
- return "", "pick_dialog.py 缺失", True
55
- req = json.dumps({"mode": "file", "initial": initial, "title": title}).encode("utf-8")
56
- env = dict(os.environ, PYTHONIOENCODING="utf-8")
57
- try:
58
- if os.name == "nt":
59
- cp = subprocess.run([sys.executable, _SELF], input=req,
60
- capture_output=True, timeout=600, env=env,
61
- creationflags=CREATE_NO_WINDOW)
62
- else:
63
- cp = subprocess.run([sys.executable, _SELF], input=req,
64
- capture_output=True, timeout=600, env=env)
65
- except Exception as e:
66
- return "", str(e), True
67
- if cp.returncode != 0:
68
- err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
69
- return "", (err[-1] if err else "native picker unavailable"), True
70
- try:
71
- data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
72
- except Exception:
73
- return "", "对话框输出无法解析", True
74
- return str(data.get("path") or ""), "", False
75
-
76
-
77
- def main():
78
- req = {}
79
- try:
80
- req = json.loads(sys.stdin.read() or "{}")
81
- except Exception:
82
- pass
83
- import tkinter as tk
84
- from tkinter import filedialog
85
-
86
- root = tk.Tk()
87
- root.withdraw()
88
- try:
89
- root.attributes("-topmost", True) # 别被全屏浏览器盖住
90
- except Exception:
91
- pass
92
- is_file = req.get("mode") == "file"
93
- kw = {"title": req.get("title") or ("选择文件" if is_file else "选择文件夹")}
94
- init = req.get("initial") or ""
95
- if init and os.path.isdir(init):
96
- kw["initialdir"] = init
97
- try:
98
- if is_file:
99
- kw["filetypes"] = [("备份包", "*.zip"), ("全部文件", "*.*")]
100
- path = filedialog.askopenfilename(**kw) or ""
101
- else:
102
- path = filedialog.askdirectory(**kw) or ""
103
- except Exception:
104
- sys.exit(1)
105
- sys.stdout.write(json.dumps({"path": path}))
106
- root.destroy()
107
-
108
-
109
- if __name__ == "__main__":
110
- main()
1
+ #!/usr/bin/env python3
2
+ """原生「选择文件夹」对话框(main.py 的 /api/pick_folder 用)。
3
+
4
+ Tk 必须活在自家进程的主线程里:HTTP 请求线程里建 root 在 macOS 上会崩,
5
+ Windows 上反复建/销毁也不稳,所以隔离成独立进程。ask_directory() 是父端
6
+ 入口,把本文件拉起为子进程,请求参数(JSON:initial 起始目录 / title 标题)
7
+ 走 stdin,选中目录以 JSON({"path": ...})走 stdout,用户取消回空串;
8
+ 对话框部分是文件尾的 main()。任何一步失败都以非零码退出,父端据此回
9
+ fallback=true 让前端回落网页目录弹框。"""
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ # 非 Windows 置 0:POSIX 的 Popen 对非零 creationflags 抛 ValueError(manager 同款守卫)
17
+ CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
18
+ _SELF = str(Path(__file__).resolve())
19
+
20
+
21
+ def ask_directory(initial="", title="选择文件夹"):
22
+ """父端入口:拉起子进程弹原生对话框,返回 (path, error, fallback)。
23
+
24
+ 用户取消时 path 为空串;子进程起不来(机器无 tkinter 等)时 error
25
+ 非空且 fallback=True,调用方原样转给前端回落网页目录弹框。"""
26
+ if not Path(__file__).exists():
27
+ return "", "pick_dialog.py 缺失", True
28
+ req = json.dumps({"initial": initial, "title": title}).encode("utf-8")
29
+ env = dict(os.environ, PYTHONIOENCODING="utf-8")
30
+ try:
31
+ if os.name == "nt":
32
+ cp = subprocess.run([sys.executable, _SELF], input=req,
33
+ capture_output=True, timeout=600, env=env,
34
+ creationflags=CREATE_NO_WINDOW)
35
+ else:
36
+ cp = subprocess.run([sys.executable, _SELF], input=req,
37
+ capture_output=True, timeout=600, env=env)
38
+ except Exception as e:
39
+ return "", str(e), True
40
+ if cp.returncode != 0:
41
+ err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
42
+ return "", (err[-1] if err else "native picker unavailable"), True
43
+ try:
44
+ data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
45
+ except Exception:
46
+ return "", "对话框输出无法解析", True
47
+ return str(data.get("path") or ""), "", False
48
+
49
+
50
+ def ask_file(initial="", title="选择文件"):
51
+ """父端入口:拉起子进程弹原生「选择文件」对话框(导入备份包用)。
52
+ 返回 (path, error, fallback),语义与 ask_directory 一致。"""
53
+ if not Path(__file__).exists():
54
+ return "", "pick_dialog.py 缺失", True
55
+ req = json.dumps({"mode": "file", "initial": initial, "title": title}).encode("utf-8")
56
+ env = dict(os.environ, PYTHONIOENCODING="utf-8")
57
+ try:
58
+ if os.name == "nt":
59
+ cp = subprocess.run([sys.executable, _SELF], input=req,
60
+ capture_output=True, timeout=600, env=env,
61
+ creationflags=CREATE_NO_WINDOW)
62
+ else:
63
+ cp = subprocess.run([sys.executable, _SELF], input=req,
64
+ capture_output=True, timeout=600, env=env)
65
+ except Exception as e:
66
+ return "", str(e), True
67
+ if cp.returncode != 0:
68
+ err = (cp.stderr or b"").decode("utf-8", "replace").strip().splitlines()
69
+ return "", (err[-1] if err else "native picker unavailable"), True
70
+ try:
71
+ data = json.loads((cp.stdout or b"").decode("utf-8", "replace") or "{}")
72
+ except Exception:
73
+ return "", "对话框输出无法解析", True
74
+ return str(data.get("path") or ""), "", False
75
+
76
+
77
+ def main():
78
+ req = {}
79
+ try:
80
+ req = json.loads(sys.stdin.read() or "{}")
81
+ except Exception:
82
+ pass
83
+ import tkinter as tk
84
+ from tkinter import filedialog
85
+
86
+ root = tk.Tk()
87
+ root.withdraw()
88
+ try:
89
+ root.attributes("-topmost", True) # 别被全屏浏览器盖住
90
+ except Exception:
91
+ pass
92
+ is_file = req.get("mode") == "file"
93
+ kw = {"title": req.get("title") or ("选择文件" if is_file else "选择文件夹")}
94
+ init = req.get("initial") or ""
95
+ if init and os.path.isdir(init):
96
+ kw["initialdir"] = init
97
+ try:
98
+ if is_file:
99
+ kw["filetypes"] = [("备份包", "*.zip"), ("全部文件", "*.*")]
100
+ path = filedialog.askopenfilename(**kw) or ""
101
+ else:
102
+ path = filedialog.askdirectory(**kw) or ""
103
+ except Exception:
104
+ sys.exit(1)
105
+ sys.stdout.write(json.dumps({"path": path}))
106
+ root.destroy()
107
+
108
+
109
+ if __name__ == "__main__":
110
+ main()
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "code-review-helper",
3
+ "version": "0.1.0",
4
+ "description": "把代码评审前的风险检查接入 CodeBee。",
5
+ "author": {
6
+ "name": "CodeBee"
7
+ },
8
+ "skills": "./skills/",
9
+ "interface": {
10
+ "displayName": "代码评审助手",
11
+ "shortDescription": "提测前快速检查边界、异常、并发与安全风险。",
12
+ "longDescription": "一个轻量的代码评审守则插件,安装后会在 code 类型任务中自动注入检查清单。",
13
+ "developerName": "CodeBee",
14
+ "category": "代码开发",
15
+ "capabilities": ["Review", "Write"],
16
+ "defaultPrompt": ["请按评审清单检查这段代码。"]
17
+ }
18
+ }
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: 代码评审助手
3
+ scopes:
4
+ - code
5
+ note: 提测前检查边界、异常、并发与安全风险
6
+ ---
7
+
8
+ # 代码评审助手
9
+
10
+ 在提交评审前,按下面顺序检查并给出证据:
11
+
12
+ 1. 输入边界:空值、超长值、非法状态和权限边界是否有明确行为。
13
+ 2. 异常路径:失败是否可观察、可重试,是否会吞掉错误或留下半成品。
14
+ 3. 并发与资源:共享状态、锁、超时、子进程和临时文件是否能收敛。
15
+ 4. 安全:路径穿越、命令注入、凭据泄漏和不可信内容注入是否被阻断。
16
+ 5. 回归:为每个修复点补一个最小测试,并说明没有覆盖的风险。