codebee 0.1.101 → 0.1.103

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 (134) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -21
  3. package/README.md +3 -6
  4. package/app/.playwright-cli/console-2026-09-23T08-26-03-635Z.log +88 -0
  5. package/app/.playwright-cli/page-2026-09-23T08-26-03-845Z.yml +126 -0
  6. package/app/.playwright-cli/page-2026-09-23T08-29-44-203Z.png +0 -0
  7. package/app/.playwright-cli/page-2026-09-23T08-35-42-046Z.yml +210 -0
  8. package/app/.playwright-cli/page-2026-09-23T08-36-52-858Z.png +0 -0
  9. package/app/.playwright-cli/page-2026-09-23T08-37-33-938Z.png +0 -0
  10. package/app/.playwright-cli/run-detail-after.png +0 -0
  11. package/app/core/acceptance.py +100 -100
  12. package/app/core/acp_client.py +457 -426
  13. package/app/core/aiflavor.py +197 -197
  14. package/app/core/analytics.py +38 -38
  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 -30
  19. package/app/core/backend_profiles.py +188 -188
  20. package/app/core/backup.py +470 -470
  21. package/app/core/beekeeper.py +183 -183
  22. package/app/core/benchstore.py +102 -102
  23. package/app/core/board.py +21 -17
  24. package/app/core/bookmeta.py +1036 -1036
  25. package/app/core/bookmeta_catalog.py +180 -180
  26. package/app/core/branching.py +137 -137
  27. package/app/core/builtin_agent.py +13 -4
  28. package/app/core/capability.py +144 -144
  29. package/app/core/catalog.py +28 -6
  30. package/app/core/chaptersafe.py +32 -32
  31. package/app/core/checkpoints.py +117 -31
  32. package/app/core/cleanup.py +303 -303
  33. package/app/core/command_auth.py +302 -302
  34. package/app/core/compaction.py +201 -201
  35. package/app/core/contracts.py +255 -255
  36. package/app/core/covergen.py +356 -356
  37. package/app/core/defectretro.py +43 -43
  38. package/app/core/diagnostics.py +114 -114
  39. package/app/core/dispatch.py +1 -1
  40. package/app/core/dispatch_log.py +128 -128
  41. package/app/core/env_scrub.py +85 -85
  42. package/app/core/errorlog.py +137 -137
  43. package/app/core/eval_matrix.py +91 -82
  44. package/app/core/evaluation.py +174 -174
  45. package/app/core/failure_notify.py +290 -290
  46. package/app/core/flow_graph.py +33 -33
  47. package/app/core/goal_service.py +167 -167
  48. package/app/core/health.py +389 -389
  49. package/app/core/history.py +38 -38
  50. package/app/core/hooks.py +162 -162
  51. package/app/core/knowledge_pipeline.py +86 -86
  52. package/app/core/manager.py +2385 -2385
  53. package/app/core/mcp_client.py +7 -2
  54. package/app/core/mocks.py +64 -64
  55. package/app/core/notify.py +201 -201
  56. package/app/core/novel_quality.py +181 -181
  57. package/app/core/openapi.py +217 -209
  58. package/app/core/operations.py +228 -228
  59. package/app/core/paihang.py +145 -145
  60. package/app/core/pipeline.py +6 -3
  61. package/app/core/policy.py +9 -0
  62. package/app/core/portguard.py +111 -111
  63. package/app/core/presence.py +77 -77
  64. package/app/core/preview.py +293 -293
  65. package/app/core/project_memory.py +206 -206
  66. package/app/core/prompt_guard.py +78 -78
  67. package/app/core/publish/__init__.py +7 -7
  68. package/app/core/publish/auto.py +659 -562
  69. package/app/core/publish/chaprows.py +138 -138
  70. package/app/core/publish/fanqie.py +16 -10
  71. package/app/core/publish/flow.py +29 -1
  72. package/app/core/publish/flows-fanqie-calibrated.json +442 -442
  73. package/app/core/publish/manager.py +1185 -1177
  74. package/app/core/publish/qimao.py +193 -193
  75. package/app/core/publish/volumes.py +108 -108
  76. package/app/core/publish/ws.py +139 -139
  77. package/app/core/quality_gate.py +201 -201
  78. package/app/core/redact.py +38 -38
  79. package/app/core/repeat_guard.py +129 -129
  80. package/app/core/retrieval.py +137 -137
  81. package/app/core/sandbox_runtime.py +95 -0
  82. package/app/core/selfupdate.py +384 -384
  83. package/app/core/session_log.py +161 -161
  84. package/app/core/sessions.py +312 -312
  85. package/app/core/settings.py +215 -215
  86. package/app/core/settings_schema.py +282 -282
  87. package/app/core/share_page.py +114 -114
  88. package/app/core/skill_scan.py +139 -139
  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 +13 -3
  97. package/app/core/story_tracking.py +275 -275
  98. package/app/core/task_compile.py +199 -199
  99. package/app/core/telemetry.py +292 -292
  100. package/app/core/tlsctx.py +64 -64
  101. package/app/core/token_meter.py +147 -147
  102. package/app/core/tool_outputs.py +107 -107
  103. package/app/core/tracing.py +207 -207
  104. package/app/core/upstream.py +41 -41
  105. package/app/core/usage.py +1038 -1038
  106. package/app/core/volumes.py +389 -389
  107. package/app/core/webhooks.py +89 -89
  108. package/app/core/wxdigest.py +865 -865
  109. package/app/core/zentao.py +2692 -2692
  110. package/app/main.py +1 -0
  111. package/app/mcp_server.py +244 -244
  112. package/app/output/_createbook_body.json +1 -0
  113. package/app/output/_probe_fq_click.py +50 -0
  114. package/app/output/_probe_fq_form.py +42 -0
  115. package/app/output/_probe_fq_modal.py +55 -0
  116. package/app/output/_probe_fq_points.py +71 -0
  117. package/app/output/_probe_fq_submit.py +34 -0
  118. package/app/output/_probe_fq_watch.py +62 -0
  119. package/app/output/workbench-clean-preview.jpg +0 -0
  120. package/app/output/workbench-reference-render.png +0 -0
  121. package/app/output/workbench-shell.png +0 -0
  122. package/app/pick_dialog.py +110 -110
  123. package/app/ui/app.js +165 -29
  124. package/app/ui/artdiff.js +74 -74
  125. package/app/ui/hive3d.js +491 -223
  126. package/app/ui/i18n.js +18 -1
  127. package/app/ui/icons/bee.svg +79 -79
  128. package/app/ui/index.html +24 -4
  129. package/app/ui/manifest.json +16 -16
  130. package/app/ui/qrcode.js +2297 -2297
  131. package/app/ui/style.css +548 -18
  132. package/bin/tutti.js +147 -147
  133. package/package.json +1 -1
  134. package/tools/wx_reader.py +261 -261
@@ -1,215 +1,215 @@
1
- # -*- coding: utf-8 -*-
2
- """运行设置(data/settings.json):并发 worker 数 + 默认保存路径。"""
3
- from __future__ import annotations
4
-
5
- import json
6
- import threading
7
- from pathlib import Path
8
-
9
- from . import paths
10
-
11
- _LOCK = threading.RLock()
12
- _FILE = paths.DATA_DIR / "settings.json"
13
-
14
- # default_workdir 为空表示未自定义,用 builtin_workdir() 回落;
15
- # telemetry_errors:匿名错误回传开关(默认开;关掉后版本 ping/错误上传/诊断包遥测部分全部停发,
16
- # 「导出诊断包」是用户手动操作不受此开关限制)
17
- # publish_daily_cap / publish_fail_streak / publish_fail_window_h:自动发布护栏——
18
- # 每任务每平台每日成功发章上限、平台连续失败几次后暂停自动发布、连败只统计
19
- # 最近多少小时内的失败(跨天旧失败不进「连续」口径)(publish/auto.py 读取)
20
- # pet_enabled / pet_mode:桌面蜜蜂(app/pet.py)开关与显示模式(默认关,设置页
21
- # 可开)。关闭后看护线程不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always
22
- # 不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always 常驻,tasks_only 仅任务
23
- # 运行时出现(空闲 90s 隐身)。
24
- # cleanup_enabled / cleanup_retention_days:每日垃圾清理(core/cleanup.py)——
25
- # 运行过程日志/发布截图/bak 残留等超期自动清理;retention 为保留天数。
26
- # 个人推送通道(core/notify.py 读取):Bark(iOS)/ ntfy(自托管友好)/
27
- # Server酱(微信)/ Telegram Bot。密钥只存本机 settings.json,不出网(推送
28
- # 时才带着发给对应服务)。bark_server 留空回落官方 api.day.app。
29
- DEFAULTS = {"max_concurrent_jobs": 12, "default_workdir": "", "hooks_token": "",
30
- "hooks_signing_secret": "",
31
- "telemetry_errors": True, "publish_daily_cap": 10,
32
- "publish_fail_streak": 3, "publish_fail_window_h": 12,
33
- "notify_webhook": "", "notify_base_url": "",
34
- "notify_bark_server": "", "notify_bark_key": "",
35
- "notify_ntfy_topic": "", "notify_serverchan_key": "",
36
- "notify_telegram_token": "", "notify_telegram_chat_id": "",
37
- # 评测台定时回归(evalbench.fire_due,automation tick 调用):
38
- # enabled=False 关;days 间隔天数控节流;mcp_servers=内置智能体的
39
- # MCP 服务器清单(JSON 数组字符串,stdio 形态)
40
- "bench_auto_enabled": False, "bench_auto_days": 7,
41
- # 评测反哺:实测分软信号进绑定链排序(dispatch 读 benchstore.bonus_for,
42
- # ±4.5 封顶翻不动硬约束)。默认开——评测了就该去影响;关=纯榜单展示
43
- "bench_feedback_enabled": True,
44
- "mcp_servers": "",
45
- # 备份远程通道(remotesync):导出成功后自动 PUT 到 WebDAV/S3 兼容
46
- # 端点(Basic auth);密钥只存本机
47
- "backup_remote_enabled": False, "backup_remote_url": "",
48
- "backup_remote_user": "", "backup_remote_pass": "",
49
- "pet_enabled": False, "pet_mode": "always", "pet_skin": "plush",
50
- "cleanup_enabled": True, "cleanup_retention_days": 14,
51
- # claude_config_sync:打开/运行前防线是否直写 ~/.claude/settings.json。
52
- # 关闭后交互 claude 配置归用户手动管理(cc-switch 等),编排步骤经
53
- # runner 一次性 --settings 注入绑定链,路由不受影响(manager 读取)
54
- "claude_config_sync": True}
55
- # 桌宠形象白名单(与 app/pet.py 的 SKINS 对齐;这里不 import pet 模块,避免
56
- # core 反向依赖 app 根目录脚本)
57
- PET_SKINS = ("plush", "robot")
58
- SECRET_KEYS = frozenset({"hooks_token", "hooks_signing_secret", "notify_webhook",
59
- "notify_bark_key", "notify_ntfy_topic", "notify_serverchan_key",
60
- "notify_telegram_token", "notify_telegram_chat_id", "backup_remote_pass"})
61
-
62
-
63
- def public_view(value=None):
64
- """Return settings suitable for API responses without credential material."""
65
- source = dict(value if isinstance(value, dict) else load())
66
- for key in SECRET_KEYS:
67
- if key in source:
68
- source[key] = "***" if source[key] else ""
69
- return source
70
- # 并发保护上限 24:编排任务有空位即直接启动,满载回滚 queued 排队自动补跑,
71
- # 不秒判失败;同任务单飞守卫在 jobs 层。该上限保护的是本机资源(CLI 子进程
72
- # 数),与各 API 厂商的限流无关;direct 对话走 jobs 轻量池不占此额度。
73
- MIN_WORKERS, MAX_WORKERS = 1, 24
74
-
75
-
76
- def builtin_workdir():
77
- """内置默认保存路径:数据目录内的 workspace/(CodeBee 自己的地盘,
78
- 启动 ensure_dirs 即创建;老版本落在数据目录同级——Windows 上是
79
- AppData\\Roaming\\workspace,平时不存在点开即报错,已废弃)。"""
80
- return str(paths.DATA_DIR / "workspace")
81
-
82
-
83
- def default_workdir():
84
- """当前生效的默认保存路径(自定义优先,回落内置值)。"""
85
- raw = str(load().get("default_workdir") or "").strip()
86
- return raw if raw else builtin_workdir()
87
-
88
-
89
- def _norm_workdir(v):
90
- """展开 ~ 并转绝对路径;空值合法(回落内置)。返回 (标准化串, 错误)。"""
91
- v = str(v or "").strip()
92
- if not v:
93
- return "", None
94
- if v.startswith("~"):
95
- v = str(Path(v).expanduser())
96
- p = Path(v)
97
- if not p.is_absolute():
98
- return "", "默认保存路径必须是绝对路径"
99
- return str(p), None
100
-
101
-
102
- def load():
103
- with _LOCK:
104
- try:
105
- data = json.loads(_FILE.read_text(encoding="utf-8"))
106
- except Exception:
107
- data = {}
108
- out = dict(DEFAULTS)
109
- if isinstance(data, dict):
110
- out.update({k: v for k, v in data.items() if k in DEFAULTS})
111
- try:
112
- out["max_concurrent_jobs"] = max(MIN_WORKERS, min(MAX_WORKERS, int(out["max_concurrent_jobs"])))
113
- except Exception:
114
- out["max_concurrent_jobs"] = DEFAULTS["max_concurrent_jobs"]
115
- wd, _ = _norm_workdir(out.get("default_workdir"))
116
- out["default_workdir"] = wd
117
- out["default_workdir_effective"] = wd if wd else builtin_workdir()
118
- return out
119
-
120
-
121
- def save(patch):
122
- """合并保存。返回 (view, 错误)。"""
123
- if not isinstance(patch, dict):
124
- return load(), "payload 必须是对象"
125
- with _LOCK:
126
- cur = load()
127
- if "max_concurrent_jobs" in patch:
128
- try:
129
- cur["max_concurrent_jobs"] = int(patch["max_concurrent_jobs"])
130
- except Exception:
131
- return cur, "max_concurrent_jobs 必须是整数"
132
- if not MIN_WORKERS <= cur["max_concurrent_jobs"] <= MAX_WORKERS:
133
- return cur, "max_concurrent_jobs 取值 %d-%d" % (MIN_WORKERS, MAX_WORKERS)
134
- if "default_workdir" in patch:
135
- wd, err = _norm_workdir(patch.get("default_workdir"))
136
- if err:
137
- return cur, err
138
- cur["default_workdir"] = wd
139
- if "hooks_token" in patch:
140
- cur["hooks_token"] = str(patch.get("hooks_token") or "").strip()[:128]
141
- if "hooks_signing_secret" in patch:
142
- cur["hooks_signing_secret"] = str(patch.get("hooks_signing_secret") or "").strip()[:256]
143
- if "telemetry_errors" in patch:
144
- cur["telemetry_errors"] = bool(patch.get("telemetry_errors"))
145
- if "publish_daily_cap" in patch:
146
- try:
147
- v = int(patch.get("publish_daily_cap"))
148
- except (TypeError, ValueError):
149
- return cur, "publish_daily_cap 必须是 0-50 的整数(0=不限制)"
150
- cur["publish_daily_cap"] = max(0, min(50, v))
151
- if "publish_fail_streak" in patch:
152
- try:
153
- cur["publish_fail_streak"] = max(1, min(10, int(patch.get("publish_fail_streak"))))
154
- except (TypeError, ValueError):
155
- return cur, "publish_fail_streak 必须是 1-10 的整数"
156
- if "publish_fail_window_h" in patch:
157
- try:
158
- cur["publish_fail_window_h"] = max(1, min(168, float(patch.get("publish_fail_window_h"))))
159
- except (TypeError, ValueError):
160
- return cur, "publish_fail_window_h 必须是 1-168 的数字"
161
- if "notify_webhook" in patch:
162
- cur["notify_webhook"] = str(patch.get("notify_webhook") or "").strip()[:300]
163
- if "notify_base_url" in patch:
164
- cur["notify_base_url"] = str(patch.get("notify_base_url") or "").strip()[:200]
165
- for _nk, _nmax in (("notify_bark_server", 200), ("notify_bark_key", 128),
166
- ("notify_ntfy_topic", 200), ("notify_serverchan_key", 128),
167
- ("notify_telegram_token", 128), ("notify_telegram_chat_id", 64)):
168
- if _nk in patch:
169
- cur[_nk] = str(patch.get(_nk) or "").strip()[:_nmax]
170
- if "bench_auto_enabled" in patch:
171
- cur["bench_auto_enabled"] = bool(patch.get("bench_auto_enabled"))
172
- if "bench_auto_days" in patch:
173
- try:
174
- cur["bench_auto_days"] = max(1, min(90, int(patch.get("bench_auto_days"))))
175
- except (TypeError, ValueError):
176
- return cur, "bench_auto_days 必须是 1-90 的整数"
177
- if "bench_feedback_enabled" in patch:
178
- cur["bench_feedback_enabled"] = bool(patch.get("bench_feedback_enabled"))
179
- if "mcp_servers" in patch:
180
- cur["mcp_servers"] = str(patch.get("mcp_servers") or "").strip()[:4000]
181
- if "backup_remote_enabled" in patch:
182
- cur["backup_remote_enabled"] = bool(patch.get("backup_remote_enabled"))
183
- for _rk, _rmax in (("backup_remote_url", 300), ("backup_remote_user", 128),
184
- ("backup_remote_pass", 128)):
185
- if _rk in patch:
186
- cur[_rk] = str(patch.get(_rk) or "").strip()[:_rmax]
187
- if "pet_enabled" in patch:
188
- cur["pet_enabled"] = bool(patch.get("pet_enabled"))
189
- if "pet_mode" in patch:
190
- pm = str(patch.get("pet_mode") or "").strip()
191
- if pm not in ("always", "tasks_only"):
192
- return cur, "pet_mode 只能是 always 或 tasks_only"
193
- cur["pet_mode"] = pm
194
- if "pet_skin" in patch:
195
- ps = str(patch.get("pet_skin") or "").strip()
196
- if ps not in PET_SKINS:
197
- return cur, "pet_skin 只能是 %s 之一" % "/".join(PET_SKINS)
198
- cur["pet_skin"] = ps
199
- if "cleanup_enabled" in patch:
200
- cur["cleanup_enabled"] = bool(patch.get("cleanup_enabled"))
201
- if "claude_config_sync" in patch:
202
- cur["claude_config_sync"] = bool(patch.get("claude_config_sync"))
203
- if "cleanup_retention_days" in patch:
204
- try:
205
- cur["cleanup_retention_days"] = int(patch.get("cleanup_retention_days"))
206
- except (TypeError, ValueError):
207
- return cur, "cleanup_retention_days 必须是整数"
208
- if not 1 <= cur["cleanup_retention_days"] <= 365:
209
- return cur, "cleanup_retention_days 取值 1-365"
210
- _FILE.parent.mkdir(parents=True, exist_ok=True)
211
- tmp = _FILE.with_suffix(".tmp")
212
- tmp.write_text(json.dumps(cur, ensure_ascii=False, indent=2), encoding="utf-8")
213
- tmp.replace(_FILE)
214
- cur["default_workdir_effective"] = cur["default_workdir"] if cur["default_workdir"] else builtin_workdir()
215
- return cur, None
1
+ # -*- coding: utf-8 -*-
2
+ """运行设置(data/settings.json):并发 worker 数 + 默认保存路径。"""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import threading
7
+ from pathlib import Path
8
+
9
+ from . import paths
10
+
11
+ _LOCK = threading.RLock()
12
+ _FILE = paths.DATA_DIR / "settings.json"
13
+
14
+ # default_workdir 为空表示未自定义,用 builtin_workdir() 回落;
15
+ # telemetry_errors:匿名错误回传开关(默认开;关掉后版本 ping/错误上传/诊断包遥测部分全部停发,
16
+ # 「导出诊断包」是用户手动操作不受此开关限制)
17
+ # publish_daily_cap / publish_fail_streak / publish_fail_window_h:自动发布护栏——
18
+ # 每任务每平台每日成功发章上限、平台连续失败几次后暂停自动发布、连败只统计
19
+ # 最近多少小时内的失败(跨天旧失败不进「连续」口径)(publish/auto.py 读取)
20
+ # pet_enabled / pet_mode:桌面蜜蜂(app/pet.py)开关与显示模式(默认关,设置页
21
+ # 可开)。关闭后看护线程不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always
22
+ # 不再拉起、在岗蜜蜂轮询到 false 自行退出;mode=always 常驻,tasks_only 仅任务
23
+ # 运行时出现(空闲 90s 隐身)。
24
+ # cleanup_enabled / cleanup_retention_days:每日垃圾清理(core/cleanup.py)——
25
+ # 运行过程日志/发布截图/bak 残留等超期自动清理;retention 为保留天数。
26
+ # 个人推送通道(core/notify.py 读取):Bark(iOS)/ ntfy(自托管友好)/
27
+ # Server酱(微信)/ Telegram Bot。密钥只存本机 settings.json,不出网(推送
28
+ # 时才带着发给对应服务)。bark_server 留空回落官方 api.day.app。
29
+ DEFAULTS = {"max_concurrent_jobs": 12, "default_workdir": "", "hooks_token": "",
30
+ "hooks_signing_secret": "",
31
+ "telemetry_errors": True, "publish_daily_cap": 10,
32
+ "publish_fail_streak": 3, "publish_fail_window_h": 12,
33
+ "notify_webhook": "", "notify_base_url": "",
34
+ "notify_bark_server": "", "notify_bark_key": "",
35
+ "notify_ntfy_topic": "", "notify_serverchan_key": "",
36
+ "notify_telegram_token": "", "notify_telegram_chat_id": "",
37
+ # 评测台定时回归(evalbench.fire_due,automation tick 调用):
38
+ # enabled=False 关;days 间隔天数控节流;mcp_servers=内置智能体的
39
+ # MCP 服务器清单(JSON 数组字符串,stdio 形态)
40
+ "bench_auto_enabled": False, "bench_auto_days": 7,
41
+ # 评测反哺:实测分软信号进绑定链排序(dispatch 读 benchstore.bonus_for,
42
+ # ±4.5 封顶翻不动硬约束)。默认开——评测了就该去影响;关=纯榜单展示
43
+ "bench_feedback_enabled": True,
44
+ "mcp_servers": "",
45
+ # 备份远程通道(remotesync):导出成功后自动 PUT 到 WebDAV/S3 兼容
46
+ # 端点(Basic auth);密钥只存本机
47
+ "backup_remote_enabled": False, "backup_remote_url": "",
48
+ "backup_remote_user": "", "backup_remote_pass": "",
49
+ "pet_enabled": False, "pet_mode": "always", "pet_skin": "plush",
50
+ "cleanup_enabled": True, "cleanup_retention_days": 14,
51
+ # claude_config_sync:打开/运行前防线是否直写 ~/.claude/settings.json。
52
+ # 关闭后交互 claude 配置归用户手动管理(cc-switch 等),编排步骤经
53
+ # runner 一次性 --settings 注入绑定链,路由不受影响(manager 读取)
54
+ "claude_config_sync": True}
55
+ # 桌宠形象白名单(与 app/pet.py 的 SKINS 对齐;这里不 import pet 模块,避免
56
+ # core 反向依赖 app 根目录脚本)
57
+ PET_SKINS = ("plush", "robot")
58
+ SECRET_KEYS = frozenset({"hooks_token", "hooks_signing_secret", "notify_webhook",
59
+ "notify_bark_key", "notify_ntfy_topic", "notify_serverchan_key",
60
+ "notify_telegram_token", "notify_telegram_chat_id", "backup_remote_pass"})
61
+
62
+
63
+ def public_view(value=None):
64
+ """Return settings suitable for API responses without credential material."""
65
+ source = dict(value if isinstance(value, dict) else load())
66
+ for key in SECRET_KEYS:
67
+ if key in source:
68
+ source[key] = "***" if source[key] else ""
69
+ return source
70
+ # 并发保护上限 24:编排任务有空位即直接启动,满载回滚 queued 排队自动补跑,
71
+ # 不秒判失败;同任务单飞守卫在 jobs 层。该上限保护的是本机资源(CLI 子进程
72
+ # 数),与各 API 厂商的限流无关;direct 对话走 jobs 轻量池不占此额度。
73
+ MIN_WORKERS, MAX_WORKERS = 1, 24
74
+
75
+
76
+ def builtin_workdir():
77
+ """内置默认保存路径:数据目录内的 workspace/(CodeBee 自己的地盘,
78
+ 启动 ensure_dirs 即创建;老版本落在数据目录同级——Windows 上是
79
+ AppData\\Roaming\\workspace,平时不存在点开即报错,已废弃)。"""
80
+ return str(paths.DATA_DIR / "workspace")
81
+
82
+
83
+ def default_workdir():
84
+ """当前生效的默认保存路径(自定义优先,回落内置值)。"""
85
+ raw = str(load().get("default_workdir") or "").strip()
86
+ return raw if raw else builtin_workdir()
87
+
88
+
89
+ def _norm_workdir(v):
90
+ """展开 ~ 并转绝对路径;空值合法(回落内置)。返回 (标准化串, 错误)。"""
91
+ v = str(v or "").strip()
92
+ if not v:
93
+ return "", None
94
+ if v.startswith("~"):
95
+ v = str(Path(v).expanduser())
96
+ p = Path(v)
97
+ if not p.is_absolute():
98
+ return "", "默认保存路径必须是绝对路径"
99
+ return str(p), None
100
+
101
+
102
+ def load():
103
+ with _LOCK:
104
+ try:
105
+ data = json.loads(_FILE.read_text(encoding="utf-8"))
106
+ except Exception:
107
+ data = {}
108
+ out = dict(DEFAULTS)
109
+ if isinstance(data, dict):
110
+ out.update({k: v for k, v in data.items() if k in DEFAULTS})
111
+ try:
112
+ out["max_concurrent_jobs"] = max(MIN_WORKERS, min(MAX_WORKERS, int(out["max_concurrent_jobs"])))
113
+ except Exception:
114
+ out["max_concurrent_jobs"] = DEFAULTS["max_concurrent_jobs"]
115
+ wd, _ = _norm_workdir(out.get("default_workdir"))
116
+ out["default_workdir"] = wd
117
+ out["default_workdir_effective"] = wd if wd else builtin_workdir()
118
+ return out
119
+
120
+
121
+ def save(patch):
122
+ """合并保存。返回 (view, 错误)。"""
123
+ if not isinstance(patch, dict):
124
+ return load(), "payload 必须是对象"
125
+ with _LOCK:
126
+ cur = load()
127
+ if "max_concurrent_jobs" in patch:
128
+ try:
129
+ cur["max_concurrent_jobs"] = int(patch["max_concurrent_jobs"])
130
+ except Exception:
131
+ return cur, "max_concurrent_jobs 必须是整数"
132
+ if not MIN_WORKERS <= cur["max_concurrent_jobs"] <= MAX_WORKERS:
133
+ return cur, "max_concurrent_jobs 取值 %d-%d" % (MIN_WORKERS, MAX_WORKERS)
134
+ if "default_workdir" in patch:
135
+ wd, err = _norm_workdir(patch.get("default_workdir"))
136
+ if err:
137
+ return cur, err
138
+ cur["default_workdir"] = wd
139
+ if "hooks_token" in patch:
140
+ cur["hooks_token"] = str(patch.get("hooks_token") or "").strip()[:128]
141
+ if "hooks_signing_secret" in patch:
142
+ cur["hooks_signing_secret"] = str(patch.get("hooks_signing_secret") or "").strip()[:256]
143
+ if "telemetry_errors" in patch:
144
+ cur["telemetry_errors"] = bool(patch.get("telemetry_errors"))
145
+ if "publish_daily_cap" in patch:
146
+ try:
147
+ v = int(patch.get("publish_daily_cap"))
148
+ except (TypeError, ValueError):
149
+ return cur, "publish_daily_cap 必须是 0-50 的整数(0=不限制)"
150
+ cur["publish_daily_cap"] = max(0, min(50, v))
151
+ if "publish_fail_streak" in patch:
152
+ try:
153
+ cur["publish_fail_streak"] = max(1, min(10, int(patch.get("publish_fail_streak"))))
154
+ except (TypeError, ValueError):
155
+ return cur, "publish_fail_streak 必须是 1-10 的整数"
156
+ if "publish_fail_window_h" in patch:
157
+ try:
158
+ cur["publish_fail_window_h"] = max(1, min(168, float(patch.get("publish_fail_window_h"))))
159
+ except (TypeError, ValueError):
160
+ return cur, "publish_fail_window_h 必须是 1-168 的数字"
161
+ if "notify_webhook" in patch:
162
+ cur["notify_webhook"] = str(patch.get("notify_webhook") or "").strip()[:300]
163
+ if "notify_base_url" in patch:
164
+ cur["notify_base_url"] = str(patch.get("notify_base_url") or "").strip()[:200]
165
+ for _nk, _nmax in (("notify_bark_server", 200), ("notify_bark_key", 128),
166
+ ("notify_ntfy_topic", 200), ("notify_serverchan_key", 128),
167
+ ("notify_telegram_token", 128), ("notify_telegram_chat_id", 64)):
168
+ if _nk in patch:
169
+ cur[_nk] = str(patch.get(_nk) or "").strip()[:_nmax]
170
+ if "bench_auto_enabled" in patch:
171
+ cur["bench_auto_enabled"] = bool(patch.get("bench_auto_enabled"))
172
+ if "bench_auto_days" in patch:
173
+ try:
174
+ cur["bench_auto_days"] = max(1, min(90, int(patch.get("bench_auto_days"))))
175
+ except (TypeError, ValueError):
176
+ return cur, "bench_auto_days 必须是 1-90 的整数"
177
+ if "bench_feedback_enabled" in patch:
178
+ cur["bench_feedback_enabled"] = bool(patch.get("bench_feedback_enabled"))
179
+ if "mcp_servers" in patch:
180
+ cur["mcp_servers"] = str(patch.get("mcp_servers") or "").strip()[:4000]
181
+ if "backup_remote_enabled" in patch:
182
+ cur["backup_remote_enabled"] = bool(patch.get("backup_remote_enabled"))
183
+ for _rk, _rmax in (("backup_remote_url", 300), ("backup_remote_user", 128),
184
+ ("backup_remote_pass", 128)):
185
+ if _rk in patch:
186
+ cur[_rk] = str(patch.get(_rk) or "").strip()[:_rmax]
187
+ if "pet_enabled" in patch:
188
+ cur["pet_enabled"] = bool(patch.get("pet_enabled"))
189
+ if "pet_mode" in patch:
190
+ pm = str(patch.get("pet_mode") or "").strip()
191
+ if pm not in ("always", "tasks_only"):
192
+ return cur, "pet_mode 只能是 always 或 tasks_only"
193
+ cur["pet_mode"] = pm
194
+ if "pet_skin" in patch:
195
+ ps = str(patch.get("pet_skin") or "").strip()
196
+ if ps not in PET_SKINS:
197
+ return cur, "pet_skin 只能是 %s 之一" % "/".join(PET_SKINS)
198
+ cur["pet_skin"] = ps
199
+ if "cleanup_enabled" in patch:
200
+ cur["cleanup_enabled"] = bool(patch.get("cleanup_enabled"))
201
+ if "claude_config_sync" in patch:
202
+ cur["claude_config_sync"] = bool(patch.get("claude_config_sync"))
203
+ if "cleanup_retention_days" in patch:
204
+ try:
205
+ cur["cleanup_retention_days"] = int(patch.get("cleanup_retention_days"))
206
+ except (TypeError, ValueError):
207
+ return cur, "cleanup_retention_days 必须是整数"
208
+ if not 1 <= cur["cleanup_retention_days"] <= 365:
209
+ return cur, "cleanup_retention_days 取值 1-365"
210
+ _FILE.parent.mkdir(parents=True, exist_ok=True)
211
+ tmp = _FILE.with_suffix(".tmp")
212
+ tmp.write_text(json.dumps(cur, ensure_ascii=False, indent=2), encoding="utf-8")
213
+ tmp.replace(_FILE)
214
+ cur["default_workdir_effective"] = cur["default_workdir"] if cur["default_workdir"] else builtin_workdir()
215
+ return cur, None