一个运行在 Anna App 平台上的迷你笔记应用。用户可以创建、查看、删除笔记,并通过本地 Executa Tool 调用 Host LLM 生成笔记总结。
本项目的核心目的是验证对 Anna App 平台模型的理解——不是普通 Web App,所有数据读写和 LLM 调用都必须通过 Anna Host API。
Anna App iframe
→ AnnaAppRuntime.connect() ← 连接 Host
→ anna.storage.get / anna.storage.set ← 读写笔记
→ anna.tools.invoke("tool-dev-mini-notes") ← 调用 Executa Tool
→ Executa Tool (JSON-RPC 2.0 / stdio) ← 本地工具进程
→ sampling/createMessage (reverse JSON-RPC) ← 借用 Host LLM
→ summary 返回 UI
mini-notes-app/
├── manifest.json # Anna App 清单(schema/permissions/ui/dev)
├── app.json # 应用元信息
├── package.json # pnpm workspace root
├── pnpm-workspace.yaml
│
├── ui/ # 前端项目(React + TypeScript + Vite)
│ ├── index.html
│ ├── vite.config.ts
│ └── src/
│ ├── main.tsx # 入口
│ ├── App.tsx # 根组件(连接状态 + 组件组合)
│ ├── App.css # 全局样式
│ ├── types.ts # Note, InvokeEnvelope
│ ├── anna/
│ │ ├── runtime.ts # AnnaAppRuntime.connect() 封装
│ │ ├── storage.ts # anna.storage.get/set 封装
│ │ └── tools.ts # anna.tools.invoke 封装
│ ├── hooks/
│ │ └── useNotes.ts # 笔记 CRUD 状态管理
│ └── components/
│ ├── NoteInput.tsx # 输入框 + 保存
│ ├── NoteList.tsx # 笔记列表
│ ├── NoteItem.tsx # 单条笔记(含删除)
│ ├── EmptyState.tsx # 空状态
│ ├── ErrorBanner.tsx # 错误提示
│ └── Summarize.tsx # 总结按钮 + 结果
│
├── executas/
│ └── tool-dev-mini-notes/ # Executa Tool(Node.js + TypeScript)
│ ├── executa.json # Executa manifest
│ ├── package.json # sentinel(node type)
│ ├── tsconfig.json
│ ├── scripts/
│ │ └── bundle.mjs # esbuild + SEA 打包脚本
│ └── src/
│ ├── index.ts # 入口(stdio 事件循环)
│ ├── rpc.ts # JSON-RPC 2.0 框架
│ ├── types.ts # 类型定义
│ └── handlers/
│ ├── initialize.ts # v2 协商 + sampling 能力声明
│ ├── describe.ts # 工具 manifest
│ ├── invoke.ts # summarize 处理 + sampling 调用
│ ├── health.ts # 健康检查
│ └── shutdown.ts # 优雅退出
│
├── fixtures/
│ └── sampling-mock.jsonl # Mock sampling fixture
│
├── scripts/
│ ├── build-executa.sh # 本机二进制打包(macOS/Linux)
│ ├── build-executa.ps1 # 本机二进制打包(Windows)
│ ├── test-rpc.sh # JSON-RPC 手动测试(21 项)
│ └── test-integration.md # 集成测试指南
│
└── .github/
└── workflows/
└── release.yml # 三平台构建 + GitHub Release
- Node.js >= 18
- pnpm >= 9(
npm install -g pnpm) - uv(Python 包运行器,
anna-app dev需要)# macOS/Linux curl -LsSf https://astral.sh/uv/install.sh | sh # Windows PowerShell powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
- @anna-ai/cli(Anna App 开发 CLI)
npm install -g @anna-ai/cli
pnpm install# 仅构建前端
pnpm --filter ui build
# 构建全部(前端 + Executa)
pnpm build前端构建产物输出到 bundle/ 目录,这是 Anna App 加载的静态文件。
pnpm build && anna-app validate --strict预期输出:✓ validate passed
pnpm dev
# 等价于:pnpm build && anna-app dev --no-llm打开 http://localhost:5180/,你应该看到:
- Mini Notes App 加载
- "✅ 已连接到 Anna App Runtime"
- 输入框和空状态
在 UI 中测试:
- 输入笔记文字 → 点击保存(或按 Enter)→ 笔记出现在列表中
- 点击笔记右侧的 ✕ 删除笔记
- 点击 "✨ Summarize 全部笔记" → 预期显示错误
--no-llm禁用了 Host 的 LLM/sampling 能力。当 Executa Tool 发起sampling/createMessage反向请求时,Harness 拒绝该请求并返回错误[-32603] harness started with --no-llm。这是预期行为——说明
anna.tools.invoke → Executa → sampling的 完整链路是通的,只是 LLM 被禁用了。后端 sampling 的正确性通过下面的 mock fixture 单独验证。
pnpm executa:dev
# 等价于:anna-app executa dev \
# --dir executas/tool-dev-mini-notes \
# --mock-sampling fixtures/sampling-mock.jsonl \
# --invoke summarize \
# --args '{"notes":[{"content":"明天跟客户 follow up"},{"content":"修复登录 bug"},{"content":"Workshop 内容想法"}]}'预期输出:
{
"success": true,
"data": {
"summary": "根据你记录的笔记,主要涉及三个方面:1)客户跟进与商务沟通..."
}
}确认 sampling/createMessage 被发起:查看 stderr 日志,应包含:
Sending sampling/createMessage id=sampling-...
Sampling complete: model=mock-model, length=...
这证明 Executa Tool 确实发起了反向 sampling/createMessage 请求,并且从 fixture 中获得了模拟的 LLM 响应。
bash scripts/test-rpc.sh覆盖 initialize、describe、invoke、health、错误处理(未知 method、无效 JSON),共 21 项断言。
- 打开
ui/src/anna/storage.ts - 所有笔记读写都通过
anna.storage.get({ key: "notes" })和anna.storage.set({ key: "notes", value: [...] }) - 前端代码中不存在
localStorage、IndexedDB或直接fetch调用 - 浏览器 DevTools Network 面板可观测
postMessage通信
调用链追踪:
NoteInput.onSave
→ useNotes.addNote(content)
→ saveNotes(newNotes) ← storage.ts:31
→ anna.storage.set({ key, value }) ← Host API
useNotes.refresh (on mount)
→ loadNotes() ← storage.ts:17
→ anna.storage.get({ key }) ← Host API
NoteItem.onDelete
→ useNotes.removeNote(index)
→ saveNotes(filtered) ← storage.ts:31
→ anna.storage.set({ key, value }) ← Host API
- 打开
ui/src/anna/tools.ts - Summarize 通过
anna.tools.invoke({ tool_id: "tool-dev-mini-notes", method: "summarize", args: { notes } })调用 - Executa Tool 的
invoke.ts处理请求并发起sampling/createMessage反向 RPC fixtures/sampling-mock.jsonl提供 mock LLM 响应
调用链追踪:
Summarize.handleSummarize
→ summarizeNotes(notes) ← tools.ts:19-22
→ anna.tools.invoke({ tool_id, args }) ← Host API
→ Executa invoke handler ← invoke.ts
→ sendRequest(sampling/createMessage) ← 反向 JSON-RPC
→ Host mock fixture 响应 ← sampling-mock.jsonl
→ { success: true, data: { summary } }
使用 esbuild 将 TypeScript 源码打包为单个 CommonJS 文件,再使用 Node.js SEA(Single Executable Applications)将打包结果注入为独立可执行二进制。
TypeScript 源码
→ esbuild bundle(单文件 CJS)
→ node --experimental-sea-config(SEA blob)
→ postject(注入 node 二进制)
→ archive(tar.gz / zip)
# macOS / Linux
bash scripts/build-executa.sh --archive
# Windows PowerShell
.\scripts\build-executa.ps1 -Archive产物输出到 executas/tool-dev-mini-notes/sea/:
tool-dev-mini-notes-{platform}.tar.gz(macOS/Linux)tool-dev-mini-notes-{platform}.zip(Windows)
tool-dev-mini-notes-{platform}.{tar.gz|zip}
└── tool-dev-mini-notes-{platform}/
├── manifest.json ← 二进制分发 manifest(tool_id, platform, entrypoint)
└── bin/
└── tool-dev-mini-notes{.exe} ← 独立可执行二进制
支持的 platform key:
darwin-arm64darwin-x86_64windows-x86_64
| 触发 | 行为 |
|---|---|
workflow_dispatch(手动) |
构建三平台二进制,上传为 workflow artifacts |
push tag v*(推送标签) |
构建三平台二进制,创建 GitHub Release 并上传 assets |
一次发布生成三个平台的产物:
| Platform | Asset |
|---|---|
| macOS Apple Silicon | tool-dev-mini-notes-darwin-arm64.tar.gz |
| macOS Intel | tool-dev-mini-notes-darwin-x86_64.tar.gz |
| Windows x64 | tool-dev-mini-notes-windows-x86_64.zip |
每个平台的 job 包含构建后的 smoke test(describe 验证响应包含正确的 tool_id)。
| 概念 | 说明 | 在本项目中 |
|---|---|---|
| manifest.json | Anna App 清单文件,声明 App 的权限、UI、Executa 依赖和本地开发配置 | 根目录 manifest.json,声明了 tools.invoke、storage.* 权限 |
| bundle | 前端静态文件(HTML + JS + CSS),由 Vite 构建 | bundle/ 目录,manifest.json 的 ui.bundle.entry 指向 index.html |
| Executa Tool | 本地工具进程,通过 JSON-RPC 2.0 over stdio 与 Anna Host 通信 | executas/tool-dev-mini-notes/,type 为 node |
| Anna Storage / APS KV | Anna 平台的键值存储 API,App 通过 anna.storage.get/set 读写数据 |
笔记存储在 key "notes" 下,本地无 login 时使用 legacy in-memory runtime_state |
| Sampling | Executa Tool 通过反向 JSON-RPC sampling/createMessage 向 Host 请求 LLM 生成 |
invoke.ts 中发起 sampling 请求,本地测试用 --mock-sampling fixture |
| Binary Archive | 将 Executa Tool 打包为独立可执行二进制,用于分发 | scripts/build-executa.* + GitHub Actions 生成三平台二进制 |
manifest.json
├── 声明 required_executas → tool-dev-mini-notes
├── 声明 ui.bundle → bundle/index.html (Vite 构建产物)
├── 声明 ui.host_api → storage: [get, set]
│ → tools: [tool-dev-mini-notes]
└── 声明 dev → 本地开发配置
Anna App Runtime (Host)
├── 加载 bundle/ → 前端 iframe
├── 注入 SDK → /static/anna-apps/_sdk/latest/index.js
├── 提供 storage API → anna.storage.get/set → legacy memory (dev)
├── 路由 tools.invoke → 启动 Executa Tool (JSON-RPC/stdio)
└── 响应 sampling → mock fixture (dev) 或真实 LLM (prod)
- PLANNING.md — 完整 14 章节项目规划
- PLAN.md — 7 阶段开发计划
- TASKS.md — 60+ 项任务清单
- DECISIONS.md — 技术决策记录
- CHANGELOG.md — 修改记录
- scripts/test-integration.md — 集成测试指南
- ❌ 真实 Anna 账号 /
anna-app login - ❌ 真实 LLM API key
- ❌ 云端 APS 存储
- ❌ 笔记持久化跨 dev 重启(legacy in-memory runtime_state)
- ❌ 线上发布 Anna App
- ❌ 代码签名 / 公证 / Windows Authenticode
- ❌ 编辑、搜索、标签、多笔记本、富文本、附件、协同
MIT