Skip to main content
Glama
README.md
# PWA Sticker MCP · 共享表情包

让人和 AI 共用同一个 PWA 表情包库:人在抽屉里点一下发送,模型通过 MCP 调同一张图发送。

The human-facing PWA drawer and the AI-facing MCP server share one sticker library and one attachment contract.

这个公开版从真实家庭 PWA 的现役功能中独立抽出,保留了最有用的部分:

- PWA 表情包抽屉:上传、懒加载、最近使用排序、长按或键盘进入管理、删除
- iOS 友好的正方形网格:不依赖滚动 grid 中容易塌陷的 `aspect-ratio`
- 静态 WebP 抽屉预览:有 `ffmpeg` 时自动生成 192px 首帧,实际发送仍是原图或原 GIF
- MCP 三工具:`list_stickers`、`send_sticker`、`add_sticker`
- 图片-only 消息:发送 `{ text: "", attachments: [...] }`,不必伪造一段占位文字
- 一个可以直接运行的本地 Relay/demo,方便先看完整闭环再接自己的聊天后端

仓库不包含我们家的表情图片、聊天记录、域名、账号或凭据。

## 先跑起来

需要 Node.js 20+;`ffmpeg` 可选。

```bash
npm install
npm run demo
```

打开 <http://127.0.0.1:8787>,添加一张图片并点它,页面会显示刚发出的 image-only 消息。
数据默认写在项目内的 `.data/`,已经被 `.gitignore` 排除。

这次运行同时给 MCP 提供了可用 Relay。另开一个终端:

```bash
STICKER_RELAY_URL=http://127.0.0.1:8787 npm run mcp
```

## 接入 MCP 客户端

Claude Desktop、Claude Code 或其他支持 stdio MCP 的客户端可使用:

```json
{
  "mcpServers": {
    "stickers": {
      "command": "node",
      "args": ["/absolute/path/to/pwa-sticker-mcp/src/mcp-server.js"],
      "env": {
        "STICKER_RELAY_URL": "https://chat.example.com",
        "STICKER_TOKEN": "your-relay-token",
        "STICKER_STICKERS_PATH": "/api/stickers",
        "STICKER_UPLOAD_PATH": "/api/upload",
        "STICKER_SEND_PATH": "/api/send"
      }
    }
  }
}
```

工具语义:

- `list_stickers(query?)`:列出共享库,可按名字过滤
- `send_sticker(query)`:按 id、全名或唯一关键词匹配并发送
- `add_sticker(path, name?)`:把模型所在机器上的本地图片登记进共享库,但不发送

关键词命中多张时工具会请模型收窄,不会擅自挑一张。

## 把抽屉装进现有 PWA

引入模块和样式:

```html
<link rel="stylesheet" href="/sticker-drawer.css">

<div id="stickerDrawer"></div>
<button
  id="stickerButton"
  type="button"
  aria-controls="stickerDrawer"
  aria-expanded="false"
  aria-pressed="false"
>
  表情包
</button>

<script type="module">
  import { StickerDrawer } from '/sticker-drawer.js'

  new StickerDrawer({
    root: document.querySelector('#stickerDrawer'),
    trigger: document.querySelector('#stickerButton'),
    apiBase: 'https://chat.example.com',
    headers: () => ({ Authorization: `Bearer ${getRelayToken()}` }),
    async onSend(attachment) {
      await sendMessage({ text: '', attachments: [attachment] })
    },
    onNotice(message, kind) {
      showToast(message, kind)
    }
  })
</script>
```

省略 `onSend` 时,组件会把相同 payload POST 到 `send` endpoint。端点可改名:

```js
new StickerDrawer({
  root,
  trigger,
  endpoints: {
    stickers: '/app/stickers',
    upload: '/app/upload',
    send: '/app/send'
  }
})
```

组件使用中性的 CSS 变量,不要求接入者采用 demo 的颜色:

```css
:root {
  --psm-surface: #171717;
  --psm-surface-muted: #242424;
  --psm-ink: #fafafa;
  --psm-muted: #b8b8b8;
  --psm-accent: #f1b657;
  --psm-danger: #e26363;
  --psm-line: #343434;
}
```

## Relay API 合同

现有后端不必使用 demo server,只要实现下面五个请求:

```text
GET    /api/stickers
POST   /api/upload?name=<filename>        raw image body
POST   /api/stickers                      register uploaded metadata
POST   /api/stickers/<id>/use             update recent-use order
DELETE /api/stickers/<id>
POST   /api/send                          send an image attachment
```

`GET /api/stickers` 返回:

```json
{
  "stickers": [
    {
      "id": "abc123",
      "name": "wave hello",
      "url": "/uploads/wave.gif",
      "thumb_url": "/uploads/sticker-thumb-wave.webp",
      "mime": "image/gif",
      "width": 320,
      "height": 240,
      "last_used": 1787800000
    }
  ]
}
```

`POST /api/send` 的核心 payload:

```json
{
  "text": "",
  "attachments": [
    {
      "url": "/uploads/wave.gif",
      "name": "wave hello",
      "mime": "image/gif",
      "kind": "image",
      "width": 320,
      "height": 240
    }
  ]
}
```

如果你的聊天 API 字段不同,在 PWA 的 `onSend` 里转换一次,并让 MCP 的
`STICKER_SEND_PATH` 指向一个接受上述 payload 的轻量适配端点即可。

## 本地 Relay 配置

```bash
HOST=127.0.0.1 \
PORT=8787 \
STICKER_DATA_DIR=/path/to/data \
STICKER_TOKEN=choose-a-token \
npm run demo
```

- 默认只监听 `127.0.0.1`
- 设置 `STICKER_TOKEN` 后,所有 `/api/*` 请求要求同一个 token:MCP 使用 Bearer;打开 demo 首页时,
  本地服务器会写入同值的 HttpOnly/SameSite cookie,所以浏览器 demo 仍能直接使用
- `STICKER_FFMPEG=off` 可关闭缩略图生成;未安装 `ffmpeg` 时也会自动退回原图预览
- 删除库条目不会删除已经上传的原图,因为旧消息仍可能引用它

## 测试

```bash
npm test
npm run check
```

聚焦测试覆盖共享库排序/删除/重复登记,以及“本地图片 → 上传 → 入库 → MCP 同构发送 payload”的完整路径。

## 来历与署名

它诞生于一个家庭 AI 伴侣项目:人可以在手机 PWA 里攒表情包,AI 也能在语气到了时自然发同一套图。

公开版由 **词词** 发起并授权;**Sunnymilk(Sunny,家里的 Codex)** 从现役实现中抽取、泛化、补齐 MCP 壳与独立 demo。

Open-source edition initiated by **Cici** and extracted/generalized by **Sunnymilk (Sunny, the household Codex)**.

## License

MIT