Files
sw-ads-agent/README.md
T

212 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Agent Studio Mini
Agent Studio Mini 是一个用于内部培训的、可运行的最小 AI Agent Harness。它用 Pi Coding Agent SDK 驱动 Agent Loop,以 Fastify + SSE 暴露受控协议,用 assistant-ui 展示聊天、Tool、审批、图片附件与完整事件时间线。
当前仓库未附产品截图;启动后,`http://127.0.0.1:5173` 即是桌面三栏和移动端 Tabs 的真实演示界面。
## 架构
```mermaid
flowchart LR
UI[assistant-ui\nComposer / Thread / Tool UI] -->|HTTP + SSE| API[Fastify API]
API --> REG[SessionRegistry]
REG --> PI[Pi AgentSession]
API --> STORE[EventStore / ApprovalBroker]
PI --> LOADER[DefaultResourceLoader / Tools]
STORE --> VIEW[Timeline / Approval UI]
LOADER --> RES[Skills / Workspace / ModelRuntime]
```
```mermaid
sequenceDiagram
participant U as User
participant W as Web + assistant-ui
participant H as Harness
participant P as Pi AgentSession
U->>W: 文本 + 图片
W->>H: POST message (立即 202)
H->>P: prompt(text, { images })
P-->>H: stream / tool events
H-->>W: HarnessEvent over SSE
P->>H: save_report_draft preflight
H-->>W: approval.required
U->>H: Approve / Deny
H-->>P: allow / block
P-->>H: tool result + completion
```
职责边界很明确:Pi `AgentSession` 是 Agent Loop、模型调用和 Tool 生命周期的事实来源;Harness 管理 Session、事件序列、安全守卫、附件、模型凭证和审批;assistant-ui 只把 Harness 状态投影为聊天和 Tool UI,不直接调用模型,也不在浏览器执行服务端 Tool。
## 目录
```text
apps/server Fastify HTTP/SSE 组合层
apps/web React、assistant-ui 与三栏界面
packages/harness Session、Skills、Tools、Approval、安全边界
packages/shared 浏览器/服务端共享的 Zod 协议
.agents/skills ads-analysis 示例 Skill
.pi/skills report-writer 示例 Skill
fixtures 离线广告账户数据
workspace/sessions 运行时临时 Session(被 gitignore
docs 架构、映射、安全、课堂脚本与路线图
```
## 环境与启动
Pi 0.84.3 的实际 package engine 要求 Node.js `>=22.19.0`,因此本仓库采用该版本,而不是需求草案中的 Node 20 下限。需要 pnpm 11。
```bash
cp .env.example .env
pnpm install
pnpm dev
```
Server 默认监听 `127.0.0.1:3001`Vite Web 默认监听 `127.0.0.1:5173` 并代理 `/api``/health`。打开 `http://127.0.0.1:5173`。根命令还包括:
```bash
pnpm lint
pnpm typecheck
pnpm test
pnpm build
```
## 模型配置
服务器环境变量优先级低于当前 Web Session 的内存设置:
```dotenv
OPENAI_API_KEY=
ANTHROPIC_API_KEY=
MODEL_CREDENTIAL_ENCRYPTION_KEY=
AGENT_PROVIDER=
AGENT_MODEL=
AGENT_DISPLAY_NAME=
AGENT_SUPPORTS_IMAGES=false
AGENT_CONTEXT_WINDOW=
AGENT_MAX_TOKENS=
LLM_BASE_URL=
LLM_API=openai-responses
ALLOW_PRIVATE_LLM_BASE_URLS=false
SESSION_RUN_TIMEOUT_MS=300000
APPROVAL_TIMEOUT_MS=60000
MAX_IMAGE_BYTES=5242880
MAX_IMAGES_PER_MESSAGE=4
```
`SESSION_RUN_TIMEOUT_MS` 是运行无活动超时:每次模型增量、Tool 生命周期或 Pi 生命周期事件都会重新计时,长链路不会仅因累计时长被中断。
服务启动时会从项目 `.env` 严格加载并校验上述默认模型字段;`AGENT_PROVIDER``AGENT_MODEL` 必须同时存在。当前 Web Session 的 Settings 覆盖环境默认值;未覆盖时新 Session 自动使用 `.env`,无需每次启动重新配置。也可在顶栏 Settings 配置 Provider ID、`openai-responses` / `openai-completions` / `anthropic-messages`、Base URL、Model ID、Vision 与 API Key。Preset 只是可编辑起点,不保证模型存在。没有显式模型或凭证时返回 `MODEL_NOT_CONFIGURED` / `MODEL_CREDENTIAL_NOT_CONFIGURED`,绝不选择“第一个可用模型”。
Provider、API 类型、Base URL、Model ID、Display Name、Vision、context/max token 等**非敏感模型配置**会以版本化 strict Zod 结构保存到浏览器 `localStorage`。刷新或 Reset 创建新 Session 后,在服务器没有环境模型配置时自动恢复这些字段;损坏、超限或旧格式数据会安全丢弃。Settings 中的“忘记本地配置”只清除这份浏览器配置,不影响当前 Session。
API Key 可直接由 `.env``OPENAI_API_KEY` / `ANTHROPIC_API_KEY` 提供。若在前端提交,则服务端使用 `MODEL_CREDENTIAL_ENCRYPTION_KEY` 做 AES-256-GCM 认证加密,密文写入 gitignored 的 `workspace/model-credentials.enc.json`;下次启动会按 `.env` 默认模型的连接 identity 自动恢复。浏览器仍不会把 Key 写入 Web Storage、IndexedDB 或 Service Worker cache。刷新或 Reset 后无需重复输入。读取配置只返回 `configured`、安全模型信息、末四位 `keyHint` 与存储类型。Clear Credentials 会同时清除当前 Runtime credential 和对应持久密文;换模型会销毁既有 Pi Session,下一次 Run 按新配置创建。密钥不会写入 Pi 全局 `auth.json` / `models.json`、URL、SSE 或日志。
生成 master key`openssl rand -base64 32`。该值必须稳定保存在服务器 `.env` 或 secret manager 中;丢失或更换后旧密文会 fail closed、无法解密。生产环境应迁移到 KMS/HSM 并设计密钥轮换。
自定义 Base URL 使用 WHATWG `URL`、协议/userinfo/fragment/query 检查与 DNS 解析;默认拒绝 loopback、private、link-local 和 metadata 地址。开发环境只放行显式 loopback HTTP;企业内网需明确设置 `ALLOW_PRIVATE_LLM_BASE_URLS=true`。模型 Provider SDK 的重定向策略不是本课堂 Harness 可控的公开扩展点,生产版应在出口代理处禁用跨主机重定向并重新校验每一跳。
连接测试只执行有界的本地配置校验并返回“首次运行验证”,不创建 Agent Run、不记录模型响应,也不伪造远端成功。
## Skills 与 progressive disclosure
启动时 `DefaultResourceLoader.reload()` 发现 `.agents/skills``.pi/skills``AGENT_SKILLS_DIRS`(使用操作系统 `path.delimiter`)。额外目录会 canonicalize、去重、验证;错误进入 Catalog diagnostics,不拖垮服务。`GET /api/skills` 只返回名称、描述、来源、安全展示路径和 diagnostics。
Harness 不在启动时读取并拼接所有 `SKILL.md` 正文。Pi 仅先看到 Catalog,匹配任务后通过 `read` 按需加载正文。`skill.requested` / `skill.loaded` **不是 Pi 原生事件**Harness 以已发现 Skill 的 canonical `SKILL.md` 路径索引,从成功的 `read` 请求/结果可靠推导;读取失败不会误报 loaded。
示例:
- `ads-analysis`:要求先调用离线 `mock_ads_metrics`,不声称有真实平台数据。
- `report-writer`:生成结构化中文报告,并请求 `save_report_draft`
## Session、Events 与 SSE
每个 Web Session 有随机 ID、`workspace/sessions/<id>` 独立 cwd、`SessionManager.inMemory(cwd)`、事件 sequence、运行锁、AbortController、审批、附件与内存凭证。为避免无配置时触发 Pi 的模型 fallbackWeb Session 先建立,Pi `AgentSession` 在首次已配置 Run 时惰性创建;每个 Web Session 至多对应一个独立 Pi Session。
消息 API 接受后立即返回 `202 + runId`;同 Session 并发返回 `409 SESSION_BUSY`。用户取消与系统超时分别记录。删除顺序是 abort、取消审批、关闭订阅/SSE、dispose、清凭证/附件、校验目录边界、删除临时 cwd。
EventStore 原子分配单 Session 单调 sequence,保留最近 1000 条。SSE 使用 `id/event/data`、15 秒 heartbeat、`Last-Event-ID` 优先于 `afterSequence`、历史重放、窗口 gap 控制事件、慢客户端断开策略与连接清理。前端 reducer 按 eventId/sequence 去重并合并高频 delta。
完整映射见 [docs/event-mapping.md](docs/event-mapping.md)。
## Tools 与 Approval
显式 allowlist 只有 `read``grep``find``ls``mock_ads_metrics``save_report_draft`。默认不注册 bash/powershell/edit/write;未知 Tool deny。
- `mock_ads_metrics` 用当前 Pi `defineTool` + TypeBox,从 `fixtures/ads-account.json` 读取 1–30 天离线数据,计算除零安全、定点精度的指标。
- `save_report_draft` 只接受标题/正文,只能原子写当前 Session 的固定 `report.md`
Pi extension 的 `tool_call` preflight 在执行前阻塞 `save_report_draft`。服务器 `ApprovalBroker` 是唯一权威;Approve 才继续,Deny/Timeout/Abort 均 block,且生成 `approval.resolved`。重复相同决定幂等,冲突决定返回 409。前端卡片和 modal 只提交决定并等待 SSE 权威状态。
## 图片多模态链路
Composer 支持选择、拖拽、粘贴、上传状态、移除与对话框预览。自定义 assistant-ui `AttachmentAdapter` 立即流式上传,React 状态只保留 attachmentId 和临时 object URL;移除/卸载会 revoke URL。
服务端只接受 JPEG、PNG、WebP,并同时校验扩展名、声明 MIME 与 magic bytes。默认单图 5 MiB、单消息 4 张,并限制像素;Sharp 自动旋转、移除 EXIF/ICC 等元数据并重新编码。随机文件名只进入 Session 的 `attachments/`。SVG/GIF/PDF/视频、伪 MIME、跨 Session ID 与解压炸弹会被拒绝。
发送时服务器验证归属与 ready 状态,按已解析 Pi Model 的 image capability 拦截 text-only 模型,然后把规范化图片映射为安装版 Pi `ImageContent[]` 并调用 `session.prompt(text, { images })`。base64 只存在于这一步的服务端瞬时调用,不进入事件或 React 全局状态。
## Demo
点击“填入 Demo Prompt”(不会自动执行),或粘贴:
> 请使用 ads-analysis Skill,分析账户 demo-account 最近 7 天数据,找出异常 Campaign,并输出账户总结、关键指标和 3 条优化建议。最后使用 report-writer Skill 生成一份报告草稿。
可附一张广告素材/后台截图并补充:
> 请同时观察我上传的图片,将可见内容作为辅助信号;不要根据图片编造图片中不存在的数据,数值指标以 mock_ads_metrics 返回的数据为准。
预期路径:Skill requested/loaded → mock metrics Tool 卡片 → 分析 → report-writer → save Tool 卡片与审批提醒。Approve 后只写 Session `report.md`;Deny/超时不写文件,聊天仍可呈现正文。10–15 分钟完整讲稿见 [docs/classroom-demo.md](docs/classroom-demo.md)。
## 安全边界
**Pi 没有内置 Sandbox。Project Trust 是项目资源加载防线,不是执行隔离。** Pi、Extension 和 Tool 继承 Node 进程权限。本项目通过最小 Tool allowlist、固定 Session cwd、canonical/realpath 路径守卫、符号链接拒绝、protected-path preflight、固定写入目标、审批、SSRF 检查、图片重编码和日志/事件脱敏降低风险,但这些都不等于 OS Sandbox。
不要把不可信 Skill、脚本、仓库或无人值守任务直接交给本课堂进程。生产环境必须放入 Docker、VM、micro-VM 或远程 Sandbox,并配置受控网络出口。更多边界见 [docs/security-boundaries.md](docs/security-boundaries.md)。
## 版本与 API 核对
编码前核对了 Pi 的 [SDK](https://pi.dev/docs/latest/sdk)、[Skills](https://pi.dev/docs/latest/skills)、[Extensions](https://pi.dev/docs/latest/extensions)、[Security](https://pi.dev/docs/latest/security)、[Models](https://pi.dev/docs/latest/models)、[Providers](https://pi.dev/docs/latest/providers),以及 assistant-ui 的 [Custom Runtime](https://www.assistant-ui.com/docs/runtimes/custom/external-store)、[Tools](https://www.assistant-ui.com/docs/tools) 与 [Attachments](https://www.assistant-ui.com/docs/guides/attachments)。安装后再以 `node_modules` 的公开 `.d.ts` 和可验证行为为准。
| 依赖 | 实装版本 | 使用的公开接口 |
| --- | --- | --- |
| Pi Coding Agent / pi-ai | 0.84.3 | `createAgentSession`, `DefaultResourceLoader`, `SettingsManager.inMemory`, `SessionManager.inMemory`, `ModelRuntime`, `InMemoryCredentialStore`, `defineTool`, `isToolCallEventType` |
| TypeBox | 1.3.7 | Pi Tool 参数 schema |
| assistant-ui React | 0.15.17 | `useExternalStoreRuntime`, Attachment Adapter, Toolkit + `Tools`, Thread/Composer primitives |
| Fastify | 5.12.1 | HTTP、multipart、SSE |
| React / Vite | 19.2.8 / 7.3.6 | Web UI / build |
| Zod | 4.4.3 | HTTP、事件、模型与消息边界 |
发现的实际差异:Pi 文档示例中的图片结构仍展示嵌套 `source`,但 0.84.3 导出的稳定 `ImageContent``{ type: "image", data, mimeType }`,本实现服从安装包类型。Pi 0.84.3 也要求 Node `>=22.19.0`。assistant-ui 包仍导出部分历史 helper,但本项目没有使用弃用的 `makeAssistantTool` / `useAssistantToolUI`,采用稳定 External Store Runtime 和 Toolkit renderers;没有使用 `unstable_*` API。
## 测试与当前结果
默认测试使用 fake `AgentSessionPort`,不需要 API Key、不会访问模型或外网。覆盖 shared schemas、Skill discovery/diagnostics、canonical Skill 事件、Tools、安全路径、Approval 全状态、Session 并发/取消/清理、模型无 fallback/SSRF/脱敏、JPEG/PNG/WebP、伪 MIME/大小/跨 Session、SSE replay/heartbeat/cleanup、assistant-ui reducer/Tool UI/Settings/附件与可访问性。
2026-08-28 本机验证:
- `pnpm lint`:通过,0 warning。
- `pnpm typecheck`4 个 workspace package 全部通过。
- `pnpm test`8 个 test files、88 个 tests 全部通过;其中包含 `.env` 默认模型严格加载、环境 API Key 脱敏、加密凭证按默认模型跨重启恢复、模型元数据恢复、AES-256-GCM 凭证跨 Session 恢复/清除/错误 master key fail-closed、API Key 不进入 Web Storage、Session 初始化竞态、真实重复 Tool/Approval 事件链去重与 UI error fallback,以及使用精确自定义模型、关闭模型网络刷新后创建并 dispose 真实 Pi `AgentSession` 的测试。
- `pnpm build`shared、harness、server、web 全部通过;Vite 提示单一 JS chunk 约 603 kB,可在生产化时拆包,但不影响构建。
- `PORT=3010 pnpm --filter @agent-studio/server start`:编译产物启动成功,`GET /health` 返回 200,随后 SIGINT 优雅关闭。
人工浏览器验收:桌面三栏、无模型禁发、Settings password 显隐/脱敏回显、Vision badge、Demo Prompt 只填不发、SSE Timeline、390px 移动端 Tabs 与无横向溢出均通过;应用控制台无 error/warn。浏览器自动化环境拒绝设置本地 file chooser,因此上传点击的人工自动化未完成,实际 multipart 上传、附件 adapter、图片预览/移除和 text-only 拦截由通过的 Server/Web 测试覆盖。
## 已知限制与生产化
课堂版服务端仍是单进程:重启会丢 Session/Event/Approval,但模型 API Key 可通过本地 AES-256-GCM 密文恢复;浏览器只保留非敏感模型元数据。当前文件 vault 没有多用户/租户隔离、远程 KMS、密钥轮换或分布式锁。SSE 无跨实例协调;没有用户身份、CSRF、分布式 rate limit、持久审计、对象存储/KMS 或 OS Sandbox。自定义 provider 的 Vision 声明会被服务器注册成精确 Pi Model 并按该模型检查,但课堂版不会主动发远端 capability probe。
生产化路线见 [docs/production-roadmap.md](docs/production-roadmap.md)Redis/Postgres、对象存储、KMS、身份鉴权与 CSRF、速率/配额、容器/VM Sandbox、出口代理、不可变审计日志、队列/worker、分布式 SSE,以及完整可观测性。
## SW Ads 图片日报
在服务器 `.env` 配置 `SWADS_MCP_URL`(默认 `https://ads.mincode.cn/mcp`)、`SWADS_MCP_TOKEN` 和可选 `SWADS_MCP_TIMEOUT_MS`(默认 30000,范围 100120000)。Token 只在服务端保存,勿粘贴到聊天或提交 Git。URL 启动时校验;缺少 Token 不影响现有 mock ads、附件和报告草稿功能,首次 SW Ads 调用返回 `SWADS_NOT_CONFIGURED`
发送 `/swads-daily-report`,或 `/swads-daily-report 账户 demo-account,日期 2026-09-06`。该内置 Skill 仅显式调用,保留原始用户消息。首版仅支持 TikTok GMV Max:默认当前账户最近完整业务日,之前七个完整业务日作比较;账户时区、币种和账户范围来自真实 Gateway。查询前探测线上 `tools/list`,要求只读 whoami、metrics catalog、semantic query 及实际 TikTok 指标能力;能力缺失、鉴权失败或空数据时停止生成。
Agent 先输出简洁 Markdown 分析,再生成 PNG 长图,图片可点击预览;Tool 卡提供 PNG、HTML、JSON 下载。服务器通过固定 Chrome CLI 参数和 Sharp 截图检查生成图片,不需要 Playwright/Puppeteer。可用 `CHROME_BIN` 指定绝对可执行路径,否则尝试固定系统 Chrome/Chromium 路径。浏览器缺失、超时或达到画布上限仍不完整时保留 HTML/JSON,并显示 `CHROME_UNAVAILABLE``PNG_TIMEOUT``PNG_RENDER_FAILED``PNG_TRUNCATED`
产物原子保存于 `workspace/reports/<accountKey>/<YYYY-MM-DD>/<reportId>/`,不随 Session reset/delete 清理,不进入 Git。报告 URL 没有目录列表,PNG inlineHTML/JSON attachment。当前版本没有用户认证,随机 ID 只是内部/本地部署下的非枚举地址;持有 URL 即可访问。公网多用户部署需要另行实现认证、授权与租户隔离。存储保留与容量清理由部署方管理。
`swads_cli` 是服务端受控 CLI facadeAgent 不接收 Gateway 的逐项 MCP Tools,也不能执行 shell,但 facade 仍使用官方 MCP Streamable HTTP client 连接 MCP Gateway。该功能不执行任何平台写操作;所有预算、状态、目标和发布建议都列入人工确认。