Skip to content

Latest commit

 

History

History
376 lines (293 loc) · 12.6 KB

README.md

File metadata and controls

376 lines (293 loc) · 12.6 KB

Mini Notes with LLM Summary

一个运行在 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

构建

构建前端 bundle

# 仅构建前端
pnpm --filter ui build

# 构建全部(前端 + Executa)
pnpm build

前端构建产物输出到 bundle/ 目录,这是 Anna App 加载的静态文件。

校验 manifest

pnpm build && anna-app validate --strict

预期输出:✓ validate passed


本地开发与测试

1. 启动 UI Harness(--no-llm 模式)

pnpm dev
# 等价于:pnpm build && anna-app dev --no-llm

打开 http://localhost:5180/,你应该看到:

  • Mini Notes App 加载
  • "✅ 已连接到 Anna App Runtime"
  • 输入框和空状态

在 UI 中测试:

  1. 输入笔记文字 → 点击保存(或按 Enter)→ 笔记出现在列表中
  2. 点击笔记右侧的 ✕ 删除笔记
  3. 点击 "✨ Summarize 全部笔记" → 预期显示错误

为什么 --no-llm 下 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 单独验证。

2. 后端 Executa Sampling 测试(Mock)

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 响应。

3. 手动 JSON-RPC 测试

bash scripts/test-rpc.sh

覆盖 initialize、describe、invoke、health、错误处理(未知 method、无效 JSON),共 21 项断言。


确认调用链路

确认 notes 存储走 anna.storage.*

  1. 打开 ui/src/anna/storage.ts
  2. 所有笔记读写都通过 anna.storage.get({ key: "notes" }) 和 anna.storage.set({ key: "notes", value: [...] })
  3. 前端代码中不存在 localStorage、IndexedDB 或直接 fetch 调用
  4. 浏览器 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

确认 summary 走 anna.tools.invoke → Executa → sampling/createMessage

  1. 打开 ui/src/anna/tools.ts
  2. Summarize 通过 anna.tools.invoke({ tool_id: "tool-dev-mini-notes", method: "summarize", args: { notes } }) 调用
  3. Executa Tool 的 invoke.ts 处理请求并发起 sampling/createMessage 反向 RPC
  4. 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)

Archive 结构

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-arm64
  • darwin-x86_64
  • windows-x86_64

GitHub Actions 发布

触发方式

触发 行为
workflow_dispatch(手动) 构建三平台二进制,上传为 workflow artifacts
push tag v*(推送标签) 构建三平台二进制,创建 GitHub Release 并上传 assets

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)。


Anna App 平台概念解释

概念 说明 在本项目中
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)

更多文档


不要求

  • ❌ 真实 Anna 账号 / anna-app login
  • ❌ 真实 LLM API key
  • ❌ 云端 APS 存储
  • ❌ 笔记持久化跨 dev 重启(legacy in-memory runtime_state)
  • ❌ 线上发布 Anna App
  • ❌ 代码签名 / 公证 / Windows Authenticode
  • ❌ 编辑、搜索、标签、多笔记本、富文本、附件、协同

License

MIT