codebee 0.1.10 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,27 @@ CodeBee 的用户可感知变更记录。发布新版时:最上面加一节,
4
4
  `<!-- relnotes:start -->…<!-- relnotes:end -->` 段(那段会被 `npm view` 的
5
5
  README 元数据带回,供老版本在「发现新版本」时展示新版更新内容)。
6
6
 
7
+ ## v0.1.12(2026-09-19)
8
+
9
+ - 代码任务计划落盘:每次运行把编排计划写进工作目录 .codebee/task_plan.md,与任务规格(spec.md)、验证证据(evidence.md)同居「任务档案」,断点续跑/复盘全程可查
10
+ - 多子任务防漂移:实现子任务提示词自动带上「第 i/N 项 + 已完成清单」,长任务链不再重做已完成项
11
+ - 连载剧情模块库:plot-modules.md(拆文沉淀的桥段/爽点素材)每章自动注入
12
+ - 连载赛马败者精华:落选候选稿的优点自动并入首轮修订意见
13
+
14
+ ## v0.1.11(2026-09-18)
15
+
16
+ - **连载发布闭环(主轨)**:番茄/七猫作家后台 CDP 直操通道(连接扫码/建书/发章/表单探测/截图存证)+ 详情页出版台面板;发布台账幂等(章号去重)+ 三道护栏(每日上限/连续失败暂停/人工确认闸——默认填好表单停,提交权留给用户)
17
+ - **定时自动发布(P2.5)**:任务级 task.auto_publish 到点触发批量发布;直发模式需 flows 校准文件解锁;批量发布按章号顺序、失败即停
18
+ - 代码任务 Best-of-N:单路失败后 worktree 隔离赛马兜底,验证通过+改动最小者胜(借鉴 orca)
19
+ - 需求拷问采访态:goal 模糊时先问 1-3 个澄清问题再开跑(借鉴 grill-me)
20
+ - 任务队列双病根根治:job 蒸发看门狗自愈 + 排队回填;同因连撞不再重复退避付费;失败退避等待在运行行/运行详情/执行结果/运行记录处显示「将于 HH:MM 自动续跑」,不再笼统显示排队中
21
+ - 运行前配置同步防线:每次真实派发自动把绑定链首落进 CLI 自家配置——绑定换供应商后不再撞上旧 CLI 配置空跑(opencode 钉死旧端点案)
22
+ - 评审新增「AI 味」确定性检测:译制腔/套话密度统计随评审下发参考线(借鉴 oh-story,命中才提示、评审官结合上下文判断)
23
+ - 任务规格文件化:任何任务在工作目录留 .codebee/spec.md(目标/参数/验证命令),随任务分支版本化、复盘续跑可见
24
+ - Git 变更视图豁免 .codebee/ 元数据目录,面板只看任务产出
25
+ - 报告区失败/空报告/无报告分态可见提示,不再静默留白
26
+ - 新增类型菜单成本预估行、步骤卡「实际派发模型」徽章、建 PR 自定义标题(承接 v0.1.10 未列项)
27
+
7
28
  ## v0.1.10(2026-09-18)
8
29
 
9
30
  - Git 工作台「建 PR」支持自定义标题:提交信息框有内容时自动兼作 PR 标题(一处输入两用)
package/README.md CHANGED
@@ -21,12 +21,14 @@ Kimi Code、MiMo Code、Grok Build、Pi、DeepSeek Harness……),提供
21
21
  不会把你的任务内容交给任何第三方。
22
22
 
23
23
  <!-- relnotes:start -->
24
- ### 最新版更新内容(v0.1.8
25
-
26
- - README 全面重写:功能导览 + 实机截图,新增蜂巢/经验库/插件市场/自动化/对话模式等章节
27
- - 修复部分机器上孤儿 CLI 清扫拖慢启动 60 秒的问题(改后台执行,不再挡就绪)
28
- - 连载更稳:死链自动补位活链 CLI、限流退避窗口可视化、答疑单步轮不重跑
29
- - 目录页管理四道防线;评审全挂不再误判未达标;新增欢迎引导与手工添加模型
24
+ ### 最新版更新内容(v0.1.11
25
+
26
+ - **连载发布闭环**:番茄/七猫作家后台一键发布——连接扫码后自动建书/发章(默认填好表单人工确认提交),出版台面板+发布台账+每日上限/连续失败暂停护栏
27
+ - **定时自动发布**:任务到点自动批量发章,直发模式需流程校准文件解锁
28
+ - 代码任务 Best-of-N 赛马兜底;goal 模糊时先问 1-3 个澄清问题再开跑
29
+ - 任务队列自愈(看门狗+排队回填);同因连撞不再重复退避付费,退避期明示「将于 HH:MM 自动续跑」
30
+ - 运行前自动同步 CLI 配置:绑定换供应商后不再撞上旧配置空跑
31
+ - 评审新增「AI 味」参考线;任务留 .codebee/spec.md 规格档案;报告区空态可见
30
32
  <!-- relnotes:end -->
31
33
 
32
34
  ---
@@ -0,0 +1,51 @@
1
+ # -*- coding: utf-8 -*-
2
+ """AI 味确定性检测(借鉴 oh-story-claudecode 的去AI味思路)。
3
+
4
+ 模型评审是主观分,这里补一道客观参考线:统计译制腔/套话高频词的
5
+ 密度,产出报告行注入评审提示词,让评审官把「套话密度」纳入评分
6
+ 参考。只做参考不做门禁——「仿佛」「缓缓」在正常小说里也常见,
7
+ 误伤风险由评审官(能读懂上下文)兜底,脚本只负责指认位置与密度。
8
+ """
9
+ from __future__ import annotations
10
+
11
+ # 译制腔/套话词表(按误导性排序,持续运维)
12
+ AI_PHRASES = (
13
+ "空气仿佛凝固", "眸中闪过一丝", "嘴角勾起一抹", "心中一动",
14
+ "值得注意的是", "总而言之", "综上所述", "不难发现",
15
+ "在这一刻", "仿佛在诉说着", "无声地诉说着", "见证着",
16
+ "深深地看了一眼", "似乎在", "空气仿佛", "一抹微笑",
17
+ "不禁", "仿佛",
18
+ )
19
+
20
+ # 密度告警线(每千字命中次数):超过即提示评审官重点关注
21
+ ALERT_PER_KILO = 8.0
22
+
23
+
24
+ def analyze(text):
25
+ """统计套话命中。返回 {hits: {短语: 次数}, per_kilo: 每千字密度, alert: bool}。"""
26
+ text = text or ""
27
+ total = len(text)
28
+ hits = {}
29
+ for p in AI_PHRASES:
30
+ p = p.strip()
31
+ if not p:
32
+ continue
33
+ n = text.count(p)
34
+ if n:
35
+ hits[p] = n
36
+ n_hits = sum(hits.values())
37
+ per_kilo = round(n_hits * 1000.0 / total, 2) if total else 0.0
38
+ return {"hits": hits, "per_kilo": per_kilo, "alert": per_kilo >= ALERT_PER_KILO}
39
+
40
+
41
+ def report_line(text):
42
+ """生成注入评审提示词的一行报告;无命中返回空串。"""
43
+ r = analyze(text)
44
+ if not r["hits"]:
45
+ return ""
46
+ top = "、".join("「%s」×%d" % (k, v)
47
+ for k, v in sorted(r["hits"].items(), key=lambda x: -x[1])[:8])
48
+ line = "- [AI味检测] 确定性统计:套话密度 %.1f/千字(%s)。" % (r["per_kilo"], top)
49
+ if r["alert"]:
50
+ line += "密度超过告警线 %.0f/千字,请重点评审译制腔与套话问题。" % ALERT_PER_KILO
51
+ return line
@@ -378,6 +378,15 @@ def _tick():
378
378
  _fire(dict(t), now)
379
379
  except Exception:
380
380
  log.exception("automation: tick 处理任务 %s 异常,已跳过", tid)
381
+ # 定时发布联动(P2.5):任务级 auto_publish 标记到点触发批量发布。
382
+ # 发布逻辑全在 publish/auto(护栏/幂等/单飞),这里只当调度宿主。
383
+ try:
384
+ from .publish import auto as _pub_auto
385
+ n = _pub_auto.fire_due()
386
+ if n:
387
+ log.info("automation: 定时发布触发 %d 个任务", n)
388
+ except Exception:
389
+ log.debug("automation: 定时发布检查跳过", exc_info=True)
381
390
 
382
391
 
383
392
  def _loop():
@@ -321,6 +321,10 @@ def collect_changes(workdir, max_diff=30000):
321
321
  continue
322
322
  if xy == "??" and _attach_path(path):
323
323
  continue
324
+ # .codebee/ 是本工具的任务元数据目录(spec.md 等):随任务分支提交版本化,
325
+ # 但不算产品变更——面板/裁决视图只看任务产出,过程文件是噪音
326
+ if path == ".codebee" or path.startswith(".codebee/") or path.startswith('" .codebee'):
327
+ continue
324
328
  files.append({"status": _status_code(xy), "path": path})
325
329
  if xy == "??":
326
330
  untracked.append(path)
package/app/core/jobs.py CHANGED
@@ -20,6 +20,12 @@ _alive = 0 # 活跃 worker 线程数
20
20
  _target = 3 # 目标并发数(settings.max_concurrent_jobs)
21
21
  _pool_lock = threading.Lock()
22
22
  _seq = 0
23
+ MAX_POOL = 12 # 并发上限:每个 job 只是拉起 CLI 子进程,跨任务并行无共享
24
+ # 资源(同任务单飞另有守卫),照竞品(emdash/munder-difflin
25
+ # 高并行环境)放开到 12
26
+ WATCHDOG_INTERVAL_S = 60 # 队列看门狗巡检周期
27
+ WATCHDOG_STALE_S = 120 # queued 超过该秒数视为掉队(正常入队到被拿起 ≤5s)
28
+ _watchdog_started = False
23
29
 
24
30
  AI_REPAIR_PROMPT = """你是环境工程师。在 Windows 上执行下面的安装命令失败了,请诊断原因并给出修正命令。
25
31
  只输出一个 ```json 代码块,不要输出其他内容。JSON 结构:
@@ -48,9 +54,9 @@ def _repair_command_allowed(cmd):
48
54
 
49
55
 
50
56
  def configure(max_workers):
51
- """设置目标并发数(1-6):扩容立即补线程,缩容由空闲线程自行退出。"""
57
+ """设置目标并发数(1-12):扩容立即补线程,缩容由空闲线程自行退出。"""
52
58
  global _target
53
- _target = max(1, min(6, int(max_workers)))
59
+ _target = max(1, min(MAX_POOL, int(max_workers)))
54
60
  if _started:
55
61
  _resize()
56
62
  return _target
@@ -97,14 +103,37 @@ def enqueue(job):
97
103
 
98
104
 
99
105
  def _ensure_workers():
100
- """队列里积压超过空闲 worker 数时补线程(惰性扩容,替代启动期预建)。"""
106
+ """队列里积压超过空闲 worker 数时补线程(惰性扩容,替代启动期预建)。
107
+ 看门狗线程同样惰性起:与 worker 一样不在启动期 Thread.start()(杀软
108
+ 挂起新线程的真实装机案例,见 start_worker 注释),首队到达时一起补。"""
109
+ global _watchdog_started
101
110
  with _pool_lock:
102
111
  pending = _QUEUE.qsize()
103
112
  need = max(_target, 1) - _alive + pending
113
+ if not _watchdog_started:
114
+ _watchdog_started = True
115
+ threading.Thread(target=_watchdog, name="job-watchdog",
116
+ daemon=True).start()
104
117
  if need > 0:
105
118
  _resize()
106
119
 
107
120
 
121
+ def _watchdog():
122
+ """队列看门狗:周期把卡死的 queued 编排运行补回队列。
123
+
124
+ job 队列在内存里,任何一次入队丢失(2026-09-18 实案:r-20260918-211920
125
+ 排队 1 小时无人接手、进程未重启则启动补队永远不跑)都会让 UI 永远
126
+ 「排队中」。这里每分钟自愈一次;重复入队由 worker 出队守卫(非 queued
127
+ 跳过)与同任务单飞守卫兜底,幂等。巡检自身异常绝不退出。"""
128
+ import time as _t
129
+ while True:
130
+ _t.sleep(WATCHDOG_INTERVAL_S)
131
+ try:
132
+ requeue_pending(limit=6, max_age_s=WATCHDOG_STALE_S)
133
+ except Exception:
134
+ pass
135
+
136
+
108
137
  def cancel(run_id):
109
138
  ev = CANCELS.get(run_id)
110
139
  if ev:
@@ -155,11 +184,26 @@ def _task_active_run(task_id, exclude_run_id=None):
155
184
  return None
156
185
 
157
186
 
187
+ def _err_signature(err):
188
+ """错误串的稳定特征:剥掉章号/引用号/数字等易变片段,只留病根文本。
189
+ 同因连撞判定用——退避重跑后错误若还长得一样,说明墙没动(欠费/配置死),
190
+ 再多次退避也只是重复付费。"""
191
+ import re
192
+ s = str(err or "")
193
+ s = re.sub(r"第\s*\d+\s*章", "第N章", s)
194
+ s = re.sub(r"err_[0-9a-fA-F]+", "err_X", s)
195
+ s = re.sub(r"\d+", "N", s)
196
+ return re.sub(r"\s+", " ", s).strip()[:400]
197
+
198
+
158
199
  def _maybe_auto_resume(run_id):
159
200
  """连载任务失败自动续跑:继承已完成章继续,最多 AUTO_RESUME_MAX 次。
160
201
 
161
202
  真实长篇单次运行常因供应商拥堵超时中断;这里在 worker 收尾时自动重排一次
162
203
  续跑(store.retry_task 会带上 inherit),让整个流程真正无人值守。
204
+ 同因连撞止损:续跑副本再失败时与本次失败的错误签名比对(2026-09-18
205
+ 重写任务 kimi 403 欠费案),一模一样说明退避没换来不同结果,直接落
206
+ 终态写明死因,不再烧剩余的退避次数。
163
207
  """
164
208
  try:
165
209
  from . import store
@@ -173,6 +217,16 @@ def _maybe_auto_resume(run_id):
173
217
  return False # 用户主动取消的运行绝不自动续跑
174
218
  if int(run.get("auto_resumes") or 0) >= AUTO_RESUME_MAX:
175
219
  return False
220
+ prev_id = run.get("auto_resumed_from")
221
+ if prev_id:
222
+ prev = store.get_run(prev_id)
223
+ sig_now = _err_signature(run.get("error"))
224
+ if (prev and sig_now and prev.get("error")
225
+ and sig_now == _err_signature(prev.get("error"))):
226
+ store.update_run(run_id, auto_resume_stopped="same_cause",
227
+ error=(run.get("error") or "")
228
+ + "|自动续跑止损:连续两次失败原因相同,不再重试")
229
+ return False
176
230
  if _task_active_run(task["id"], exclude_run_id=run_id):
177
231
  return False # 同任务已有运行排队/在跑:再排副本只会与之撞车
178
232
  ok, err, new_run = store.retry_task(task["id"])
@@ -282,11 +336,43 @@ def _yield_duplicate(run_id):
282
336
  return False
283
337
 
284
338
 
285
- def requeue_pending(limit=10):
286
- """启动补队:队列在内存里,进程一死排队项就没人管了(2026-09-18
287
- 七猫 r-162724 排队僵尸案:续跑副本 created 后服务重启,Timer 随进程
288
- 蒸发,运行永远停在「排队中」)。重启后把近 24h 的遗留 queued 编排运行
289
- 重新入队;同任务已有在跑/排队的不重复补。返回补队条数。"""
339
+ def _age_s(run, now=None):
340
+ """run 已创建多少秒(created_at 解析失败返回 0:宁可早补,别误判卡死)。"""
341
+ import time as _t
342
+ now = now if now is not None else _t.time()
343
+ try:
344
+ ts = _t.mktime(_t.strptime(run.get("created_at") or "", "%Y-%m-%d %H:%M:%S"))
345
+ except Exception:
346
+ return 0.0
347
+ return max(0.0, now - ts)
348
+
349
+
350
+ def _in_resume_backoff(run, now=None):
351
+ """续跑副本是否还在退避等待窗口内(resume_enqueue_at 未到点)。
352
+
353
+ 到点时间由 _maybe_auto_resume 落在 run 上,Timer 到点才入队;巡检/补队
354
+ 若无视它直接重排,等于把 300s 网关退避窗口烧掉(立即重排撞同一堵墙)。"""
355
+ import time as _t
356
+ now = now if now is not None else _t.time()
357
+ at = str(run.get("resume_enqueue_at") or "")
358
+ if not at:
359
+ return False
360
+ try:
361
+ return _t.mktime(_t.strptime(at, "%Y-%m-%d %H:%M:%S")) > now
362
+ except Exception:
363
+ return False # 解析失败按「不在退避」处理:宁可入队,别让副本永远卡死
364
+
365
+
366
+ def requeue_pending(limit=10, max_age_s=None):
367
+ """把遗留的 queued 编排运行重新入队(启动补队与运行期巡检共用)。
368
+
369
+ 队列在内存里,进程一死排队项就没人管了(2026-09-18 七猫 r-162724
370
+ 排队僵尸案:续跑副本 created 后服务重启,Timer 随进程蒸发,运行永远
371
+ 停在「排队中」)。同任务已有在跑/排队的不重复补。
372
+
373
+ max_age_s:巡检模式只补「卡了超过该秒数」的,刚入队的正常排队不掺和;
374
+ None(启动模式)全量补。resume_enqueue_at 未到点的续跑副本两种模式都
375
+ 跳过——重启不该把退避窗口烧掉。返回补队条数。"""
290
376
  try:
291
377
  from . import store
292
378
  except Exception:
@@ -300,6 +386,10 @@ def requeue_pending(limit=10):
300
386
  continue
301
387
  if not run.get("task_id") or not _recent(run):
302
388
  continue
389
+ if _in_resume_backoff(run):
390
+ continue # 退避窗口内的续跑副本:到点 Timer 自会入队
391
+ if max_age_s is not None and _age_s(run) < max_age_s:
392
+ continue
303
393
  if _task_active_run(run["task_id"], exclude_run_id=run["id"]):
304
394
  continue # 同任务已有更活跃的运行,别再排一份
305
395
  _QUEUE.put({"kind": "orchestration", "run_id": run["id"],
@@ -326,13 +416,15 @@ def _worker():
326
416
  run_id = job.get("run_id")
327
417
  ev = cancel_event_for(run_id) if run_id else threading.Event()
328
418
  try:
329
- # 出队后再兜一次底:排队期取消(cancel 已直接落终态)的任务
330
- # 不再进流水线,避免 execute_run 又把 cancelled 改回 running。
331
- # 查不到的 run(如测试 mock)不拦,保持原行为。
419
+ # 出队后状态闸:非 queued 一律跳过。排队期取消的(cancel
420
+ # 直接落终态)不再进流水线;running/done/failed 的是看门狗
421
+ # 重排/双入队产生的重复副本——另一 worker 已在跑或已跑完,
422
+ # 再 execute_run 会把同一运行执行两次。查不到的 run(测试
423
+ # mock)不拦,保持原行为。
332
424
  if run_id:
333
425
  from . import store
334
426
  r0 = store.get_run(run_id)
335
- if r0 and r0.get("status") == "cancelled":
427
+ if r0 and r0.get("status") != "queued":
336
428
  continue # task_done 由 finally 统一收口,不能在此重复
337
429
  if job.get("kind") == "orchestration" and _yield_duplicate(run_id):
338
430
  continue # 同任务单飞:续跑副本让位(已落 cancelled)
@@ -365,7 +457,17 @@ def _worker():
365
457
 
366
458
  def workers_info():
367
459
  with _pool_lock:
368
- return {"target": _target, "alive": _alive}
460
+ return {"target": _target, "alive": _alive, "queued": _QUEUE.qsize()}
461
+
462
+
463
+ def _drain_test_queue():
464
+ """测试辅助:清空内存队列并结清未决 join(生产代码勿调)。"""
465
+ while True:
466
+ try:
467
+ _QUEUE.get_nowait()
468
+ _QUEUE.task_done()
469
+ except Exception:
470
+ return
369
471
 
370
472
 
371
473
  def _now():
@@ -1429,6 +1429,55 @@ def _sync_launch_model(entry, binding):
1429
1429
  return notes
1430
1430
 
1431
1431
 
1432
+ # kind(runner 调用族)→ catalog 条目 id:flow/编排智能体的 id 常是业务名
1433
+ # (draft-c12 等),CLI 身份在 kind 里;目录型智能体的 id 就是条目 id。
1434
+ _KIND_ENTRY = {"codex": "codex-cli", "claude": "claude-code",
1435
+ "qwen": "qwencode", "opencode": "opencode", "aider": "aider"}
1436
+ _RUNTIME_SYNC = {"fps": {}, "lock": threading.Lock()}
1437
+
1438
+
1439
+ def sync_runtime_config(agent):
1440
+ """运行前防线:把「CLI 绑定」页当前链首落进该 CLI 自家配置——与一键打开
1441
+ 同一套 _sync_launch_model 写盘,不改任何语义。
1442
+
1443
+ 绑定换供应商后 CLI 自家配置不会自愈,而同步过去只发生在一键打开:
1444
+ 2026-09-18 opencode 钉死讯飞 404 旧端点案——绑定早切到云知声,起跑撞旧
1445
+ 配置 2 秒 UnknownError,3 次续跑全灭。bind_agent 入口调用本函数即全覆盖
1446
+ (死链补位也在覆盖内:_dead_binding_substitute 内部同样走 bind_agent)。
1447
+
1448
+ 指纹缓存:同绑定(模型/供应商/端点/KEY/codex 段)只写一次盘,步骤级高频
1449
+ 调用近零成本,也不与用户手工编辑配置打架(绑定没变就不动);无绑定或
1450
+ 条目无配置段直接返回(测试空转,绝不碰真实家目录);任何失败静默放过
1451
+ ——防线不拦运行,配置错误会在步骤错误里现形。"""
1452
+ try:
1453
+ cand = [agent.get("id"), _KIND_ENTRY.get(agent.get("kind") or "")]
1454
+ entry = None
1455
+ for x in cand:
1456
+ if x:
1457
+ entry = catalog.by_id(x)
1458
+ if entry:
1459
+ break
1460
+ if entry is None or not (entry.get("config") or {}).get("path"):
1461
+ return
1462
+ from . import modelhub
1463
+ binding = modelhub.resolve_binding(entry["id"]) or {}
1464
+ model = (binding.get("model") or "").strip()
1465
+ env = binding.get("env") or {}
1466
+ if not model and not env:
1467
+ return # 没配链或链全死:没有可落的目标,绝不动用户配置
1468
+ # 指纹取解析结果整体:env 里带端点+KEY(provider 字典不含原始 KEY,
1469
+ # 换 KEY 必须可见),codex 段单独入纹
1470
+ fp = (model, repr(sorted(env.items())),
1471
+ repr(binding.get("codex_provider") or "")[:300])
1472
+ with _RUNTIME_SYNC["lock"]:
1473
+ if _RUNTIME_SYNC["fps"].get(entry["id"]) == fp:
1474
+ return
1475
+ _sync_launch_model(entry, binding)
1476
+ _RUNTIME_SYNC["fps"][entry["id"]] = fp
1477
+ except Exception:
1478
+ pass
1479
+
1480
+
1432
1481
  def launch(entry, open_browser=True):
1433
1482
  """一键打开:web 类后台起服务并自动开浏览器;console 类新开终端窗口跑交互 TUI。
1434
1483
 
@@ -1930,7 +1930,16 @@ def bind_agent(agent, difficulty="default"):
1930
1930
  """按绑定生成应用了供应商/模型覆盖的 agent 副本;无绑定时原样返回。
1931
1931
 
1932
1932
  binding_configured:该 CLI 是否配过绑定链(配没配与解析结果分开带出——
1933
- 执行层死链闸门只对「配过但全死」判失败,没配过的回落 CLI 本机默认)。"""
1933
+ 执行层死链闸门只对「配过但全死」判失败,没配过的回落 CLI 本机默认)。
1934
+
1935
+ 所有真实派发都经此函数(含死链补位),入口顺带做运行前配置同步防线:
1936
+ 绑定换供应商后 CLI 自家配置不会自愈(2026-09-18 opencode 钉死讯飞旧端点
1937
+ 案),这里把当前链首落进 CLI 自家配置。失败静默,不拦派发。"""
1938
+ try:
1939
+ from . import manager
1940
+ manager.sync_runtime_config(agent)
1941
+ except Exception:
1942
+ pass
1934
1943
  rid = agent.get("id")
1935
1944
  b = bindings().get(rid) or bindings().get(
1936
1945
  "codex-cli" if rid == "codex" else "claude-code") or {}
package/app/core/paths.py CHANGED
@@ -56,6 +56,7 @@ TASKS_DIR = DATA_DIR / "tasks"
56
56
  RUNS_DIR = DATA_DIR / "runs"
57
57
  USAGE_DIR = DATA_DIR / "usage"
58
58
  ERRORS_DIR = DATA_DIR / "errors"
59
+ PUBLISH_DIR = DATA_DIR / "publish" # 一键发布:台账/作品登记/流程选择器覆盖/浏览器 profile
59
60
  CATALOG_FILE = DATA_DIR / "catalog.json"
60
61
  ENABLED_FILE = DATA_DIR / "orchestration.json"
61
62