@mcd0luo/celes-worker-spawn
v0.3.4
Published
Celes 调度中心 DSH 插件:纯 JS 的 worker 会话拉起 + registry 巡检自动重派(去 shell 化替代 spawn-worker.sh / watcher.sh / respawn-w100.sh)
Readme
@mcd0luo/celes-worker-spawn
Celes 调度中心 DSH 插件:worker 会话拉起 + registry 巡检自动重派(纯 JS 去 shell 化)。
仅面向 DSH web profile 宿主使用(注入 sessions / agents / sessionPersistence /
webServer / tools / settings,经 ctx.get("sessionController") 走进程内官方 RPC + workspaceRegistry + agentPresets;0.1.1 宿主回退 ctx.get("apiProxy") 走调度中心内部 RPC(W248 适配)。
替代现役 bash 三件套(/server-center/worker-ops,git 真源):
| 现役 bash | 本插件对应 |
|---|---|
| spawn-worker.sh(bridge RPC 拉起,五步) | spawnWorker() / POST {prefix}/spawn / agent 工具 spawn_worker |
| 手工 rm -rf 会话目录(调度中心硬删) | agent 工具 delete_worker(文件级真删,插件内无 shell) |
| cat registry.tsv 看自己派的 worker | agent 工具 list_workers(domain = 发起者本会话;all:true 列全部) |
| watcher.sh(30s 巡检 + 失败重派 + DONE/FAILED 收口) | watch 巡检(config.watch.enabled 时 setInterval 驱动) |
| respawn-w100.sh(重启后重派) | 语义并入 watch(registry 持久化在盘,陈旧 RUNNING 行重启后判 gone → 自动重派);registry 行缺失时外部 POST {prefix}/spawn 复现 |
能力
1. 拉起(spawn-worker.sh 五步移植)
spawnWorker(wid, briefText, {title, workspace, provider, model, effort, agentPreset, reportTo, autoDelete}):
- 有
workspace→workspace.create拿 workspaceId(分组);否则跳过; sessions.create(payload ={workspaceId}或{cwd: workspace || workerBase},可选并入agentPreset预设字段)→ sessionId;sessions.rename→ 标题<wid>·<短名>(短名自动剥掉<wid>·前缀,防双前缀);provider+model齐 →sessions.selectModel(reasoningEffort缺省 = 配置默认);sessions.prompt(简报文本,mode: "queue")。
返回 {ok:true, sessionId, title, workspaceId, agentPreset?, autoDelete};任一步失败返回
{ok:false, step, error}(step 为该 RPC 步骤名)。agentPreset 为 session.create 响应回显的实际
生效预设 id(透传验证用);autoDelete 回显本次是否请求汇报后自动归档。强制三规则落地:
主动命名(标题)、工作区/分组、非裸无头会话。
2. agent 工具 spawn_worker(delete_worker / list_workers 见 §3.9/§3.10)
任意会话内 AI 直接调用:{wid, brief, title?, workspace?, provider?, model?, reasoningEffort?, agentPreset?, reportTo?, autoDelete?},
brief 为自包含简报文本。返回同 spawnWorker 结果,并附 sourceSession(调用方会话 id)标来源。
autoDelete/deleteAfterReport 为布尔可选项(deleteAfterReport 是 autoDelete 的兼容别名)。
3. HTTP API(挂在 dsh web webServer,默认 127.0.0.1 回环)
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | {prefix}/health | 健康检查(无鉴权,供重启探活) |
| POST | {prefix}/spawn | 拉起 worker(含可选 autoDelete) |
| GET | {prefix}/usage | 查询会话上下文/token 占用(?sessionId= 或 ?wid=) |
| POST | {prefix}/archive | 归档会话({sessionId} 或 {wid};DSH 无 session.delete,归档=隐藏+日志保留) |
| GET | {prefix}/presets | 枚举 DSH agent 预设(agentPreset.list) |
| POST | {prefix}/preset | 切换已存在会话的 agent 预设({sessionId, agentPreset};仅 blank 会话可切) |
POST /spawn body:{wid, brief?, briefFile?, title?, workspace?, provider?, model?, reasoningEffort?, agentPreset?, reportTo?, autoDelete?}
——reportTo/autoDelete 语义见 §3.6/§3.8;brief 文本优先;缺则读 briefFile(默认 ${workerBase}/workspaces/${wid}/.brief)。
provider/model/reasoningEffort 缺省取自配置默认。工作区图纸落盘(.brief/.taskname/
.workspace/.retries 初始化,镜像 spawn-worker.sh)供看门狗重派复用。除 /health 外的请求
在配置了 token 时需带 x-celes-center-token 头。
3.5 DSH agent 预设(agentPreset,可选)
spawnWorker / POST /spawn / 工具 spawn_worker 三入口均支持可选 agentPreset,原样透传给
session.create 的 payload(DSH 原生字段);缺省不传该字段 = 走 DSH 默认预设。内置预设 id 映射:
| 预设 id | 模式 |
|---|---|
| standard | 标准模式 |
| cordis | 创造模式 |
| minimal | 极简模式 |
| code | PTC 模式 |
预设仅是 session.create 的一次性入参(非持久全局设置),每个 worker 独立指定。
已存在会话的预设切换(W120 新增):POST {prefix}/preset body {sessionId, agentPreset} 走
宿主 agentPreset.select RPC。DSH 限制:仅 blank(未开始) 会话可切换——一旦会话已有
turn/start 事件即固定,宿主返回 agent-preset-locked。因此 worker 的预设应在创建时经
agentPreset 指定(三入口已透传);/preset 仅用于「先建会话、后切换」的 blank 场景。
GET {prefix}/presets 返回实际枚举(含 name/trust/isDefault/broken)。
3.6 worker 完成自动反馈(reportTo,可选)
spawnWorker / POST /spawn / 工具 spawn_worker 三入口均支持 reportTo(主会话 id)。
非空时,插件在投递简报前自动把下面一段追加到简报末尾(spawnWorker 内部注入,不改变
brief 原文本):
【完成反馈(自动注入)】任务完成后:
1) 完整报告写入 ${workerBase}/results/<wid>-<taskname>.md(Markdown:成果/验证/产出路径)。
2) 用 session_send_message 工具(target=<reportTo>)发一条完成消息:成功 → WORKER_<wid>_DONE <成果一句话 + 关键验证/产出路径>;失败 → WORKER_<wid>_FAILED <原因>。
3) (可选)用 report_usage 工具读取本会话上下文/token 占用(缺省即本会话),把占用信息附到报告末尾或回执括号/分号后缀中。- 工具入口缺省自动:
spawn_worker的 execute 在调用方未显式给reportTo时,自动用exec.agent.session.id(发起会话 id)——AI 拉起 worker 时回报自动回到发起会话; - HTTP 入口:body 可选
reportTo,有则注入,无则不注入(脚本/外部调用可自行决定); <taskname>用于结果文件名,含路径分隔符/控制字符时消毒为-(防注入工作区外路径);- 收口纪律与 LTS「worker-dispatch · 收口」一致:报告落
results/<wid>-*.md+ 日志末行WORKER_<wid>_DONE|FAILED回执,看门狗以该文件为交付物判定依据。
3.7 worker 上下文/token 占用上报(report_usage / usage,W120 新增)
数据源:宿主 session.list 每项自带的 projections.values(由 dsh-token-meter +
dsh-session-stats 两个投影单元随会话事件实时折合,本 profile 已挂载;冷会话经
sessionProjectionCache 读取)。四个键均为宿主权威数据,无需客户端配合:
| 键 | 内容 |
|---|---|
| tokenUsage | {uncachedInputTokens, outputTokens, cacheReadTokens, cacheWriteTokens} —— provider 报告的用量累计 |
| contextPressure | {pressureTokens?, projectedTokens?, contextWindow?} —— 最近一次请求的输入/缓存压力与上下文窗口 |
| contextBreakdown | {systemTokens, toolsTokens, messageTokens} —— 系统/工具/消息占用估计 |
| sessionStats | {turns, steps, llmMs, toolMs, ttftMs, ttftSteps, decodeMs, decodeTokens} —— 轮数/步数与耗时分桶 |
上报接口:
- 代理工具
report_usage:任一会话可调,{sessionId?}缺省 = 发起者本会话;返回{ok, sessionId, running, blank, agentPreset?, asOfSeq?, usage:{tokenUsage, contextPressure, contextBreakdown, sessionStats}}——worker 将其附到完成报告 /WORKER_<wid>_DONE回执即完成「上报给主会话」; - HTTP
GET {prefix}/usage?sessionId=<id>或?wid=<wid>(wid 经 registry extra 的sess=解析): 主会话/脚本可随时轮询任意 worker 的占用——「写入可查位置」。 - 完成反馈注入段(§3.6)已加第 3 条提醒 worker 使用该工具。
3.8 汇报完成后自动归档(autoDelete,W120 新增)
spawnWorker / POST /spawn / 工具 spawn_worker 支持布尔 autoDelete(别名 deleteAfterReport)。
设为 true 时:
- 拉起成功后落
.autoDelete侧车标记并登记 RUNNING 行(extra 带autodel=1); - watch 巡检到该行「会话结束 + 交付物在案 → DONE」时,自动调
workspace.archiveSession(sessionId)归档该新 worker 会话;成功/失败均记 watcher.log / alerts.log,registry extra 标注归档结果; - 重派(retryWorker)保留
.autoDelete语义(读侧车标记透传给新会话)。
DSH 限制(如实注明):宿主无 session.delete RPC;归档 = workspace.archiveSession:
会话从 web 侧边栏隐藏、日志与持久化保留、可逆(/archive 端点或对端 unarchive 恢复)。
存量 worker 不改动;autoDelete 仅作用于本次拉起的新会话,且只在「有交付物」时归档
(无交付物 → FAILED,不归档)。
3.9 硬删除 worker 会话(agent 工具 delete_worker,W720 新增)
文件级真删(不是 §3.8 的归档):会话日志目录连同 sidecar 登记一并从盘上删除,不可恢复。
只面向本插件自己拉起的 worker(wid)或显式 sessionId;{wid?, sessionId?, reason?} 三参可选,
sessionId 优先,否则用 wid 经 registry extra 的 sess= 解析;两者都无 → {ok:false, error:"wid or sessionId required"}。
执行顺序(隐藏优先,顺序即语义):
| 步 | 动作 | 说明 |
|---|---|---|
| 1 | 取消在跑 agent | ctx.get("agents").get(sessionId).cancel({kind:"user"},{keepInbox:true}) —— 与官方停止按钮同一调用;whenIdle() 最多等 5s,异常只告警 |
| 2 | 定位会话目录 | sessionPersistence.list() → header → sessionPersistence.locate(header) → loc.path 去最后一段 |
| 3 | 盖墓碑 | workspaceRegistry.archiveSession(sessionId)(官方 UI 唯一隐藏通道;此刻会话仍在持久化清单,必然可归档) |
| 4 | 真删目录 | rm(dir, {recursive:true, force:true})(node:fs/promises;本插件无 shell、零子进程,不用 rm -rf) |
| 5 | 摘工作区记账 | 对仍列该会话的 workspace 记录逐个 entity.detachSession(sessionId)(归档只隐藏、保留 sessionIds 槽位 = 复活向量) |
| 6 | 删投影缓存 | storageDomain.get("session_projcache").table("sessions").delete(sessionId)(fail-soft) |
| 7 | 清登记 | registry.tsv 删该 wid 行(复用 updateRegistry)+ 删 workspaces/<wid>/ 下 .brief/.reportTo/.autoDelete/.retries/.taskname/.workspace/.reportFile,目录空则删 |
| 8 | 审计双写 | watcher.log + alerts.log 各一条 delete_worker <wid> 已硬删除会话 <sid>(dir=..., detached=N) |
返回 {ok:true, wid, sessionId, dir, detached, cacheDropped, registryRow:"removed"|"absent", tombstoned}。
墓碑失败(workspaceRegistry 未注入 / 归档被拒)会写 alerts.log,但不阻断删除。
3.10 本 domain worker 列表(agent 工具 list_workers,W720 新增)
{all?, includeDead?},均缺省 false/true。
- domain 归属:registry extra 的
source=<发起会话id>(spawn_worker落行时写入)优先, 缺失回退report_to=<发起会话id>;调用者 id 取exec.agent.session.id。 缺省只列本会话自己派的 worker;all:true不过滤(列 registry 全部)。 - 每行
{wid, status, sessionId, title, workspace, startedAt, state}:title=<wid>·<taskname>(读workspaces/<wid>/.tasknamesidecar),workspace= registry extra 的ws=,state=running|gone|unknown(复用sessState(),对sessions.list一次性取回的结果逐行判态)。 - 排序:
startedAt倒序;includeDead:false只留state==="running"。 - 返回
{ok:true, domain:<调用者会话id|null>, all, count, workers:[...]}。
4. 巡检看门狗(watcher.sh 移植,watch.enabled 才启动)
每 watch.intervalMs(默认 30s)一轮:
- (0.3.1)ghost 回收:先扫一遍 registry,把「可证已死」的 RUNNING 行降级为 FAILED(§4.1);
0.5 (0.3.2 / W735)判活边界:
sessions.list只用来判本进程持有的行(extra 的proc=<本实例 pid>)。proc存在且 ≠ 本进程 pid(= 另一个 dsh-web 实例持有的行)与proc缺失(含 bash 遗留pid=行)一律不判活、不判 DONE、不重派,只计数留痕;会话列表 RPC 失败时整轮直接跳过(证据不足,绝不因抖动重派)。跨实例「持有者进程确证已死」的行只由步骤 0 的 ghost 回收判 FAILED(§4.2); - 读
${workerBase}/registry.tsv(TSV:wid、t0、status、extra…); - 对本进程持有的
status==RUNNING(DONE/FAILED* 跳过)行:从 extra 抽sess=xxx,sessions.list判blank/running/gone; running→ active;blank/gone且有交付物 →DONE(写 watcher.log + 重写 registry);blank/gone且无交付物 → anomaly;- anomaly:
age = now - t0 < graceMs(默认 10min,防新派会话瞬时 gone 误判)→ 跳过;.retries < maxRetries→ 自动重派(用.brief+.taskname/.workspace图纸重派,.retries+1,写 watcher.log / alerts.log + RUNNING 行重建);否则 →status=FAILED(写 watcher.log / alerts.log + registry 收口); - 交付物判定:
${workerBase}/results/<wid>-*.md存在(readdir + startsWith)或${workerBase}/coplanner/mailbox/coordinator/inbox.jsonl含"from":"<wid>"; - (W120)DONE 时若该行带
autodel=1/.autoDelete标记 →workspace.archiveSession自动归档 (§3.8),并把归档结果写进 registry extra 与 watcher.log / alerts.log。
registry 重写 = 跨进程 O_EXCL 锁 + 唯一 tmp 名 + rename 原子替换(§4.2;同 bash 的 tmp+mv, 但 tmp 名不再固定,且读改写整段进跨进程临界区)。
4.1 ghost 回收(迭代方向 F,0.3.1 新增)
问题:registry 由「进程」写入,但进程会死。死进程留下的 status=RUNNING 行(其进程号已不存在、
其 sess= 会话也已不可用)在跨进程 / 未来视图里就是幽灵:Studio UI 因为按本进程 ownEntries 过滤
看不见,registry 本身却越积越脏。
做法:每轮巡检开头跑一遍独立的 ghost 回收(lib/ghost.js,纯函数 + 依赖注入),只有能证明该行
已死时才动它:
| 判据 | 说明 |
|---|---|
| 行带进程号 token | proc=<pid>(0.3.1 起 spawnWorker/retryWorker 自动写入,值为宿主 dsh-web 进程号)或遗留 pid=<n>;pid=- 与无 token 一律不动 |
| 该进程确证已死 | process.kill(pid, 0) 抛 ESRCH(无 shell、零子进程);EPERM(进程在但无权)判活着,不动 |
| 会话确证 gone | sessions.list 查询成功、列表非空、且其中没有该 sess= 会话。查询失败 / 空列表 = 证据不足 → 不动(比 sessState 更严,杜绝 RPC 抖动误判) |
| 无交付物在案 | 交付物在案的行仍按原语义判 DONE,不动 |
| 已过宽限期 | now - t0 ≥ watch.graceMs,刚派出的行不与主巡检抢判 |
命中 → status=FAILED + extra 前置 看门狗 ghost 回收(<时刻>):proc=<pid> 进程已死 + 会话 gone + 无交付物
(原有 sess=/ws=/source= token 原样保留),并写 watcher.log + alerts.log 审计行。
落盘经 updateRegistry 锁内复检(盘上仍是 RUNNING 才降级),回收自身异常一律吞掉——绝不影响
DONE / 重派 / FAILED 既有语义。只改 status/extra:不杀进程、不重派、不删会话、不动 registry 其它行。
单测/冒烟:node test/ghost-reaper.smoke.mjs(43 项)、node test/ghost-reaper.wiring.mjs(10 项,
假 ctx + 假 registry + 假 pid 直接驱动真插件 apply())。
4.2 registry 跨进程写安全(0.3.2 / W735 新增)
问题(W735 审计):3180/3184/3190 三个 dsh-web 实例的 workerBase 同指
/server-center/runtime/worker-exec,registry.tsv 是跨进程共享可变状态,而旧实现
① 进程内 Promise 链只挡同进程并发;② tmp 名固定为 registry.tsv.tmp(两个实例同时写会互踩同一个
tmp → rename 后盘上可能是半截表)。真实两进程压测(test/registry-race.mjs,2×60 次并发读改写):
修复前 120 次写入只剩 19 行,修复后 120 行一行不丢。
做法(lib/reglock.js,全部 node 内建;无 shell、零子进程 —— 不 spawn flock(1)):
| 机制 | 细节 |
|---|---|
| 唯一 tmp 名 | <file>.tmp-<pid>-<seq>(同 crates/workers/src/registry.rs 的 pid+seq 命名),rename 原子替换;tmp 永不互撞 |
| 跨进程锁 | <file>.lock 的 O_CREAT|O_EXCL(fs.open(path,"wx"));锁文件内容 pid \t 时刻 \t token |
| 死持有者破拆 | 持有者进程 process.kill(pid,0) → ESRCH(确证已死)即破拆重试,不会因持有者崩溃永久死锁 |
| 空锁/残留破拆 | 无 pid 的空锁文件、以及本进程上次释放失败的残留,超 registryLock.staleMs(默认 30s)才破拆 |
| 活持有者 | 永不按年龄强拆;等到 registryLock.timeoutMs(默认 10s)后放弃本次写入并抛错(不丢行优先于写入成功,绝不退化成无锁读改写),调用方按非致命处理并写 alerts.log |
| 释放 | 校验 token 归属,绝不错删别人的锁 |
单测/冒烟(全部零 shell、零子进程):
node test/registry-concurrency.smoke.mjs # 42 项:唯一 tmp 名 / 丢行对照 / 锁语义 / 两实例并发 spawn / 拿不到锁即放弃写入
node test/watchdog-ownership.smoke.mjs # 25 项:判活边界(跨进程不重派、本进程才重派 / 判 DONE)
node test/preset-select-noref.smoke.mjs # 22 项:presetSelectRpc 无 live agent 的结构化错误
node test/ghost-reaper.smoke.mjs # 43 项(0.3.1 既有)
node test/ghost-reaper.wiring.mjs # 10 项(0.3.1 既有)
node test/provisioning-ledger.smoke.mjs # 19 项(0.3.3 / W847:先落盘后创建 + wid 永不复用)
node test/recovery-reconcile.smoke.mjs # 9 项(0.3.3 / W847:PROVISIONING 恢复对账,不重派)
node test/auto-receipt.smoke.mjs # 21 项(0.3.3 / W847:自动兜底回执幂等 + 不抢跑 + 重试)
# 真·跨进程:由外部并发启动同一脚本 2 份(脚本自身不起子进程)
BASE=$(mktemp -d); node test/registry-race.mjs A 60 "$BASE" & node test/registry-race.mjs B 60 "$BASE" &
wait; wc -l "$BASE/registry.tsv" # 期望 120同版顺带修(W735 第 3 条):presetSelectRpc 的「无 live agent」分支原读未定义变量 noref
(ReferenceError → 外层 catch 返回含糊的 "noref is not defined" 且丢掉 code);现返回
{ok:false, code:"agentPreset.select-refused", error:"session <id> has no live agent", detail}。
4.3 台账三件事 + 自动兜底回执(0.3.3 / W847 新增)
先落盘后创建:spawnWorker 在 sessions.create 之前先写一条 status=PROVISIONING 行
(extra 带 proc=<pid>、parent=、report_to=、brief=<sha1-12>);create 成功后立刻把 sess=
写回该行,随后转 RUNNING;任一后续步骤失败转 FAILED(extra 记 fail=<step>: <原因>)。
W847 起所有 spawn 都登记(此前只有 autoDelete 会登记);老格式行(RUNNING/DONE/FAILED、
pid=-、缺字段)解析路径不变。
wid 永不复用:对外入口(工具 / HTTP /spawn)若发现该 wid 在 registry 已有任何历史行
(含 DONE/FAILED/墓碑)→ 返回 {ok:false, step:"validate", error:"wid \"...\" 已占用(registry 已有历史行:<status>),名字永不复用"},
绝不覆盖/顺延;仅 retryWorker 内部重派传 reuse:true 复用同一 wid。
恢复对账(替代盲重派):巡检对未终结的 PROVISIONING 行只做对账、绝不重派——
本进程行超 watch.provisioningStaleMs、别的进程行其持有者 process.kill(pid,0) 确证已死时才处理;
先看 registry 的 sess=,缺失则按 parent= 匹配 sessions.list 的 parentSessionId(带 updatedAt ≥ t0);
child 存在 → RUNNING,确证 gone / 无匹配 → FAILED。对账本身不再调 spawn/create。
ghost.js 的 fail-safe 五条(无 token / 探活未知 / 会话未确证 gone / 交付物在案 / 宽限期内 一律不动)
reapGhosts() 已完整覆盖(lib/ghost.js:94-102、scanGhosts:117-147),本版无需改。
自动兜底回执(回执是运行时行为,不是提示词行为):worker 会话结算(实时态由 running 变
idle/gone)时,由插件的 runWatch 代发一条回执——不依赖 worker 执行三步契约:
| worker 自报路径 | 自动兜底路径 | 最终判定 |
|---|---|---|
| 报告 + session_send_message + results/<wid>.receipt-ok 三步齐全 | 看到 receipt-ok → 不抢跑,不发兜底回执 | delivered()(报告 + receipt-ok)→ DONE |
| 写了报告、但没写 receipt-ok(静默停/中断) | 报告在案 → 发 WORKER_<wid>_AUTO_DONE | DONE |
| 报告缺失(重试耗尽 / 无 reportTo / 会话 gone) | 终态 → 发 WORKER_<wid>_AUTO_FAILED(含具体原因 + 报告路径) | FAILED |
| worker 自己在途 | — | 会话仍 running → 不动,等结算 |
- 复用同一个
startWatch()/runWatch()定时器(config.watch.intervalMs),不新增第二个轮询; - 幂等:每条 RUNNING 行至多一条兜底回执,标记
results/<wid>.auto-receipt(dsh-web 重启不重发); - 可判别:
WORKER_<wid>_AUTO_DONE|AUTO_FAILED+ 正文首行「(自动兜底回执 · celes-worker-spawn 0.3.3 代发)」; - 投递:走既有
reportTo(session-前缀校验)经sessionController.prompt/ apiProxy;失败按watch.autoReceiptRetries重试并记 watcher.log / alerts.log,失败不写标记(下一轮自然重试); - 限制(如实):插件的数据边界是
session.list,读不到会话历史(DSH 会话日志是session.v3.jsonl.zstd多帧 zstd,零依赖不解析),故回执正文以「报告路径 + 终态 + 原因」代替 「最后一条 assistant 消息摘要」。
单测/冒烟:node test/provisioning-ledger.smoke.mjs(19 项)、node test/recovery-reconcile.smoke.mjs(9 项)、
node test/auto-receipt.smoke.mjs(21 项);假 ctx + 真 registry + 真 apply(),零 shell。
5. /tmp 卫生脚本(scripts/tmp-hygiene.mjs,0.3.1 新增)
白名单式只读扫描 + 可选执行(默认 dry-run,--apply 才删):
node /src/dsh_plugins/scripts/tmp-hygiene.mjs # 只列清单/统计,不删任何东西
node /src/dsh_plugins/scripts/tmp-hygiene.mjs --apply # 真删(逐条删前复检)
node /src/dsh_plugins/scripts/tmp-hygiene.mjs --all --min-age-hours=72红线:只认白名单里的目录(ts-*、playwright-*、celestea-*、tsx-*、vitest-*、
i18n-(test|datafolder)*、audit-sink-test*、wg-stubs*、nms-(fixture|closedloop)*)且 mtime
≥ --min-age-hours(默认 24h);符号链接一律跳过;同名普通文件只提示不删;隐藏条目与其余一切
未识别条目(含散落日志、随机名目录、socket)只统计、永不删除。删除前逐条 lstat 复检
(仍白名单、仍够老、仍非符号链接、dev/ino 未变),任何不一致即放弃。
配置
| 字段 | 默认 | 说明 |
|---|---|---|
| routePrefix | /celes-center/worker | HTTP 路由前缀 |
| token | "" | 共享访问令牌;空 = 仅信任本机回环 |
| workerBase | /server-center/runtime/worker-exec | worker 运行根(registry.tsv / workspaces / results / watcher.log / alerts.log) |
| defaultProvider | celestea | 默认模型 provider(替代 spawn-worker.sh 读 settings.yaml) |
| defaultModel | deepseek-v4-flash | 默认模型名 |
| defaultEffort | max | 默认推理档位 |
| watch.enabled | false | 是否启用巡检看门狗(观察期默认关闭) |
| watch.intervalMs | 30000 | 巡检间隔(毫秒) |
| watch.maxRetries | 2 | 自动重派次数上限(与 watcher.sh 的 .retries ≤ 2 一致) |
| watch.graceMs | 600000 | 新派 worker 宽限期(毫秒),期内异常不重派 |
| watch.autoReceipt | true | W847:worker 结算时由插件代发兜底回执(不依赖 worker 自觉) |
| watch.autoReceiptRetries | 3 | W847:兜底回执单轮投递重试次数(失败不写标记,下一轮再试) |
| watch.autoReceiptRetryMs | 300 | W847:兜底回执投递重试间隔(毫秒) |
| watch.provisioningStaleMs | 120000 | W847:本进程 PROVISIONING 行超时多久才对账(防与在飞 spawn 抢) |
| registryLock.staleMs | 30000 | 跨进程锁「空锁文件 / 本进程残留」破拆阈值(毫秒);活持有者持有的锁不受此值影响 |
| registryLock.timeoutMs | 10000 | 抢锁超时(毫秒);超时放弃本次写入以防丢行(不丢行优先) |
| registryLock.pollMs | 25 | 抢锁轮询间隔(毫秒) |
经 settings hub 注册(namespace celestea-center-worker-spawn,live 热更新;watch 改动会
按新配置重拨定时器)。
去 shell 说明
硬约束:禁止 shell —— 不 child_process / 不 exec / 不 bash / 不派生任何子进程。
全部能力由 node 内建模块承担:
node:fs/promises:readFile/writeFile/appendFile/readdir/mkdir/rename(registry 原子重写、日志追加、交付物探测、侧车图纸读写);node:path:join(一切路径从config.workerBase拼出,无硬编码 Linux 路径);- 调度中心 RPC 双后端(W248):0.1.2+ 宿主用
ctx.get("sessionController")直调官方 RPC(create/rename/selectModel/prompt/list/resolveAgent)+workspaceRegistry(create/archiveSession)agentPresets(remoteExportList/select),业务对象直接传参;0.1.1 宿主回退ctx.get("apiProxy")(workspace.create/sessions.create/sessions.rename/sessions.selectModel/sessions.prompt/sessions.list);网关方法为宿主侧 dispatch, 必须传完整 client-request envelope{type:"client-request", rpcId, method, payload}(宿主从request.payload取业务字段,缺 type/rpcId/method 会被静默拒绝),返回入参沿用ctx.get("apiProxy")原样透传;每条 RPC 结果都做检查(ok === false/ prompt 的accepted !== true→ 失败返回并 暴露真实拒绝原因);envelope 防御式解析const r = env?.result; return r?.value ?? r;;
- 定时巡检用
setInterval(非 cron/systemd 派生)。
跨平台:不依赖 date/awk/sed/grep/curl/python3;时间戳与 registry 解析全部内建实现
(t0 格式与 bash date +%F_%T 兼容互通)。
与 bash 版的已知偏差 / 边界
- 审计登记不落盘:watcher.sh 重派时调
dispatch/bin/audit-log.sh补登调度审计一条—— 纯 JS 插件内无法调用 shell 脚本,故改为与 alerts.log 同等告警(不写审计通道);如需要, 由外部 cron/调度按 alerts.log 补登记。 - workspace.create 失败即失败:bash 版在 workspace.create 拿不到 workspaceId 时会静默
回退
session.create(cwd=工作区);本插件按规格「任一步失败返回{ok:false, step, error}」 硬失败(更显式)。如需回退可自行改为先建会话。 - 默认模型:bash 版读
/opt/dsh/settings.yaml的agent-default-model;本插件改为 配置defaultProvider/defaultModel/defaultEffort(spec 指令)。 - 无工作区时的 cwd:bash 默认
/server-center;本插件按规格用workerBase。 - respawn-w100.sh 的「registry 无行重建」不覆盖:watch 只处理 registry 已登记行;
W100 重启恢复(registry 行缺失时)需外部用
POST {prefix}/spawn复现(body 见上)。 - registry/简报写入为尽力而为:
.brief/.taskname/.workspace/.retries落盘失败 不阻断拉起(RPC 主链路优先)。 - 观察期默认不启用巡检:
watch.enabled默认false,需显式配置打开(防误伤 b 三件套 退役前的观测窗口)。 - registry 锁是「约定式」而非文件系统强制锁(W735):本插件用 O_EXCL 锁文件
(
registry.tsv.lock),只有同样遵守该约定的写者才互斥;仍在写的 bashwatcher.sh/ Rustcrates/workers不取这把锁(各自 tmp+rename),跨生态并发写只靠「唯一 tmp 名 + 原子 rename」这一层兜底(本插件已不再使用固定 tmp 名)。三实例都升到 0.3.2 后风险才完全消除。
许可
MIT
