# 开发登录 Mini App <Badge type="warning" text="尚未发布" />

从零做出一个 Mini App：用这个人的 Micromeet 账号登录，再从它自己的后端以这个人的身份调用 Micromeet。每个函数的说明见[登录](https://docs.micromeet.ai/zh/miniapp-sdk/auth.md)，打包见[打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md)，包的全部规则见[包规范](https://docs.micromeet.ai/zh/miniapp-sdk/package-spec.md)。

## 整体流程

从第一次构建到用户打开 Mini App，谁做什么：

```mermaid
flowchart TB
  subgraph dev["你"]
    build["vite build<br/>miniapp-package/"]
    verify["micromeet-miniapp verify"]
    zip["micromeet-miniapp zip<br/>签名的 zip"]
    file["在后端域名上<br/>放验证文件"]
  end
  subgraph mm["Micromeet"]
    reg["登记 AppID<br/>每个区一个 client secret"]
    dom["验证服务器域名"]
    sign["登记并签名这个版本<br/>核对每个文件和域名"]
  end
  subgraph cw["Cowork"]
    install["安装"]
    open{"签名有效、<br/>文件没改过？"}
    run["打开 Mini App"]
    refuse["提示未经认证，<br/>不打开"]
  end
  reg --> file --> dom
  build --> verify --> sign
  dom --> sign
  sign -->|签好的 manifest.json| zip --> install --> open
  open -->|是| run
  open -->|否| refuse
```

Cowork 每次打开都验签名，页面每读一个文件都核对；文件被改过就关闭这个 Mini App。

用户点了你的登录按钮之后：

```mermaid
sequenceDiagram
  actor P as 用户
  participant Page as 页面
  participant C as Cowork
  participant M as Micromeet
  participant B as 你的后端
  P->>Page: 点登录
  Page->>C: auth.login()
  C->>M: 这个 Mini App 是谁、能请求什么
  C->>P: 确认卡（第一次）：Mini App、scope、账号
  P->>C: 授权
  C->>M: 为这个版本和这个人要 code
  Note over M: AppID 启用、版本已登记、<br/>文件一致、有验证过的服务器域名、scope 允许
  M-->>C: code（5 分钟，只能用一次）
  C-->>Page: code 和 region
  Page->>B: code 和 region
  B->>M: code 和 client secret
  M-->>B: access token、refresh token、user.ref
  B->>M: 用 access token 读 profile
  M-->>B: 姓名、邮箱、机构
  B-->>Page: 你自己的会话
```

页面始终拿不到密码、token 和 client secret。下面的步骤逐个做出每一部分。

## 1. 请 Micromeet 登记这个 Mini App

不能自助登记。把这些发给 Micromeet：

| 内容 | 示例 |
|---|---|
| AppID：小写反向域名 | `ai.example.app` |
| 名称（英文；中文可选）和开发方 | Example · Example Ltd |
| 需要的 scope | `profile:read` |
| 登录用户所在的区（SG、HK、ID） | HK |
| 后端的地址 | `https://api.example.com` |

每个区给你一个 **client secret**，只显示一次：存进后端的密钥存储。每个后端地址还会给你一个验证文件（第 2 步）。

## 2. 证明后端域名是你的

把这个文件放在 `https://api.example.com/micromeet-miniapp-<16 个字符>.txt`，用 HTTPS 返回 200，不能跳转，并一直留着；Micromeet 取回它来验证域名。开发时，后端可以跑在 `http://localhost:<端口>` 或 `http://127.0.0.1:<端口>`，在 Micromeet 允许的区不用放文件。见[打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md)里的「服务器域名」。

## 3. 写页面

安装 `@micromeet-ai/miniapp-sdk`（见[打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md)里的「构建」），然后：

```js
import { maestroSdk } from '@micromeet-ai/miniapp-sdk';

const BACKEND = 'https://api.example.com';      // 验证过的服务器域名

export async function signIn() {
  const sdk = maestroSdk();                     // 不在 Cowork 里时为 null
  if (!sdk?.auth) return { error: 'open_in_cowork' };
  const login = await sdk.auth.login({ scopes: ['profile:read'] });
  if (!login.ok) return { error: login.error };
  const response = await fetch(`${BACKEND}/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ code: login.code, region: login.region })
  });
  return response.json();                       // { session, name }：之后的请求带上 session
}
```

- 在用户点击时调用 `auth.login()`：Cowork 只在用户正在看的标签页上弹出确认卡。
- `auth.status()` 说明包是否已签名、Cowork 是否有人登录，可以用来决定是否显示登录按钮。
- 脚本都放在文件里：内联 `<script>` 不会运行。

## 4. 写后端

一个最小的 Node.js 后端：

```js
import { createServer } from 'node:http';
import { randomUUID } from 'node:crypto';
import { createMiniappAuthClient, MiniappAuthError } from '@micromeet-ai/miniapp-sdk/server';

const APP_ID = 'ai.example.app';
const PAGE = `miniapp://${APP_ID}`;             // 页面的 origin
// 每个区一个 client，用那个区的 secret。
const clients = { HK: createMiniappAuthClient({ appId: APP_ID, clientSecret: process.env.MINIAPP_SECRET_HK }) };
const sessions = new Map();                     // 实际存进你的数据库

const reply = (res, status, body) => {
  res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': PAGE, Vary: 'Origin' });
  res.end(JSON.stringify(body));
};

createServer((req, res) => {
  if (req.method === 'OPTIONS') {
    res.writeHead(204, { 'Access-Control-Allow-Origin': PAGE, 'Access-Control-Allow-Methods': 'POST', 'Access-Control-Allow-Headers': 'Content-Type, Authorization' });
    return res.end();
  }
  if (req.method !== 'POST' || req.url !== '/login') return reply(res, 404, { error: 'not_found' });
  let raw = '';
  req.on('data', (part) => (raw += part));
  req.on('end', async () => {
    try {
      const { code, region } = JSON.parse(raw);
      const auth = clients[region];
      if (!auth) return reply(res, 400, { error: 'region_not_supported' });
      const tokens = await auth.exchangeCode({ code, region });
      const session = randomUUID();
      sessions.set(session, { ref: tokens.user.ref, region, refreshToken: tokens.refreshToken });
      reply(res, 200, { session, name: tokens.user.name });
    } catch (error) {
      reply(res, 400, { error: error instanceof MiniappAuthError ? error.code : 'bad_request' });
    }
  });
}).listen(8787);
```

- 回答 `Access-Control-Allow-Origin: miniapp://<appId>`：这是页面的 origin，没有这个响应头，请求在页面里就失败了，到不了你的代码。
- 用 `user.ref` 当用户的键，存好 refresh token，之后要调用 Micromeet 时用 `refresh` 换新 token。
- 页面只拿到你自己的会话；Micromeet 的 token 留在后端。
- 部署在你验证过的服务器域名上，开发时跑在 localhost。其他语言的后端做同样的调用（见[登录](https://docs.micromeet.ai/zh/miniapp-sdk/auth.md)里的「HTTP 调用」）。

## 5. 打包、签名、安装

1. 在 Vite 插件里写 `domains: ['https://api.example.com']`，然后 `vite build`（见[打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md)）。
2. 运行 `micromeet-miniapp verify miniapp-package`，再把包交给 Micromeet；Micromeet 在每个区登记并签名这个版本，交回签好的 `manifest.json`，把它放回包里。
3. 运行 `micromeet-miniapp verify miniapp-package --signed`，然后 `micromeet-miniapp zip miniapp-package`。
4. 在 Cowork 里让 Agent 安装这个 zip，打开 Mini App，点登录。

正式版 Cowork 只打开 Micromeet 正式环境签名的版本；没有不签名的开发模式。

## 6. 开发与测试

- 不需要登录的界面和逻辑：在浏览器里 `vite dev`。那里 `maestroSdk()` 是 `null`，显示一句提示代替登录按钮。
- 只有页面的文件被签名：改后端不需要新版本。页面尽量简单，逻辑放在后端。
- 改了页面的任何文件或 `domains`，都要换新的 `version`，重新登记、签名、安装。
- 端到端测试登录需要：签好名的版本，加上登录了已登记区账号的 Cowork。
- 开发用的版本可以只写 localhost 域名；发布的版本不要带 localhost：不允许 localhost 的区会因此拒绝登录（`no_server_domain`）。

## 出错怎么办

| 在哪里 | 看到什么 | 原因和处理 |
|---|---|---|
| Cowork | 提示这个 Mini App 未经认证，打不开 | 包没有签名、由别的 Micromeet 环境签名，或签名后改过文件：安装签好名的 zip |
| 页面 | `app_not_available` | 用户所在的区没有登记这个 AppID，或已停用 |
| 页面 | `version_not_available` | 这个版本没有在那个区登记，或已停用 |
| 页面 | `no_server_domain` | 这个版本在那个区没有验证过的服务器域名，或验证文件不在了 |
| 页面 | `scope_denied` | 请求了 Micromeet 没有给这个 Mini App 的 scope |
| 页面 | `fetch` 报跨域（CORS）错误 | 后端没有回答 `Access-Control-Allow-Origin: miniapp://<appId>` |
| 后端 | `invalid_client` | 这个区的 secret 不对，或已被更换 |
| 后端 | `invalid_grant` | code 已经用过、超过 5 分钟，或不是这个 AppID 的 |

## 相关

- [登录](https://docs.micromeet.ai/zh/miniapp-sdk/auth.md): 全部登录函数、后端工具和错误
- [打包与签名](https://docs.micromeet.ai/zh/miniapp-sdk/build.md): 做出包并交给 Micromeet 签名
- [包规范](https://docs.micromeet.ai/zh/miniapp-sdk/package-spec.md): 包必须满足的全部规则
