title: RL Auto
sdk: docker
app_port: 3001
pinned: false
RL-Auto / Task优化
本地部署的 RL 任务包生成 + 校验 工具。从 echo-extension 录制包(.zip)出发,自动产出可直接导入插件评测的任务包:Instruction、Recommended Groups、Rubrics、Checker Catalog、DB Diff Criteria、Pass Policy、Golden Trajectory。
当前架构:Docker 单端口前后端分离。
docker compose up --build是唯一启动方式。 已退役:start.command/ Python static server /proxy.mjsCORS 桥。
命名对齐 echo-extension-05061347 — subtask 分组模式:
- API 分组模式:按 HTTP endpoint 分组,输出
groupMode: "api"+ 完整apiCalls对象 - MCP 分组模式:按 MCP Tool 分组,输出
groupMode: "mcp"+ 原始 tool 名(不会被改写为短名)
模式由后端在上传时根据 ZIP 内容自动识别(含 mcp_calls.json / session.json → mcp,否则 → api)。前端也可在 optionsSnapshot.requestedMode 中显式覆盖。
工程结构
.
├── apps/
│ ├── api/ # Node + TS 后端 (Fastify + better-sqlite3 + undici)
│ │ └── src/
│ │ ├── server.ts # 启动 + 静态托管 web-legacy + Fastify 路由
│ │ ├── routes/{health,analysis}.ts
│ │ ├── queue/ # Store 抽象 + SQLite/Postgres 仓储 + worker pool
│ │ ├── analyzers/detect.ts # ZIP → mode 自动识别
│ │ ├── ai/client.ts # 共享 AI client,多 provider 兼容
│ │ ├── judge/{runner,prompt}.ts # 6 维 Judge + pass policy
│ │ ├── config.ts # .env 解析
│ │ └── log.ts # pino,自动屏蔽 apiKey/Authorization
│ └── web-legacy/ # 当前 SPA(React + Vite,构建后由 Fastify 托管)
│ ├── src/ # 任务润色 + API/MCP 分组页面
│ ├── index.html
│ └── package.json
├── packages/
│ └── core/ # 平台无关纯逻辑(TS, ESM)
│ └── src/
│ ├── importers/{api,mcp}.ts # 解析录制 ZIP → Recording
│ ├── prompts/{shared,api,mcp}.ts # 系统 prompt + slim evidence
│ ├── rules/{api,mcp}.ts # 本地规则 flag(DSL 校验 + trajectory 卫生)
│ ├── rl-env.ts # buildTaskPackage()
│ └── zip-export.ts # 写出最终 *.task-package.zip
├── data/ # Docker volume:SQLite + 上传/导出(已 .gitignore)
├── docs/ # 长文档/设计资料(新增文档优先放这里)
├── Dockerfile # 多阶段 node:20 + pnpm,构建 web/core/api
├── docker-compose.yml
├── .env.example
└── pnpm-workspace.yaml
slack-cli/ 与 discord-cli/ 是独立 npm 子项目,不在本工程范围内。
本地目录卫生
- 源码只放这些地方:
apps/、packages/、根目录配置文件、README.md。新增长文档优先放docs/,避免根目录继续变胖。 - **运行数据只放
data/、.data/或.local-data/**:上传 ZIP、导出包、SQLite 文件都属于本地运行状态,已被.gitignore忽略。 - 构建产物不入库:
apps/api/dist/、packages/core/dist/、coverage/、.vitest/、tmp/都是可再生成内容。 - 本地私密配置不入库:
.env、.claude/保持本机私有;提交前只看.env.example。 - 清理命令:用
pnpm clean删除 TypeScript 构建产物;系统垃圾如.DS_Store、nul、%USERPROFILE%*已在.gitignore中兜底。
快速启动
需要 Docker(Desktop 或 Engine)。不再依赖 macOS start.command、Python static server 或浏览器 Babel/CDN。
git clone <repo>
cd Rl-Auto
cp .env.example .env
# 编辑 .env:填入 DEFAULT_API_KEY / JUDGE_API_KEY / RUBRIC_GEN_API_KEY
docker compose up --build
首次构建 3-5 分钟(拉镜像 + pnpm 装依赖 + 编译 TS)。后续 docker compose up 秒级。
启动后访问:
- 前端:http://localhost:3001/
- 健康检查:
curl http://localhost:3001/api/health→{"code":0,"msg":"ok",…}
本地开发(绕开 Docker)
pnpm install
pnpm build
mkdir -p .local-data/uploads .local-data/exports
SQLITE_PATH=$PWD/.local-data/tasks.db \
UPLOAD_DIR=$PWD/.local-data/uploads \
EXPORT_DIR=$PWD/.local-data/exports \
PORT=3001 pnpm dev:api # tsx watch,改 TS 自动重启
前端是 Vite 构建产物,由 Fastify 从 apps/web-legacy/dist/ 托管。修改前端源码后重新运行 pnpm --filter @task-optimizer/web-legacy build,再刷新浏览器。
配置说明
配置分两类:后端 .env(部署时设定) 和 前端设置面板(用户运行时改)。API/MCP 分组启动 job 时会随 optionsSnapshot 发送前端值,主模型 Key 留空则回退到 .env 默认值;首页任务润色固定浏览器直连,必须使用前端填写或个人设置保存的 Key。
后端 .env
| 变量 | 说明 | 默认 |
|---|---|---|
PORT |
监听端口 | 3001 |
LOG_LEVEL |
pino 日志级别 | info |
DEFAULT_MODEL |
主分析模型默认值 | deepseek-v4-pro |
DEFAULT_BASE_URL |
主分析模型默认 base URL | https://api.deepseek.com |
DEFAULT_API_KEY |
主分析模型密钥(前端可覆盖) | (必填) |
DEFAULT_API_MODE |
chat 或 responses |
chat |
JUDGE_MODEL / JUDGE_BASE_URL / JUDGE_API_KEY / JUDGE_API_MODE |
Judge 模型配置(只在后端) | gpt-5.5 / https://app-hk.ppapi.ai/v1/chat/completions |
RUBRIC_GEN_* |
Rubric 自动补全模型(后端专用) | gpt-5.5 / https://app-hk.ppapi.ai/v1/chat/completions |
STORE_BACKEND |
sqlite 或 postgres;多实例部署必须使用 postgres |
sqlite |
DATABASE_URL |
PostgreSQL 连接串;存在时后端自动使用 Postgres store | (空) |
SQLITE_PATH |
SQLite 路径(仅本地/单实例推荐) | /app/data/tasks.db |
UPLOAD_DIR / EXPORT_DIR |
上传 / 导出目录 | /app/data/{uploads,exports} |
MAX_UPLOAD_MB |
单文件最大尺寸 | 100 |
MAX_BATCH_FILES |
单批最大文件数 | 20 |
ITEM_MAX_ATTEMPTS |
单个 item 的 transient 失败自动重试上限 | 3 |
GLOBAL_LLM_CONCURRENCY |
单进程 LLM 调用并发上限;0 表示不额外限制 |
0 |
UPSTREAM_PROXY |
出站代理 URI(替代旧 proxy.mjs),如 socks5://127.0.0.1:1080 或 http://... |
(空) |
前端设置面板(持久化在浏览器 localStorage)
| 字段 | 说明 |
|---|---|
ruleProfile |
echo-05061347 / rl-env-4.30 / echo-04281753 |
polishTransport |
首页任务润色调用路径;当前固定为 browser 浏览器直连 |
analysisTransport |
API/MCP 分组路径;当前固定为 backend 队列 |
apiMode |
chat / responses |
baseURL |
主分析模型 endpoint |
apiKey |
浏览器直连 API Key |
savedModelCredentials / selectedModelCredentialId |
个人保存的 Base URL + Key 列表,以及当前选中的连接 |
modelPreset / customModel |
主模型名 |
concurrency |
单 job 内并发 worker 数(1-8) |
temperature / topP / maxTokens |
主模型采样参数 |
enableThinking / clearThinking |
主模型 thinking 行为开关 |
optimizeTask / feedback |
任务润色开关 + 用户反馈文本 |
strictFullMarks |
严格满分开关(影响 pass policy) |
ignoreTaskComplexityForFullMarks |
严格满分时允许 TASK COMPLEXITY=1/2,只要求其他 5 个 Judge 维度满分 |
judge.{baseURL, model, promptTemplate} |
Judge 配置(仅 baseURL/model/template 可前端改;apiKey 仍只在 .env) |
rubricGeneration.{baseURL, model} |
Rubric 自动补全配置(同上) |
关于密钥与 localStorage:首页任务润色固定走
browser直连,必须填写浏览器 API Key,或在个人设置里选择已保存的 Base URL + Key。主模型apiKey和savedModelCredentials会按用户操作保存到浏览器 localStorage。API/MCP 分组默认走后端队列。Judge / Rubric Generation 的 apiKey 仍然只在后端.env,从不出现在前端代码或响应里。
后端 API 契约
所有响应使用统一 envelope:
{ "code": 0, "msg": "ok", "data": { /* ... */ } }
非 200 时 code 为非 0 整数,msg 描述错误。
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/health |
健康检查 |
POST |
/api/polish |
首页任务润色接口;body 为 { raw, options? },返回 { zh, en, notes } |
POST |
/api/analysis/jobs |
multipart 上传 1..N 个 .zip,自动识别 mode;返回 { jobId, items[] } |
GET |
/api/analysis/jobs/:jobId |
当前 job + 所有 item 状态 |
POST |
/api/analysis/jobs/:jobId/start |
开始处理;body 含 optionsSnapshot |
POST |
/api/analysis/jobs/:jobId/cancel |
当前 in-flight item 跑完后停后续 |
POST |
/api/analysis/jobs/:jobId/items/:itemId/retry |
重置单项为 queued 重新跑 |
GET |
/api/analysis/jobs/:jobId/items/:itemId/download |
流式下载该项 *.task-package.zip |
GET |
/api/analysis/jobs/:jobId/download-all |
流式打包所有成功项 → all-results-{jobId}.zip |
- Item status:
queued | running | judging | completed | failed | stopped - Job status:
pending | running | completed | cancelled
处理流水线
每次上传 ZIP → 一个 item,每个 item 流程如下:
parse (importer) ← packages/core/importers/{api,mcp}.ts
↓
buildEvidence + runLocalRules ← packages/core/rules/{api,mcp}.ts
↓
┌─────────────────────────────────────────────────────────┐
│ for iter = 1..MAX_JUDGE_ITERATIONS (=5): │
│ buildSlimEvidence + buildUserPrompt │
│ ↓ (上一轮 judge 反馈拼进 prompt) │
│ 主模型 requestJson → analyzerResult │
│ ↓ │
│ buildTaskPackage (含 golden_trajectory) │
│ ↓ │
│ runJudge → 6 维评分 + verdict │
│ ↓ │
│ isJudgeTargetSatisfied? │
│ ✓ → break (PASS) │
│ ✗ → 更新迭代记忆,下一轮带着累计修正继续 │
│ │
│ 跟踪 best score;每轮快照 evidence/task-package/judge │
│ 落盘到 data/exports/{jobId}/{itemId}/ │
└─────────────────────────────────────────────────────────┘
↓ (PASS) → buildAnalysisZipBuffer → output.zip
↓ (5 轮仍未通过) → 写入 best 那次产物 + 失败信息
Judge 迭代
apps/api/src/queue/runner.ts 内每个 item 最多跑 MAX_JUDGE_ITERATIONS = 5 轮。每轮:
- 把上一轮
judgeResult.rationale+ 0/1 分维度的 explanation 拼成=== JUDGE FEEDBACK ===段 - 后端迭代记忆模块会合并重复问题、清理已修复维度,并生成下一轮累计反馈
- 与
optionsSnapshot.feedback(用户输入)合并塞进下一轮 user prompt - 模型据此修订
recommended_groups/recommended_rubrics/recommended_instruction
通过条件(isJudgeTargetSatisfied):
- 默认:
total_score >= 10 AND has_zeros === false→ PASS strictFullMarks开启:total_score === sum(rubrics[].max_score) AND has_zeros === false→ PASSstrictFullMarks+ignoreTaskComplexityForFullMarks开启:除task_complexity外其他 5 个维度都为2/2,且task_complexity >= 1/2→ PASS
满 5 轮仍未通过:用最高分那次的产物写持久化,抛错带 "已迭代 N/5 轮,最佳 X 分"。
每轮快照:data/exports/{jobId}/{itemId}/{evidence,task-package,judge-result,iteration-memory}-iter-N.json,便于追溯。
并发模型
Job (上传 5 个 ZIP)
├─ Worker #1 ─→ takeNextQueuedItem() → item A → 5 轮 judge 迭代 → export
├─ Worker #2 ─→ takeNextQueuedItem() → item B → 5 轮 judge 迭代 → export
├─ Worker #3 ─→ takeNextQueuedItem() → item C → ...
└─ ... (worker 数 = optionsSnapshot.concurrency, 1-8)
- per-job 内并发:
concurrency个 worker 并行 drain 同一 job 的 queue。SQLite 本地模式使用事务原子抢取;Postgres 多实例模式使用FOR UPDATE SKIP LOCKED,避免不同 API 实例重复消费同一个 item。 - 跨 job 并发:旧版有
workerLock跨 job 串行链,已撤销。多用户/多次上传可同时跑。 - 硬上限:
MAX_CONCURRENCY = 8(apps/api/src/queue/runner.ts),保护上游模型 API rate limit。 - LLM 全局上限:
GLOBAL_LLM_CONCURRENCY可限制单进程内 analyzer/judge 同时调用模型的数量。 - 失败重试:
ITEM_MAX_ATTEMPTS只重试 transient AI/network 错误;ZIP 格式错误、严格 MCP tools 缺失、Judge 质量未达标会直接 failed。 - 配置:前端"并发数"输入框(设置面板),默认 3。
MCP 严格 trajectory 卫生
为避免 Judge 在 Trajectory-Task Alignment 维度扣分(典型问题:跨 channel 的 list_messages、start_typing 噪音、get_unread 出现在 mark_read 之前),有三层防御:
第 1 层:本地规则 flag(packages/core/src/rules/mcp.ts)
runMcpRules 主动检测并 flag:
cross_entity_channelId/cross_entity_guildId/cross_entity_messageId/ ...:录制涉及多个不同实体 IDunread_check_before_mark_read:get/list_unread出现在mark_*_read之前noise_tools_present:背景调用(typing/presence/heartbeat/auth/telemetry/...)
第 2 层:evidence 过滤(isSignificantMcpCall)
Slim evidence 阶段直接剔除噪音工具,模型甚至看不到:
login | logout | refresh_token | get_session | whoami | get_current_user | get_self | me | default_config
start_typing | stop_typing | set_typing | presence | heartbeat | ping
get_pending_* | list_pending_* | telemetry_* | page_load | page_view | init_* | bootstrap_*
*_typing_* | auth_* | session_* | *_telemetry | *_heartbeat | *_ping
第 3 层:System prompt 强约束(MCP_TRAJECTORY_RULES)
写进 MCP_SYSTEM_PROMPT,模型必须遵守:
- ENTITY CONSISTENCY:所有 group + rubric 引用的 calls 必须共享同一个 channel/guild/message ID。识别主线实体 = 最多写操作命中的那个 ID。
- NOISE BLACKLIST:明确列出禁用工具,不能进
recommended_groups.calls或checker_key。 - ORDER CONSTRAINTS:
mark_*_read必须在get_unread_*/list_unread_*之前create_*必须在list_*/get_*验证调用之前send_message必须在list_messages/search_*_messages之前
- STRAY-CALL REJECTION:违反以上的 stray 调用一律剔除,可记入
evidence_limits。
输出 ZIP 格式
输出 ZIP 与上传 ZIP 完全同结构,仅 task.json 被重写为含完整任务包字段:
- 顶层保留 echo-extension 兼容字段:
metadata、task.instruction、task.networkRequests、task.subtasks、task.rubrics - 顶层嵌入 RL 字段:
rule_profile、rubric_checkers、dbdiff_criteria、pass_policy、golden_trajectory、network、validation - API 模式:
task.subtasks[*].groupMode === "api",apiCalls是完整对象({ id, name, type, input, output, metadata }) - MCP 模式:
task.subtasks[*].groupMode === "mcp",tool name 严格保留 network.json/mcp_calls.json/session.json/dbdiff.json等按原始字节保留
数据持久化
./data/ 是 Docker volume mount 目标:
data/
├── tasks.db # SQLite jobs/items
├── uploads/{jobId}/{itemId}.zip # 上传原始
└── exports/{jobId}/{itemId}/
├── evidence.json # 最终(best)证据
├── task-package.json # 最终任务包
├── judge-result.json # 最终 judge 结果
├── evidence-iter-N.json # 每轮迭代快照
├── task-package-iter-N.json
├── judge-result-iter-N.json
├── output.zip # 仅 PASS 时生成
└── error.txt # 失败时
清理某个 job 的所有数据:
docker compose down
rm -rf ./data/uploads/<jobId> ./data/exports/<jobId>
sqlite3 ./data/tasks.db "DELETE FROM items WHERE job_id='<jobId>'; DELETE FROM jobs WHERE id='<jobId>';"
docker compose up
验收清单
docker compose up --build一条命令启动成功http://localhost:3001UI 显示正常- 单 API ZIP → 分析 → 下载
*.task-package.zip:groupMode: "api"+apiCalls完整对象 - 单 MCP ZIP → 同上:
groupMode: "mcp",tool name 严格保留,无跨实体 / 噪音 / 顺序问题 - 一次拖 N 个混合 ZIP → 按
concurrency并发处理,可中途取消 - 杀容器再
docker compose up→ 队列从 SQLite 恢复,未完成 item 状态可见 - 主模型 apiKey:前端填入或选择个人保存的 Base URL + Key → 浏览器直连立即生效
- devtools Network 抓包:首页任务润色直接命中所选模型 Base URL;API/MCP 分组只命中
localhost:3001/api/* - 严格满分开关:开启 → 须
total = max_score;关闭 →total >= 10;同时开启“忽略 Complexity”时允许TASK COMPLEXITY=1/2 - Judge 失败时自动迭代下一轮,最多 5 轮,每轮都会更新后端迭代记忆并写进下轮 prompt(容器日志可见
Iteration start/Judge target not yet satisfied)
故障排查
| 现象 | 排查 |
|---|---|
docker compose up 卡在装 better-sqlite3 |
Dockerfile 已含 native deps;本地 pnpm install 失败需先装 Xcode CLT (xcode-select --install) |
/api/health 返回 502 / 连接拒绝 |
docker compose ps 看 service 状态;docker compose logs -f app 查启动日志 |
| 大 ZIP 上传 413 | 调大 MAX_UPLOAD_MB 后重启容器 |
| AI 调用失败 401 / 429 | 检查相应 KEY;后端日志会用安全 preview 显示模型返回(自动屏蔽 Authorization / apiKey) |
| Judge 一直 NEEDS_REVISION 跑满 5 轮 | 看 data/exports/{jobId}/{itemId}/judge-result-iter-N.json 和 iteration-memory-iter-N.json,找出反复失败的维度;考虑改 optionsSnapshot.feedback 给模型更针对性的指令 |
| MCP Trajectory-Task Alignment 仍 ≤ 1 | 检查录制是否真有跨 channel 的杂讯 / 噪音工具未被剔除(evidence-iter-N.json 里 slim.callInstances) |
| 主模型 apiKey 改了不生效 | 浏览器刷新;确认设置面板里 "浏览器 API Key" 输入框值已保存,或重新选择个人设置里的连接 |
设计决策与历史
- **退役
proxy.mjs**:原本浏览器直连模型 endpoint 通过 Node CORS proxy 中转。后端化之后,所有 AI 调用走apps/api/src/ai/client.ts,不需要 CORS。如需出站代理(公司网络)用UPSTREAM_PROXY环境变量(基于 undiciProxyAgent)。 - Pass policy 阈值从 9 升到 10:旧 echo-extension Judge prompt 里 PASS=9,但实际经验下 9 分包仍有较多噪音。运行时由后端
isJudgeTargetSatisfied决定,模型 prompt 不变。 - iteration 循环搬到后端:原先在 vanilla JS 前端做;后端化后,前端只负责 polling 状态,循环完整运行在
runner.ts。 - 主模型 apiKey 前端化:早期为防止 localStorage 泄漏,所有 apiKey 都不允许前端持有。现在主模型 key 已开放(用户自主选择),Judge / Rubric Generation 的 key 仍后端独控。
许可
私有项目,未开源。