# AiWorker **Repository Path**: windvane/ai-worker ## Basic Information - **Project Name**: AiWorker - **Description**: 个人 AI Agent 助手 — 多智能体协作 + MCP + Skills + Hooks + 自进化 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: main - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-08-08 - **Last Updated**: 2026-10-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # AiWorker > 个人 AI Agent 助手 → AI OS — 多智能体协作 + MCP + Skills + Hooks + 自进化 ![version](https://img.shields.io/badge/version-1.12.3-blue) ![node](https://img.shields.io/badge/Node-%3E%3D22-339933) ![typescript](https://img.shields.io/badge/TypeScript-5.x-3178C6) ![license](https://img.shields.io/badge/license-MulanPSL2.0-green) ![ui](https://img.shields.io/badge/UI-TUI%2BWeb-8b5cf6) 一套运行在本地的个人 AI Agent 助手:多专家智能体按任务自动路由,支持工具调用、MCP、技能库、生命周期 Hook、三层记忆与上下文压缩。提供 **TUI 终端** 与 **Web UI** 两种界面。 已升级为 **AI OS**(个人 AI 操作系统)**1.0**:AI 是大脑、Harness 是手脚、应用是进程、自进化引擎闭环、每会话项目目录——正式架构见 [docs/AiWorker架构.md](docs/AiWorker架构.md),逐期演进见 [plans/sprint-history.md](plans/sprint-history.md)。 ## 目录 - [系统总览](#系统总览) - [界面预览](#界面预览) - [作品展示](#作品展示) - [快速开始](#快速开始) - [交互界面](#交互界面) - [项目结构](#项目结构) - [配置](#配置) - [插件开发](#插件开发) - [测试与开发](#测试与开发) - [技术栈](#技术栈) - [相关文档](#相关文档) - [许可证](#许可证) ## 系统总览 > 一张图看完能力地图:界面接入 → Agent 内核调度 → Harness 能力(工具 / 技能 / 权限 / 回滚)→ AI OS 自动化与自进化。 ```mermaid flowchart TB subgraph UI["界面与接入"] direction LR TUI["TUI 终端
自研帧缓冲渲染
回合块折叠 · OSC8 产物可点击"] WEB["Web UI
Svelte 5 · SSE/WS 实时
暗色模式 · 系统面板"] API["HTTP API
/api/v1 · 托管 Web UI"] HEAD["headless -p
text / json / stream-json
稳定退出码 · 可进 CI"] TUI --- WEB --- API --- HEAD end subgraph CORE["Agent 内核"] direction LR ROUTE["专家路由
内置专家自动选择
正则 → LLM 语义"] LOOP["agent-loop
流式 · 中断 · 迭代预算
空转/失败自动终止"] TEAM["协作模式
/plan DAG 并行
/debate 双专家互审"] CTX["上下文管理
三层记忆 · 事件溯源
压缩 · 超长结果 spill"] ROUTE --- LOOP --- TEAM --- CTX end subgraph HARNESS["Harness 能力层"] direction LR TOOLS["工具面
内置工具 · MCP(stdio/HTTP)
插件 setup(ctx) · scope 注册
产物预览:文件 / 链接 / diff"] SKILL["技能库
SKILL.md · 触发词
/技能名 · 使用中自沉淀"] SEC["权限与沙箱
Ask / Plan / Auto · Tool(specifier) 规则
fail-closed 审批 · 写入根约束"] CKPT["检查点回滚
每轮快照 · /rewind
代码 / 对话 / 两者 · 冲突保护"] TOOLS --- SKILL --- SEC --- CKPT end subgraph OS["AI OS 自动化"] direction LR JOB["后台与调度
/bg 后台任务
/schedule 自然语言定时"] APP["应用运行时
manifest 多形态 · 生命周期
能力桥(无 terminal) · 崩溃重启"] GEN["应用即时生成
自然语言 → 应用
生成队列 · 安装运行"] EVO["自进化引擎
观察 → 提议 → 两段式确认
用例评测 · 回归自动回滚"] JOB --- APP --- GEN --- EVO end subgraph DATA["模型与数据底座"] direction LR MR["ModelRouter
多 profile 路由 · 思考模式
窗口配置 · /config 热切换"] DB[("SQLite WAL + FTS5
会话 · 审计 · 检查点")] SCOPE["作用域与资产
每会话项目目录
.aw 资产包 · 裸格式导入导出"] MR --- DB --- SCOPE end UI -->|"对话 / 命令"| CORE CORE -->|"工具调用"| HARNESS CORE -->|"任务派发"| OS HARNESS -->|"审计与快照"| DATA OS -->|"台账与用例"| DATA SEC -.->|"门禁"| TOOLS ``` ## 界面预览 ![TUI 终端 — 问候](docs/screenshots/tui_hello.png) ![TUI 终端 — 工具调用](docs/screenshots/tui_tool_call.png) ![Web UI — 演示 1](docs/screenshots/web_ui_demo1.png) ![Web UI — 演示 2](docs/screenshots/web_ui_demo2.png) ![Web UI — 演示 3](docs/screenshots/web_ui_demo3.png) ![Web UI — 演示 4](docs/screenshots/web_ui_demo4.png) ![Web UI — 演示 5](docs/screenshots/web_ui_demo5.png) ## 作品展示 用 AiWorker 生成的 Web 小作品(HTML 单文件,浏览器直接打开;`docs/demos/`): | 作品 | 预览 | 演示 | |---|---|---| | 黑洞模拟 | ![blackhole](docs/demos/blackhole.png) | [blackhole.html](docs/demos/blackhole.html) | | 海上日落 | ![ocean-sunset](docs/demos/ocean-sunset.png) | [ocean-sunset.html](docs/demos/ocean-sunset.html) | | 星舰设计 | ![starship](docs/demos/starship.png) | [starship_design.html](docs/demos/starship_design.html) | ## 快速开始 环境要求:Node.js >= 22,需要 `DEEPSEEK_API_KEY`。 ```bash # 安装依赖(根目录 + web/) npm install cd web && npm install && cd .. # 方式一:TUI 终端 npm run dev -- --dir /path/to/project --mode auto # 方式二:HTTP Server + Web UI npm run dev -- --server --port 3000 # 启动 API(同进程托管 Web UI) npm run web:dev # 另开终端:Web UI 开发模式 :5173 ``` ### 环境变量 | 变量 | 用途 | 是否必需 | |------|------|---------| | `DEEPSEEK_API_KEY` | 默认模型(DeepSeek) | 必需 | | `OPENAI_API_KEY` | lite 本地模型(`localhost:8000`) | 可选 | > 其他模型供应商可在 `config/models.json` 或 `/config add-model` 中添加,key 用 `${ENV_VAR}` 引用。 ### CLI 参数 ``` npm run dev -- [选项] -m, --mode 权限模式(默认 auto) -d, --dir <目录> 工作目录(读写统一基准,默认 ./ai_default_project) --data-dir <目录> 数据目录(默认 ./data) --show-thinking 显示思考过程(默认折叠) --server 启动 HTTP Server(REST API + 托管 Web UI) --port <端口> HTTP Server 端口(默认 3000) --host <地址> 绑定地址(默认 127.0.0.1;暴露到局域网需显式 0.0.0.0;写主机名可能只绑到其中一族地址) --allow-hosts <主机名> Host 头白名单(逗号分隔,端口可省略,支持 *.example.com;用域名访问非回环地址时必填) headless 一次性运行(1.6.0,可被脚本/CI 编排): -p, --print <提示词> 执行后退出,不进入交互模式(与 --server 互斥) --output-format <格式> text(默认,只输出回答)| json(单个结果对象)| stream-json(NDJSON 事件流) --session 续接既有会话(缺省新建) --agent 直接指定智能体(缺省按提示词路由) --yes 本次运行放行需确认的工具(进程内开关,不落盘;不覆盖 deny 规则、never_auto_approve 与受保护路径) --max-iterations 覆盖本次运行的迭代上限 ``` ```bash # 结构化输出,stdout 只有一行 JSON(日志与告警走 stderr) npm run dev -- -p "总结 reports/季度复盘.md 的三个结论" --output-format json --mode auto | jq . # 事件流:system / thinking / text / tool_call / tool_result / result / confirm_denied npm run dev -- -p "把 a.md 里的 TODO 清掉" --output-format stream-json --yes # 续接会话继续追问 npm run dev -- -p "接着上面的结论,给出下一步计划" --session ``` **退出码**:`0` 成功 / `1` 运行失败(含模型连接失败) / `2` 参数错误 / `3` 权限拒绝(fail-closed) / `4` 达到迭代上限 / `130` 中断(Ctrl+C)。 headless 的执行语义:不初始化 TUI、不打印 banner 与状态区、不启动定时调度器;**没有确认通道**, 需要确认的工具一律 fail-closed 拒绝(结构化输出 `confirm_denied`),`ask_user` 直接失败而不是挂起等待输入。 ### 权限模式 | 模式 | 说明 | 工具调用 | 适用场景 | |------|------|---------|---------| | ask | 只读问答 | 仅只读工具(读取/搜索) | 只想问、不想被改文件 | | plan | 先列计划,确认后执行 | 每步需确认 | 重要改动前先过一遍 | | auto | 自动执行,高危仍需确认 | 全部,高危/受保护路径需确认 | 日常使用 | #### 权限规则(`config/permissions.json`) 模式之上可叠加 `Tool(specifier)` 级规则,求值顺序 **deny > ask > allow**: ```json { "rules": [ { "tool": "terminal_exec", "match": "*git push --force*", "action": "deny" }, { "tool": "fs_write", "match": "*secret*", "action": "ask" }, { "tool": "terminal_exec", "match": "*npm test*", "action": "allow" } ], "never_auto_approve": ["fs_write"], "protected_paths": [".git", ".ssh", ".env", "id_rsa"] } ``` | 字段 | 匹配对象 | 语义 | |------|---------|------| | `rules[].tool` | 工具名(glob) | `fs_*`、`mcp_*`、`*`,大小写不敏感 | | `rules[].match` | 目标串(glob) | 缺省 = 该工具全命中;目标串见下表 | | `rules[].action` | — | `deny` 任何模式直接拒绝;`ask` 强制确认(无确认通道即拒绝);`allow` 仅 auto 免确认,不绕过只读模式,也不能覆盖下方 `never_auto_approve` 与 `protected_paths` | | `never_auto_approve` | 工具名(glob) | 任何模式都必须确认,无通道即拒绝 | | `protected_paths` | 路径段 | 命中即对**读与写**工具强制确认;按路径段匹配(`.git` 命中 `.git/config`,不命中 `.gitignore`/`.github/**`;`.env` 命中 `.env.local`) | | 工具类别 | 目标串(`match` 的匹配对象) | |---------|----------------------------| | fs 类(`fs_read` / `fs_write` / `fs_edit` / `fs_list`) | 解析后的绝对路径 | | `terminal_exec` / `terminal_session` | 命令文本 | | 其余工具(含 MCP / 插件) | 参数 JSON(`JSON.stringify(args)`,键顺序敏感) | | 约定 | 说明 | |------|------| | 求值优先级 | 命中多条规则时 `deny` > `ask` > `allow`;同类取配置中首条 | | plan 模式 | 保持"全确认"语义,规则不改变它 | | 规则写错 | `action` 拼写错误等在启动时告警并忽略,不会静默变成"没有规则" | #### 权限记忆(`/permissions`) 规则分两级来源,确认弹窗里的「始终允许」与命令写入的是同一张表: | 来源 | 存放位置 | 生命周期 | 撤销方式 | |------|---------|---------|---------| | 项目级 | `<--dir>/config/permissions.json` 的 `rules`(不存在时回落启动目录的同名文件;原子替换:临时文件 + `fsync` + `rename`,保留 BOM、行尾风格、权限位与其他字段) | 跨进程 | `/permissions revoke <序号>` 或删文件条目 | | 会话级 | 进程内存 | 重启即失效 | `/permissions clear-session` | | 用法 | 行为 | |------|------| | `/permissions` | 列出生效规则:动作 / 工具 / 目标 / **来源**(会话级在前、项目级在后,即规则表实际顺序) | | `/permissions allow\|ask\|deny [()]` | 写入项目级规则(如 `/permissions allow terminal_exec(npm install)`) | | `/permissions revoke <序号> [--project\|--session]` | 按列表序号撤销;来源限定不符时拒绝,而不是撤错 | | `/permissions reset` / `clear-session` | 清空项目级 / 会话级规则(reset 清空整个 `rules` 数组,含无法识别的条目) | | 约束 | 说明 | |------|------| | 确认弹窗的「始终允许」 | 只出现在 **auto 模式的高危确认**上(规则 `ask`、`never_auto_approve`、受保护路径按设计每次都问,不提供记忆)。选项固定为 `1 允许 / 2 拒绝 / 3 始终允许(写入项目配置) / 4 本会话始终允许`;生成的规则带 `exact: true`,按**字面精确匹配**(命令里的 `*` 不当通配),撤销后立即恢复询问 | | allow 规则与危险检测 | 显式 allow 规则**不再短路危险检测**:`terminal_exec` / `terminal_session` / `fs_write` 的高危输入仍要确认,只有交互产生的 `exact` 规则与 `--yes`(进程内、不落盘)可豁免;`tool: "*"` 且无 `match` 的全局 allow 拒绝写入 | | 撤销的语义 | 撤销 / 重置**先改内存再落盘**:即使配置文件损坏或缺失,规则在当前进程也立即失效,并以 warning 说明"重启后会重新生效" | | 跨进程一致性 | 按文件 `mtime`/`size` 懒重载:CLI 或另一个进程改过规则后,运行中的 server 在下一次权限判定时生效;文件不可读时保留当前规则并告警(不静默丢 deny) | | 保护清单兜底 | 配置缺失或损坏时,受保护路径回落到**内置清单**(`.git`/`.ssh`/`.env`/`id_rsa` 等),不会因为"读不到配置"而静默失去保护 | | 审计归属 | 规则变更写审计并带 `agentId` / `sessionId`(确认弹窗触发的记在该会话上,CLI 记为用户操作),审计 Tab 可见 | | 拒绝写入的情形 | 配置损坏(不是合法 JSON / 顶层非对象)→ 一律拒绝且不新建;格式非法、`never_auto_approve` 清单内工具写成 `allow`、`allow` 目标触及受保护路径(按路径段判定,`.gitignore` / `.github/*` 不误伤)、通配工具无目标 → 拒绝并回显原因,文件原样不动 | | 首次写入 | 项目配置不存在时**按需创建**(以启动目录配置为模板,含受保护路径等基础策略);`<--dir>/config/permissions.json` 若是别的工具的同名文件(不含权限键)则拒绝覆盖。因此启动目录/安装目录的配置不会被 Agent 授权动作改写 | | fail-closed | 无确认通道(headless、后台任务)不会产生任何规则 | | Web 端 | 右侧栏「权限」Tab:规则表 + 来源徽标 + warning 提示 + 新增/撤销/清空 | 写接口的凭据门(1.11.0 起统一到一处 `needsToken`):**所有写方法**(POST/PUT/DELETE)都要凭据,读面只有 `/subagents`·`/jobs`·`/schedule`·`/notify`·`/notifications` 要凭据。凭据两种等价形式:请求头 `X-AiWorker-Token`(脚本/测试/非浏览器客户端;启动日志与 `/server-token` 可查)或页面 cookie(浏览器:`GET /` 下发 `aiworker_token`,**HttpOnly + SameSite=Strict**,前端因此不需要在调用点手工带 token)。跨站(`Sec-Fetch-Site: cross-site`,或缺失时 `Origin` 与 `Host` 不一致)→ 403(即使 token 正确);凭据不符 → 401;参数非法 → 400(见「HTTP 安全模型」一节)。 #### 路径与命令边界 读根 / 写根由 `config/sandbox.json` 定义,**命令层与 fs 工具共用同一套判定**(`src/security/path-policy.ts`): | 维度 | 内容 | |------|------| | 读根 | `allowReadDirs`,留空回退工作目录;`fs_read` / `fs_list` 越界直接拒绝(拒绝原因是策略,不是"文件不存在") | | 写根 | `allowWriteDirs`,留空回退工作目录;`fs_write` / `fs_edit` 与命令层写入目标使用同一写根 | | 符号链接 | 按**真实路径**判定:从盘根**逐组件**解析(每个存在的组件都 `realpath`,`..` 在解析之后才弹出,与内核"先跟随链接再弹一层"一致),链接指向根外会被拦截 | | 解析失败 | 悬空符号链接、链接环、权限不足、组件数超限 → **一律拒绝**(fail-closed,不退化成词法判定),提示"路径无法解析为真实路径" | | 开关语义 | 路径边界**始终生效**;`sandbox.enabled: false` 只关闭命令层约束,不会静默关掉文件读写边界 | | 空路径 | 空 / 纯空白 / 非字符串入参 → 拒绝 | | 保留设备名 | Windows 下 `NUL` / `CON` / `COM1` 等保留设备名与 NTFS 备用数据流(`a.txt:stream`)→ 拒绝,不再"写入成功但内容被丢弃" | | 配置位置 | 与权限配置同样**跟随 `--dir`**:优先 `<--dir>/config/sandbox.json`,其次启动目录,最后回落到包内 `config/sandbox.json`;目录下的同名文件若不含任何沙箱键则忽略 | `config/sandbox.json` 的命令层约束(**策略级**防线,非 OS 级隔离;配置位置同样跟随 `--dir`): | 维度 | 内容 | |------|------| | 生效约束 | 工作目录越界拒绝(fail-closed)· 命令黑名单 · 敏感环境变量剥离 · **`allowWriteDirs` 写入根约束** | | 覆盖范围 | `terminal_exec` / `terminal_session` 的重定向(`>` / `>>`,非包裹命令做引号感知,引号内的 `>` 不算重定向)与写入类命令/程序的**所有像路径参数**(源与目标都查) | | 已列举的写入形态 | `Set-Content` / `Out-File` / `del` / `copy` / `mkdir`;PowerShell 别名 `rm` / `ri` / `ni` / `sc` / `cp` / `mv`;`curl -o` / `Invoke-WebRequest -OutFile` / `robocopy` / `xcopy` / `tar -C` / `Expand-Archive` / `git clone` / `npm install --prefix` | | 无法静态解析的目标 | 含变量 / 通配 / 主目录(`$`、`%`、`~`)→ 直接拒绝,提示改用 `fs_write` | | 相对路径与 `..` | 按命令**原文**逐组件解析(`link\..\x` 先跟随 `link` 再弹一层),不做词法折叠 | | **不覆盖**(诚实说明) | 解释器脚本体内部的写入(`python -c`、`node -e`、脚本文件)· 未列举的第三方程序 · 管道下游程序的写入 · `cd` 之后相对路径的真实归属(`terminal_session` 按会话工作目录判定)· 命令位置之外的写入(如 `cmd /c del x` 的子命令参数)· 未被 `looksLikePath` 识别为路径的 token · MCP 工具与插件工具的文件写入 | | 定位 | 与 danger-detector、路径校验、工具超时、输出截断共同构成多层防御,**不是沙箱替代品** | #### 检查点与回滚 | 用法 | 行为 | |------|------| | `/rewind` | 列出本会话检查点(轮次 / 时间 / 输入摘要 / 文件数 / 可恢复数) | | `/rewind ` | 先打印预览,再交互三选:**代码+对话** / **仅对话** / **仅代码**(无通道则取消,fail-closed) | | `/rewind --code --dry-run` | 只预览将还原/删除/跳过/冲突的文件与将移除的消息数,不改动任何东西 | | `/rewind --all --force` | 覆盖"有外部改动"的冲突文件(默认跳过冲突文件) | | 维度 | 说明 | |------|------| | 快照时机与位置 | 每轮对话开始时建检查点(`/checkpoints//turn-/`);写文件前先落盘**变更前内容** | | 对话回滚 | 走事件溯源:不删历史事件,追加 `rewind/applied` 标记,消息视图与后续上下文按标记截断(`/trace` 仍可回看) | | 冲突保护 | 恢复前比对当前内容与记录的"变更后哈希",不一致(手改 / 被 `terminal_exec` 改过)默认拒绝覆盖 | | 保留策略 | 每会话最近 20 轮,`AIWORKER_CHECKPOINT_KEEP` 可覆盖 | | 边界(诚实说明) | 仅 `fs_write` / `fs_edit` 可精确回滚;`terminal_exec` 等改动只记录条目(`restorable: false`),回滚时列入"跳过";单文件 > 2MB 或二进制不入快照;回滚是**文件级整体还原**,不能只撤销部分行 | | Web 端 | 右侧栏「回滚」Tab 提供同一能力(需重启后端以加载新端点) | | 子智能体改动归属(1.9.0) | 子智能体(`wk-` 会话)的写操作登记到**父会话 spawn 时所在回合**,父 `/rewind ` 一次回滚父子全部改动。若 spawn 时父回合没有检查点(父空闲期间 spawn),该写入**拒绝登记**并写审计 `checkpoint:rejected`——它不参与回滚,且不会隐式建盘复活已 prune 的回合 | ## 后台子智能体(1.9.0) 主智能体可以起**后台子智能体**做并行调研/分析:可续接、可控制、可观测、可撤销。 | 工具 | 语义 | |------|------| | `spawn_agent` | 起一个后台子智能体(`agentId` / `task` / `readOnly`);返回 `sub-xxx` | | `send_message` | 追问:运行中入 pending 队列(上限 5),空闲时起新一轮 | | `list_agents` | 列出本父会话下的子智能体(状态 / 轮次 / token / 摘要) | | `interrupt_agent` | 中断当前轮并保留会话(转 `idle`,可继续追问) | ```bash # CLI:主智能体侧直接对话即可(默认智能体已开启「并行子智能体」开关) > 帮我并行调研 A、B、C 三个方案的优缺点 # HTTP(凭据门:跨站 403 → 凭据 401 → 参数 400;1.11.0 起写方法一律要凭据) GET /api/v1/subagents?parentSessionId=<父会话> POST /api/v1/subagents { agentId, task, readOnly?, parentSessionId? } POST /api/v1/subagents/:id/messages { message } DELETE /api/v1/subagents/:id # interrupt(保留会话) DELETE /api/v1/subagents/:id?purge=1 # 彻底关闭并释放并发槽位 ``` | 维度 | 说明 | |------|------| | 状态机 | `queued → running → idle`(可续接)\| `failed`;`idle` 不占并发配额 | | 默认只读 | `readOnly` 缺省 `true`:工具面收窄为闭集 `fs_read / fs_list / web_search / web_fetch`,且权限模式收窄为 `ask` | | 并发 | 默认并发 4、每父会话 4、全局 8(超限返回错误而非排队);`MAX_PENDING=5` | | 深度 1 | 子智能体的工具面**不含**四个控制类工具,执行层再按 `wk-` 前缀硬校验一次(双层) | | 安全边界 | 子智能体无确认通道(fail-closed 立即拒绝,**用异步作用域隔离实现,不再替换进程级 provider**——此前会连父会话的确认通道一起顶掉);`spawn_agent` / `send_message` 在 `never_auto_approve` 清单内,**每次都需人工确认**,headless 下不可用 | | 只读 ≠ 零磁盘写入 | 只读指**不改用户工作目录内容**;输出溢出落盘(`/spills/`)、检查点、遥测仍会写入数据目录 | | 中断分层承诺 | 轮边界中断**必达**;工具级真中断已实现:`terminal_exec` 消费 `ToolContext.signal`,abort 时按进程树终止(Windows `taskkill /T /F`,POSIX 进程组 `SIGKILL`) | | 可观测 | 控制台「子智能体」tab(1.9.1:列出状态/轮次/父子 token/所属父会话,可追问、中断、关闭);WS 事件 `subagent/spawned\|queued\|done\|failed`(`queued` 用于"轮次结束但重新入队"这一中间态,避免误报失败);审计 `subagent:*` 带 `actor_session_id`;进程面板独立「子智能体」类型(**徽标只计活动,已结束可续接的单独标注**,不再像是"还在跑");1.10.0 起停稳时另推站内通知 | | 诚实边界 | 无 worktree/副本隔离——多个子智能体同写一个文件会互相覆盖;运行中插话仅在**轮边界**注入;`wk-` 会话不在会话列表展示 | **怎么触发**(四条路,同一套 `subagentRunner`): 1. **对话里由主智能体发起**:它调 `spawn_agent`(**受限工具**,需要智能体配置里的「并行子智能体」开关 `subagents: true` 放行——7 个内置专家默认全开;也可以继续把这 4 个工具逐个写进 `tools`)。因该工具在 `never_auto_approve` 清单内,**每次都要人工确认**;headless(`-p`)下没有确认通道 → fail-closed 不可用。 2. **TUI `/bg [--readonly] <任务>`**:用户显式提交,绑定当前会话为父会话(改动可随 `/rewind` 连带回滚),缺省完整工具面。 3. **HTTP `POST /api/v1/subagents`**:脚本/外部程序入口(`/api/v1/jobs` 是同一实现的兼容视图)。 4. **定时任务**:`/schedule` 到点后经兼容层起子智能体(超配额时本轮跳过并写 `schedule:fire` 审计,不再像旧的 jobRunner 那样排队)。 **查看与操作**:TUI `/subagents`(列表 / `send` / `stop` / `close`)、Web 控制台「子智能体」tab、进程面板 `subagent` 计数。 **开关口径**:`subagents: true` 写在 `config/agents/.yaml`(Web **控制台 →「智能体」→「并行子智能体」**勾选框即此字段;自定义智能体表单默认不勾选),开启后 4 个控制面工具进入该专家的可见工具面,无需逐个列入 `tools`(`strictTools: true` 时开关不例外,仍需显式列出);`readOnly` 模式不生效,子智能体自身恒为关闭(深度 1)。**注意路由**:「调研/分析/对比/报告」类需求会路由到 `research` 专家,因此开关必须开在**实际接活的专家**上(7 个内置专家现已默认全开)。 **模型为什么不一定会派**:工具可见 ≠ 模型一定调用。实测一次「帮我并行调研对比分析 ubuntu/fedora/rocky 的差异」时,`research` 已经拿到了 `spawn_agent`,但它把"并行"实现成**同一步里并发多个 `web_search`/`web_fetch`**(18 步 66 次调用,0 次 spawn)。因此 1.9.1 同时补了**触发条件层面的引导**:`spawn_agent` 的工具描述写明"多个独立对象 → 每个对象各调用一次",`research` 与 `default` 的 systemPrompt 也加了"并行调研"小节(每个对象一个只读子智能体 → `list_agents` 跟进 → 汇总)。仍属概率性行为,不是硬性保证;若你希望强制走子智能体,需要在 systemPrompt 里写得更硬。 > 统一说明(1.9.1):后台执行只保留 `subagentRunner` 一套;`jobRunner` 退化为兼容视图(`src/core/job-runner.ts`),状态映射 `idle ↔ done`。并发上限由 subagentRunner 决定(4 / 每父 4 / 全局 8),**超限抛错**而非排队;旧的 `job-` id 不再产生。 ## 站内通知(1.10.0) 后台任务与子智能体是**异步**的,人的注意力是同步的——通知条解决的就是这个错位:跑完了、或者卡在等你确认,不用一直盯着面板。 | 事件 | 何时推 | 内容 | |------|--------|------| | `taskEnd` | 子智能体**停稳**(正常完成,或失败:看门狗超时 / `result.truncated`——迭代上限、上下文过长、连续空响应、重复循环或工具连续失败被强制终止——/ 抛错) | 专家名 + 任务原文(前 60 字)+ 结果摘要或错误原因(**整段正文**压成单行、封顶 200 字) | | `awaiting` | 确认卡片或 `ask_user` 提问**挂起等待**时(仅 HTTP 路径;TUI 侧不推) | 确认标题 + 消息 / 问题原文;`/chat` 路径带 `sessionId`(点击可切回该会话),`/plan`、`/debate` 自建会话不带 | ```bash GET /api/v1/notify # 通知配置快照(config / configPath / exists) POST /api/v1/notify # 部分更新:{ enabled?, events?: { taskEnd?, awaiting? } }(需凭据且非跨站) GET /api/v1/notifications # 未读通知(重连/刷新后补看) POST /api/v1/notifications # { action: "seen", ids } | { action: "clear" }(需凭据且非跨站) ``` | 维度 | 说明 | |------|------| | 呈现 | Web 界面右上角通知条(默认摊开 4 张,可展开看全部、单张关闭、全部清除);点击任务类通知跳到控制台「子智能体」,带 `sessionId` 的等待类通知切回该会话 | | 开关 | **设置 → 交互 → 站内通知**(`enabled` 总开关 + 两个事件各自勾选),写 `config/notify.json`,**改完立即生效**(每次派发现读磁盘) | | 传输 | 在线走 WS `notify` 事件(**WS 重连时也会补拉一次**,断连窗口内产生的不会丢);不在线时服务端把通知留在内存环形缓冲(50 条,**含已读**——补看只返回未读),页面再打开时由 `GET /notifications` 补看 | | 只补看任务类 | 确认/提问的卡片只存在于**发起该请求那个标签页**的 SSE 流里,补看会是死链——因此 `awaiting` 只走实时通道,且带 30s 生存期(与服务端确认通道超时一致),用户答复后服务端广播 `notify/resolved` 立即撤掉 | | 多标签页 | 「全部清除」会广播给所有标签页同步撤掉;但 `awaiting` 是实时广播给所有连接的——**非发起标签页也会弹「等待你的确认」**,在那页点击只能切会话、没有可作答的卡片(要回到发起页确认),30s 后自行消失 | | 不推的情形 | 你**主动中断**的任务(`/subagents stop`、面板「中断」、删会话连带中断)——他人就在面板前,推了是噪音。注意这不是"逐案判定发起人":最终落在 `failed` 的路径都会推(含"中断后 run 卡死、被看门狗判失败"这一窄情形) | | 诚实边界 | **只做站内**:没有 OS toast / 邮件 / IM 推送,也需要服务端进程活着(进程退出后通知本就无意义)。缓冲在内存里,重启即空;配置只有事件维度、没有渠道维度(加渠道要扩键)。已读/清除是**写面**(1.11.0 起走统一凭据门):浏览器靠页面 cookie、`web:dev` 的 `:5173` 靠 Vite 代理注入,两种打开方式都能与其它标签页共享状态;服务端重启后旧 token 失效,通知条会提示刷新页面 | | 前端渲染 | 通知条不订阅 SSE,只消费 WS + `GET /notifications`;`seen` 标记在服务端只影响**补看**路径——同时开着的多个标签页仍会各收到一次实时推送 | ## 交互界面 ### TUI 终端 `npm run dev` 直接进入 TUI:下方输入框对话,上方消息区流式渲染,底部常驻状态栏。命令系统注册表化,`/help` 表格自动生成(`/<命令> --help` 查看详细用法)。 | 命令 | 说明 | |------|------| | `/mode ` | 切换权限模式 | | `/plan <任务>` | 多专家 DAG 协作 | | `/debate <话题>` | 双专家辩论 | | `/app ` | AI OS 应用生命周期管理 | | `/bg [--readonly] <任务>` | 提交后台**子智能体**(不阻塞交互;绑定当前会话,可续接/可中断;`--readonly` 收窄为只读闭集) | | `/subagents [send <消息> \| stop \| close ]` | 子智能体:列表(**全部**,含各父会话)/ 追问 / 中断 / 关闭(TUI 为本地单用户界面,不做会话归属限制;工具与 HTTP 路径仍有归属/权限校验) | | `/jobs [cancel ]` | 后台任务兼容视图(`done` 即子智能体 `idle` 可续接;`cancel` 即中断) | | `/schedule` | 定时任务管理:`add "\|自然语言>" "<任务>" [agentId]` / `remove `(自然语言先走规则解析;规则失败再走模型兜底,**兜底结果按原句校验并自动修复**,例如原句的「周六」不会被丢掉) | | `/install <路径> [-f]` | 安装 .aw 包或裸格式(.md 技能 / .json MCP / 插件目录,自动识别;其它后缀拒绝) | | `/pkg export <类型> <名称> [--raw]` | 打包导出 .aw;`--raw` 输出裸格式;`/pkg list` 查看可导出资产 | | `/skill <名称>` / `/技能名` | 手动激活技能 | | `/skills` | 查看全部技能(按专家分组 + 描述) | | `/new` | 开启新会话(清空上下文) | | `/log` | 监控日志(轮次/耗时/输入输出 token) | | `/rewind [轮次] [--code\|--chat\|--all] [--dry-run] [--force]` | 检查点回滚:无参数列出回合,带轮次交互三选(代码+对话/仅对话/仅代码),先预览再执行 | | `/permissions [allow\|ask\|deny [()] \| revoke <序号> \| reset \| clear-session]` | 权限规则:列出(含来源)/ 写项目级规则 / 撤销 / 清空项目级或会话级 | | `/context [查询]` | 上下文分层 token 占比 + MCP 工具列表 | | `/trace [序号]` | 会话轨迹时间线(--json 输出) | | `/status` | 运行状态(版本/模式/模型/专家/token) | | `/config` | 配置:model / temperature / max-tokens / iterations / thinking / skill-evo / add-model / reset(持久化) | | `/mcps` | 查看已加载的 MCP 服务器 | | `/sessions` / `/switch <序号>` | 浏览 / 切换历史会话 | | `/export [序号]` | 导出会话为 Markdown 文件 | | `/copy` | 复制最后回答原始 Markdown | | `/help [命令]` / `/exit` | 帮助 / 退出 | #### 快捷键 | 按键 | 作用 | 生效时机 | |------|------|---------| | `Enter` | 提交输入(输入为空时不提交) | 输入中 | | `Shift+Enter` / `Alt+Enter` / `Ctrl+Enter` | 换行(多行输入) | 输入中 | | `Tab` | 补全命令 / 技能名(多候选时显示提示行) | 输入中 | | `↑` / `↓` | 多行输入 → 移动光标行;有内容 → 翻历史;输入为空 → 滚动消息区 | 输入中 | | `←` / `→` / `Home` / `End` | 光标左右移动 / 行首行尾 | 输入中 | | `PgUp` / `PgDn`(滚轮同理) | 消息区翻页(10 行)/ 滚动(3 行) | 任意 | | `t` / `o` | 折叠·展开 最近思考 / 工具详情 | 回合运行中 | | `[` / `]` | 在思考·工具块之间移动焦点 | 回合运行中 | | `c` / `e` | 全部折叠 / 全部展开 | 回合运行中 | | `←` / `→` | 在历史回合的可折叠块之间移动焦点 | 输入为空且有可折叠块 | | `Enter` | 折叠 / 展开焦点块(无焦点时取最近的思考块) | 同上 | | `Esc` | 清除焦点高亮 | 同上 | | `Ctrl+C` | 运行中:中断当前回合;空闲:退出 | 全局 | | `Ctrl+D` | 退出 | 空闲 | > 空闲态的 `←` / `→` / `Enter` / `Esc` 仅在输入为空时生效,不占用字母键位;输入任意字符即自动清除高亮。 #### 回合块视图 | 区块 | 展示 | 交互 | |------|------|------| | 思考 | 流式摘要(`--show-thinking` 时展开全文) | `t` 折叠 / 展开 | | 工具调用 | 原位显示 ✓/✗、耗时与参数摘要 | `o` 折叠 / 展开详情 | | 回答 | 流式 Markdown + 产物 chips | — | #### 工具产物预览 | 产物 | TUI | Web | |------|-----|-----| | 📄 文件 | chip(含大小),终端支持 OSC 8 时可直接点击打开 | 点击 chip 打开预览窗口(Markdown / 代码 / 图片 / 视频 / 音频 / PDF / Office / diff),可全屏、可拖拽调整大小 | | 🔗 链接 | chip(站点 + 标题) | 可点击,链接产物同样进预览窗口 | | 📝 变更 | chip(`+/-` 行数) | diff 着色预览 | ### Web UI ```bash npm run web:build # 构建 → web/dist/(生产,由 Server 托管) npm run web:dev # 开发模式 → localhost:5173(API 代理到 3000) ``` 生产部署:`npm run build && npm run start -- --server --port 3000`,浏览器打开 `http://localhost:3000`(**默认只绑 `127.0.0.1`**;要局域网访问加 `--host 0.0.0.0`,用域名访问再加 `--allow-hosts <主机名>`,详见「HTTP 安全模型」)。 | 维度 | 内容 | |------|------| | 对话模式 | 对话 / 多专家协作 / 双专家辩论(顶部切换) | | 权限联动 | 权限模式实时生效:Ask 只读;Plan / Auto 走确认卡片 | | 流式与中断 | 流式输出,发送后可随时中断 | | 系统面板(⚙) | **控制台**(观测:上下文 / 轨迹 / 审计 / 进化;资源:智能体 / 技能 / MCP / 插件 / 应用 / 进程 / 调度;系统:设备与运行时)与 **设置**(模型 / 安全 / 工作区 / 交互 / 关于)两个独立入口 | | 右侧栏 | **三栏 1:2:3**(左会话列表 : 中对话 : 右面板,按视口 1/6 : 1/3 : 1/2 定宽;**折叠右栏时左列不变宽、对话区吃满 5/6**,拖分隔条只改右栏、双击回到比例)。右栏三个 Tab:**产物**(会话产出的文件/文档/链接 + 检查点时间线)/ **文件**(工作目录文件树 + 「变更前 ↔ 当前」左右比对)/ **应用**(权限已迁至 设置 → 安全;底部状态栏有权限徽章可一点直达) | | 多标签同步 | 会话列表跨标签页实时同步(WebSocket) | ### 设置 / 控制台(1.8.0) **分工一句话**:**设置 = 我改它**(会写配置、改变行为);**控制台 = 我看它**(观测系统、以只读为主)。 | 设置 tab | 能改什么 | 写到哪里 / 何时生效 | |---|---|---| | 模型 | 模型选择、温度、max-tokens、添加模型 / Provider;模型能力卡与【检测图片能力】实测 | `data/runtime-config.json` + `config/models.json`,立即生效 | | 安全 | 权限规则(增 / 撤 / 清空,带项目级与会话级来源)、受保护路径、永不自动批准、命令黑名单、敏感环境变量清理 | `config/permissions.json` + `<--dir>/config/sandbox.json`,**立即生效**(清单改动同步更新运行中的判定) | | 工作区 | 工作目录(只读展示)+ 沙箱三个根:工作目录根 / 可写根 / 可读根(带目录选择器) | `<--dir>/config/sandbox.json`,**立即生效**(每次工具调用现读磁盘) | | 交互 | 思考展示、技能自动沉淀、主题、**站内通知**(总开关 + 任务结束 / 等待确认两类事件) | `data/runtime-config.json` / `config/notify.json` / 浏览器本地,**立即生效** | | 关于 | 版本、数据目录、存储(SQLite + FTS5)、运行时 | 只读 | **两个安全细节(刻意的,不是顺手写的)**: 1. **受保护路径用生效清单回填**:配置文件里的 `protected_paths` 是**整体替换**语义,所以"少写一条"就等于静默丢掉一层保护。设置页因此用**当前生效清单**回填草稿,并且**移除内置基线项必须显式勾选确认**——这道确认由**服务端强制**(缺 `acknowledge` 直接 400 并列出将被移除的项),不只是前端弹窗。 2. **沙箱写入永远跟随 `--dir`**:读取允许回落到安装目录的配置,但写入绝不落到安装包(否则一次 UI 保存就改写安装文件,升级即丢失)。三个根"留空 = 回退工作目录",所以页面同时显示"配置里写了什么"和"**真正生效什么**",避免把"空着"误读成"不限制"。 ### 设备面板(动态探测) **设置 → 模型**(能力卡)与**控制台 → 设备与运行时**(只读快照)里的每一项都是**实时探测**结果,不是写死的清单: | 卡片 | 探测方式 | |------|----------| | 语音输入(ASR)/ 语音输出(TTS) | 逐个校验模型文件(缺哪个列哪个)+ provider 解析(sherpa 本地就绪优先,否则 edge-tts 在线降级) | | 媒体服务器 | WS 音频通道实际状态与连接数 | | 模型能力 | 图片能力按「**实测 → 配置声明 → 未声明**」三级判定。点【检测图片能力】会真实发一次 1×1 PNG 请求(约几十 token)验证端点是否接受图片;结论按**模型名**缓存到 `/device-probe.json`,换模型自动失效(面板标"此结果失效")。未实测且 `config/models.json` 的当前 profile 未声明 `vision` 时,图片提问会被明确拒绝并提示如何检测 | | 运行时 | Node / 平台 / 架构 / CPU / 内存 / PID / 运行时长、数据目录可写性(真实写文件探测,非 `accessSync`) | | 存储(记忆库) | 真实建库并验证 SQLite 版本与 FTS5 可用性 + 记忆库路径与体积 | 模型能力判定与 `/api/v1/chat` 的图片输入**同源**:实测通过后即可发送图片,不必改配置;`vision:true` 只是省掉一次实测的声明。 ### 产物工作台(右侧栏「产物」) 一次会话**产出了什么**集中在一处。它与「文件」面板分工明确:**产物 = 本会话产出了什么**(含工作目录之外的生成文档与外部链接),**文件 = 工作目录现在长什么样**(树 + 比对)。 | 区域 | 内容 | |------|------| | 顶部过滤 | 按类型 chips(全部/文档/代码/图片/媒体/链接/其他)+ 路径搜索。**没有**「所有会话」范围与「按回合」分组(前者与本会话产出无关、10 会话截断还会让人以为丢东西;后者回合号每行都标了,分组只把列表切碎) | | 左侧列表 | 每个产物带 `新增/修改/删除`、`+n −n`、回合号、大小与**来源徽标**(工具产物 / 文件系统快照 / 文档索引 / 回合检查点);**左列表与右详情的分界可拖拽调宽**(双击或 `Home` 复位,`←/→` 微调,宽度记忆在本地) | | 右侧查看器 | 与文件面板**共用同一个 `FileViewer`**:`.md` 走文档渲染、HTML 走沙箱预览(可切源码)、图片内联、文本原文、链接卡、视频/音频/PDF/Office 走独立预览窗口(含 Office 前端转换)。**行级 diff 不在这一栏**(见下方「文件面板」) | | 动作 | 打开预览 / 下载 / 复制路径 / 在对话中查看(滚到产生它的工具卡片并**持久高亮**;同一产物命中多处时可 ↑更早 / ↓更近 逐处走查,`Esc` 或「清除高亮」结束定位) | | 底部时间线 | 检查点按回合排列;点选 → **高亮**该回合产物(不隐藏其他);`回滚代码` / `回滚代码 + 对话` 均先出 `dry-run` 预览再二次确认。这是**唯一的代码回滚入口** | **诚实标注**:来源为「文件系统快照」的改动可能不是本会话 Agent 写的(工作目录快照的语义如此);老会话(Sprint 45 之前)事件里没有产物字段,面板会明确提示"仅显示文件改动与文档",不假装没有产出。 「仅对话回滚」不在产物面板:它是聊天语义,放在**对话流里用户消息的「从这里重新开始」**(hover 显示,先 `dry-run` 再确认)。 ### 文件面板(右侧栏「文件」,1.12.0) **"现在工作目录里长什么样、这个文件这次改了什么"**——文件树与左右比对各自归位: | 区域 | 内容 | |------|------| | 左:文件树 | 逐层懒加载;默认折叠 `node_modules` / `.git` / `dist` / `coverage` 等 12 类目录(勾「忽略项」可全部展开);按名称过滤;本会话有改动的文件右侧带 `+n −n` 徽标;树宽可拖拽(双击复位) | | 右:左右比对 | **默认「预览」**(选中文件先看内容,看差异是第二动作),档位顺序 `预览 → 对比 → 全文`:HTML 走沙箱渲染、图片内联、PDF 原生阅读器、`.md` 渲染后看(「全文」看源码);对比视图两侧独立行号、改写行配成一行(左红右绿、左侧带删除线),纯增/纯删只占一侧;纯文本/代码没有预览形态,直接给 `对比 / 全文` | | 数据来源 | `GET /workspace/diff`:变更前内容取自**回合检查点的 blob**(该文件本会话首次改动前),当前内容现读磁盘;对齐计算在服务端(`splitDiff`),前端只负责渲染 | | 诚实边界 | 检查点只覆盖 `fs_write` / `fs_edit`(且每会话保留最近 20 轮)。由 `terminal_exec`/MCP/插件写入的文件、二进制与大文件**拿不到变更前内容**——面板会写明原因并只显示当前内容,不会画一条假 diff;没有选中会话时同理(有树、无对比) | ### 技能编写与提示词优化(1.12.0) 技能与提示词原先只有"导入文件"和"手写文本框"两条路。现在两条都能在界面里完成,并且可以**先让模型打磨一遍**: | 入口 | 能力 | 落点 | |---|---|---| | 控制台 → 技能 → 「+ 新增技能」 | 填名称 / 一句话说明 / 绑定专家 / 触发词 / 所需工具 / Markdown 正文 → 保存。服务端校验(kebab-case 名字、正文 ≥20 字、**触发词必须是合法正则**、工具名字符集、描述长度),生成 frontmatter 并写 `skills///SKILL.md` + `manifest.json`,**保存即热生效**(无需重启) | 磁盘(`skills/`) | | 控制台 →「AI 优化正文」/ 智能体 → System Prompt → 「AI 优化提示词」 | 用当前模型按固定纪律改写:保留可验证细节(文件名/命令/阈值)、禁止编造工具与 API、只输出正文、中文进中文出。技能侧按「适用场景 / 解决方案 / 常见陷阱 / 示例」结构重写,智能体侧按「角色职责 / 工作方式 / 工具约定 / 边界 / 输出风格」重写 | **只回填编辑器,不落盘**(要保存仍需点一次保存),并带「撤销」 | | 控制台 → 技能 → 「导入资产」 | `.aw` 包 / 裸 `SKILL.md`(`.md`)/ 裸 MCP `.json`,**只认这三种后缀**——`.markdown`、`.exe` / `.com` 等非技能插件文件当场拒绝;导入前先 peek 再二次确认(插件执行代码、MCP 起进程) | 磁盘 | **边界**:优化调用有 60 秒超时;模型返回被清洗(去整体代码围栏、"以下是…"式开场),结果为空按失败处理。手写建档只写 `SKILL.md` 与 `manifest.json`(不含附加资源),同名技能默认拒绝覆盖——Web 表单不提供覆盖按钮,避免一键把别人的技能改掉。 ### HTTP 安全模型(1.11.0) | 面 | 做法 | |---|---| | 凭据 | 单点判定 `needsToken(method, pathname)`:**写方法一律要凭据**;读面只有 `/subagents`·`/jobs`·`/schedule`·`/notify`·`/notifications` 要凭据。凭据 = 请求头 `X-AiWorker-Token`(脚本/测试/非浏览器客户端)**或** `aiworker_token` cookie(浏览器:`GET /` 下发,HttpOnly + SameSite=Strict)——前端不需要在调用点手工带 token。比较用定长常量时间,避免逐字符试探 | | 跨站 | `Sec-Fetch-Site: cross-site`(缺失时比对 `Origin` 与 `Host`)→ **403**,即使 token 正确 | | 跨源读 | API 响应**不再发 `Access-Control-Allow-Origin`**,跨源读由浏览器自己拦下(此前每个 JSON 读响应都带 `*`,任意网站都能读走会话标题/子智能体任务原文/模型摘要)。唯一例外是 `/apps/:id/*` 静态资源——沙箱 iframe 是不透明源,需要它才能取自身数据文件,且该路由只发应用自身产物 | | DNS rebinding | `Host` 白名单只放行 `localhost` / `127.0.0.1` / `[::1]`(非回环部署用 `--allow-hosts` 显式放行)。**cookie 挡不住 rebinding**:其请求对浏览器是同源(`Sec-Fetch-Site: same-origin`),且 `GET /` 内联的 token 会被同源脚本读走——Host 校验才是这条路的防线 | | 监听面 | 默认只绑 `127.0.0.1`(此前绑全网卡:同网段任何人打开 `http://<机器>:3000/` 就能拿到注入的 token)。WS 升级与 HTTP 同门:Host 白名单 + 非跨站 + 凭据三者齐过才握手,否则 401 断开 | | 开发态 | `:5173` 的页面不是 API 服务器下发的、拿不到 cookie → Vite 代理在**服务端**补 `x-aiworker-token`(实时读 `<仓库>/data/server-token`,可用 `AIWORKER_TOKEN_FILE` 指定;后端不在 3000 时用 `AIWORKER_API_TARGET` 指定代理目标;读不到 token 时打印一次提示)。前端仍然不知道 token | | 诚实边界 | ① **不再支持跨源浏览器客户端**(去掉 ACAO 的代价;非浏览器脚本不受影响)。② WS 广播尚未按连接收敛:已鉴权的客户端仍收全量 `eventBus`,S53 的「非发起标签页应答不了 `awaiting`」边界继续存在。③ 读面只覆盖登记清单,`/sessions`·`/artifacts`·`/audit` 等仍未要凭据(跨源读由"无 ACAO"挡住,token 是纵深)。④ `/files` 保持无凭据——它被原生 ``/`