--- 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.mjs` CORS 桥。 命名对齐 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。 ```bash git clone 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` 秒级。 启动后访问: - 前端: - 健康检查:`curl http://localhost:3001/api/health` → `{"code":0,"msg":"ok",…}` ### 本地开发(绕开 Docker) ```bash 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: ```json { "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`** 轮。每轮: 1. 把上一轮 `judgeResult.rationale` + 0/1 分维度的 explanation 拼成 `=== JUDGE FEEDBACK ===` 段 2. 后端迭代记忆模块会合并重复问题、清理已修复维度,并生成下一轮累计反馈 3. 与 `optionsSnapshot.feedback`(用户输入)合并塞进下一轮 user prompt 4. 模型据此修订 `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` → PASS - **`strictFullMarks` + `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` / ...:录制涉及多个不同实体 ID - `unread_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`,模型必须遵守: 1. **ENTITY CONSISTENCY**:所有 group + rubric 引用的 calls 必须共享同一个 channel/guild/message ID。识别主线实体 = 最多写操作命中的那个 ID。 2. **NOISE BLACKLIST**:明确列出禁用工具,不能进 `recommended_groups.calls` 或 `checker_key`。 3. **ORDER CONSTRAINTS**: - `mark_*_read` 必须在 `get_unread_*` / `list_unread_*` 之前 - `create_*` 必须在 `list_*` / `get_*` 验证调用之前 - `send_message` 必须在 `list_messages` / `search_*_messages` 之前 4. **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 的所有数据: ```bash docker compose down rm -rf ./data/uploads/ ./data/exports/ sqlite3 ./data/tasks.db "DELETE FROM items WHERE job_id=''; DELETE FROM jobs WHERE id='';" docker compose up ``` --- ## 验收清单 1. `docker compose up --build` 一条命令启动成功 2. `http://localhost:3001` UI 显示正常 3. 单 API ZIP → 分析 → 下载 `*.task-package.zip`:`groupMode: "api"` + `apiCalls` 完整对象 4. 单 MCP ZIP → 同上:`groupMode: "mcp"`,tool name 严格保留,无跨实体 / 噪音 / 顺序问题 5. 一次拖 N 个混合 ZIP → 按 `concurrency` 并发处理,可中途取消 6. 杀容器再 `docker compose up` → 队列从 SQLite 恢复,未完成 item 状态可见 7. 主模型 apiKey:前端填入或选择个人保存的 Base URL + Key → 浏览器直连立即生效 8. devtools Network 抓包:首页任务润色直接命中所选模型 Base URL;API/MCP 分组只命中 `localhost:3001/api/*` 9. 严格满分开关:开启 → 须 `total = max_score`;关闭 → `total >= 10`;同时开启“忽略 Complexity”时允许 `TASK COMPLEXITY=1/2` 10. 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` 环境变量(基于 undici `ProxyAgent`)。 - **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 仍后端独控。 --- ## 许可 私有项目,未开源。