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.
- package/CHANGELOG.md +580 -543
- package/LICENSE +21 -21
- package/README.md +11 -0
- package/app/browser-final-1440.png +0 -0
- package/app/browser-final-390.png +0 -0
- package/app/browser-google-fallback.png +0 -0
- package/app/browser-google-test.png +0 -0
- package/app/browser-workbench-1440.png +0 -0
- package/app/browser-workbench-390.png +0 -0
- package/app/browser-workspace-google.png +0 -0
- package/app/browser-workspace-smoke.png +0 -0
- package/app/browser-workspace.png +0 -0
- package/app/core/acceptance.py +100 -100
- package/app/core/aiflavor.py +197 -197
- package/app/core/artifacts.py +148 -148
- package/app/core/assets.py +110 -110
- package/app/core/attachments.py +623 -623
- package/app/core/audit.py +30 -0
- package/app/core/automation.py +853 -758
- package/app/core/backup.py +470 -470
- package/app/core/beekeeper.py +183 -183
- package/app/core/benchstore.py +95 -0
- package/app/core/board.py +209 -209
- package/app/core/bookmeta.py +935 -930
- package/app/core/bookmeta_catalog.py +180 -180
- package/app/core/branching.py +137 -137
- package/app/core/builtin_agent.py +1673 -1234
- package/app/core/capability.py +144 -144
- package/app/core/catalog.py +7 -1
- package/app/core/chaptersafe.py +32 -32
- package/app/core/cleanup.py +303 -303
- package/app/core/compaction.py +201 -201
- package/app/core/covergen.py +356 -356
- package/app/core/defectretro.py +43 -43
- package/app/core/diagnostics.py +114 -114
- package/app/core/dispatch.py +190 -173
- package/app/core/dispatch_log.py +128 -128
- package/app/core/env_scrub.py +85 -85
- package/app/core/error_codes.py +33 -24
- package/app/core/errorlog.py +137 -137
- package/app/core/evalbench.py +720 -0
- package/app/core/evaluation.py +174 -174
- package/app/core/failure_notify.py +290 -290
- package/app/core/flows.py +228 -3
- package/app/core/goal_service.py +167 -167
- package/app/core/health.py +374 -374
- package/app/core/history.py +38 -38
- package/app/core/hooks.py +162 -162
- package/app/core/jobs.py +51 -30
- package/app/core/manager.py +2143 -2122
- package/app/core/market.py +362 -362
- package/app/core/market_remote.py +10 -0
- package/app/core/mcp_client.py +287 -0
- package/app/core/mocks.py +64 -64
- package/app/core/modelhub.py +193 -10
- package/app/core/notify.py +201 -106
- package/app/core/novel_quality.py +189 -0
- package/app/core/operations.py +228 -228
- package/app/core/paihang.py +138 -138
- package/app/core/paths.py +6 -2
- package/app/core/pipeline.py +426 -28
- package/app/core/planner.py +243 -22
- package/app/core/plugins.py +525 -0
- package/app/core/portguard.py +111 -111
- package/app/core/preview.py +293 -293
- package/app/core/publish/__init__.py +7 -7
- package/app/core/publish/auto.py +367 -367
- package/app/core/publish/browser.py +655 -539
- package/app/core/publish/fanqie.py +165 -159
- package/app/core/publish/flow.py +589 -440
- package/app/core/publish/flows-fanqie-calibrated.json +336 -332
- package/app/core/publish/ledger.py +229 -203
- package/app/core/publish/manager.py +824 -692
- package/app/core/publish/qimao.py +146 -141
- package/app/core/publish/ws.py +139 -139
- package/app/core/redact.py +38 -38
- package/app/core/remotesync.py +121 -0
- package/app/core/repeat_guard.py +129 -129
- package/app/core/router.py +398 -398
- package/app/core/runner.py +2142 -2067
- package/app/core/selfupdate.py +384 -384
- package/app/core/session_log.py +161 -161
- package/app/core/sessions.py +312 -312
- package/app/core/settings.py +186 -149
- package/app/core/settings_schema.py +270 -262
- package/app/core/share_page.py +114 -114
- package/app/core/skill_scan.py +122 -122
- package/app/core/skillpacks/fanqie-novel.md +80 -80
- package/app/core/skillpacks/market/character-bible.md +66 -66
- package/app/core/skillpacks/market/code-risk-checklist.md +58 -58
- package/app/core/skillpacks/market/git-workflow.md +57 -57
- package/app/core/skillpacks/market/release-notes.md +72 -72
- package/app/core/skillpacks/market/weekly-report.md +71 -71
- package/app/core/skillpacks/market/worldview-consistency.md +70 -70
- package/app/core/step_runner.py +132 -132
- package/app/core/store.py +104 -82
- package/app/core/task_compile.py +199 -199
- package/app/core/telemetry.py +292 -292
- package/app/core/tlsctx.py +64 -64
- package/app/core/token_meter.py +147 -147
- package/app/core/tracing.py +144 -144
- package/app/core/upstream.py +41 -41
- package/app/core/usage.py +1035 -862
- package/app/core/volumes.py +389 -389
- package/app/core/wxdigest.py +858 -858
- package/app/core/zentao.py +2614 -2465
- package/app/current-browser.png +0 -0
- package/app/main.py +3245 -2981
- package/app/mcp_server.py +175 -0
- package/app/pet.py +82 -21
- package/app/pick_dialog.py +110 -110
- package/app/plugins/code-review-helper/.codex-plugin/plugin.json +18 -0
- package/app/plugins/code-review-helper/skills/review.md +16 -0
- package/app/ui/app.js +15706 -14399
- package/app/ui/artdiff.js +74 -74
- package/app/ui/board.html +162 -162
- package/app/ui/board.js +305 -305
- package/app/ui/i18n.js +3012 -2770
- package/app/ui/icons/bee.svg +79 -79
- package/app/ui/index.html +277 -13
- package/app/ui/manifest.json +16 -16
- package/app/ui/qrcode.js +2297 -2297
- package/app/ui/style.css +802 -16
- package/bin/tutti.js +147 -147
- package/package.json +47 -46
- package/tools/wx_reader.py +261 -261
- package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +0 -88
- package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +0 -126
- package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +0 -210
- package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
- package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
- 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=
|
|
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=
|
|
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,
|
|
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
|
-
"""
|
|
663
|
-
|
|
664
|
-
|
|
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
|
|
669
|
-
|
|
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
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
"dead": [bake(
|
|
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,
|
package/app/pick_dialog.py
CHANGED
|
@@ -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. 回归:为每个修复点补一个最小测试,并说明没有覆盖的风险。
|