> ## Documentation Index
> Fetch the complete documentation index at: https://agent.minimaxi.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Node 运行时

> 入口模块导出 start(context)。宿主负责拉起进程、分配端口、鉴权和回收。

## 入口契约

* 入口是一个导出 `start(context)` 的 ES 模块。HTTP 监听开始接受连接、所有声明的路由都装好之后，再让 `start` 返回。这就是就绪信号，不需要自己写健康检查接口。
* `start` 里只做路由注册和启动监听，不要同步拉业务数据，业务数据在请求里按需加载。
* 返回 `{ dispose }`。`dispose` 关闭监听、SSE / WebSocket 连接、子进程、定时器、订阅和持久化句柄。它不是每次请求结束时的回调。
* 入口进程是宿主直接管理的进程，Mini App 可用期间必须一直活着，并占住分配给它的端口。
* 运行时不需要安装任何 Mini App SDK。Skill 提供的 `miniapp-api.ts` 只用于类型检查。

## context 字段

| 字段 | 说明 |
| - | - |
| `pluginId` / `pluginRoot` | 已接纳的插件身份和运行副本所在目录 |
| `dataDir` | 宿主为这个插件隔离的数据目录。运行时状态写在这里，不要写进工作区或载荷 |
| `listen` | 分配的回环地址 `{ host: '127.0.0.1', port }`。不要自己选端口或探测端口 |
| `signal` | 取消启动、请求和后台工作的 `AbortSignal` |
| `logger` | 结构化日志，传稳定的事件码和不含敏感信息的字段 |
| `hostConnector?` | manifest 声明且宿主授权后才存在，见[接入数据](/docs/miniapp/authors/data#host-connector) |
| `agent?` | 声明 `agentAccess` 后才存在，见 [Agent API](/docs/miniapp/authors/agent#agent-api) |

## 最小入口

```javascript miniapp/node/server.mjs theme={null}
// @ts-check
import { readFile } from 'node:fs/promises';
import { createServer } from 'node:http';
import { join } from 'node:path';

/** @typedef {import('./miniapp-api.js').MiniAppContext} MiniAppContext */
/** @typedef {import('./miniapp-api.js').MiniAppLifecycle} MiniAppLifecycle */

/**
 * @param {MiniAppContext} context
 * @returns {Promise<MiniAppLifecycle>}
 */
export async function start(context) {
  const clientEntry = await readFile(join(context.pluginRoot, 'miniapp/client/index.html'));
  const server = createServer((request, response) => {
    const url = new URL(request.url ?? '/', 'http://miniapp.local');
    if (request.method === 'GET' && url.pathname === '/dashboard') {
      response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
      response.end(clientEntry);
      return;
    }
    response.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
    response.end(JSON.stringify({ error: 'not_found' }));
  });

  await listen(server, context.listen.host, context.listen.port);
  context.logger.info('miniapp.runtime.listening');

  let disposed = false;
  const dispose = async () => {
    if (disposed) return;
    disposed = true;
    context.signal.removeEventListener('abort', onAbort);
    await close(server);
  };
  const onAbort = () => void dispose();
  context.signal.addEventListener('abort', onAbort, { once: true });
  if (context.signal.aborted) await dispose();

  return { dispose };
}

function listen(server, host, port) {
  return new Promise((resolve, reject) => {
    const onError = (error) => reject(error);
    server.once('error', onError);
    server.listen(port, host, () => {
      server.off('error', onError);
      resolve();
    });
  });
}

function close(server) {
  return new Promise((resolve, reject) => {
    server.close((error) => (error ? reject(error) : resolve()));
  });
}
```

## 什么时候运行

* **按需启动：**`@` 选中 Mini App、打开页面、`publish` 或 `restart` 时启动。启用插件不会启动它。
* **共享服务：**同一个 Mini App 只有一个 Node 进程，多个会话共用，每个会话有自己的页面，见 [Mini App 与会话](/docs/miniapp/sessions)。
* **容量：**同时最多运行 3 个不同的 Mini App 服务。第 4 个会返回 `BUSY`；更新已在运行的 Mini App 不额外占名额。
* **停止：**`stop` 只停服务，插件和同包能力都保留。关闭运行中 Mini App 的标签并确认后，会停止它的共享服务，并关闭所有会话里这个插件的页面。
* **更新和重启：**先停旧进程，再起新进程。同一个 Mini App 任何时候只有一个进程，不要自己再起第二个入口。

## 多语言与子进程

入口后面用什么实现都可以。需要 Python、Go 等后端时，由 Node 入口拉起它，让它监听自己的内部端口，再由入口在分配的端口上做反向代理。

> [!WARNING]
>
> 宿主只管理入口这一个 Node 进程。插件拉起的子进程要在 `dispose` 里自己关掉，宿主不保证能找到并结束它们。

## 端口鉴权

宿主在加载入口之前，给进程内的 `node:http` 服务装了鉴权守卫。只有 Mini App 页面和托管 MCP 客户端带着本次运行的凭据，其他请求在进入插件代码之前就会收到 `401`。插件不需要写鉴权逻辑，也看不到凭据。原理见[端口与凭据](/docs/miniapp/host/security#guard)。

* 分配的端口必须用 `node:http` 监听，可以是 `createServer`，也可以是基于它的框架。用 `https`、`http2`、裸 `net` 或子进程直接监听，守卫保护不到，服务永远不会就绪，启动以 `READINESS_TIMEOUT` 失败。
* `/__mavis/` 路径前缀归宿主所有，不要声明或处理这个前缀下的路由。
* 页面请求用相对路径或同源 URL。Service Worker、弹窗、外部浏览器和普通浏览器标签不带凭据，会收到 `401`。
* 守卫覆盖进程内所有 `node:http` 服务。需要接收第三方回调（比如 OAuth 跳转）时，走分配端口自己的路由，不要在进程里另开一个 HTTP 服务。
* 进程重启后凭据会更换。已经打开的标签在用户点刷新或重新打开之前会收到 `401`，这是预期行为。

## 日志

用 `context.logger` 记录诊断信息。宿主只采集有限的 stdout / stderr 样本，日志里不要放凭据和业务内容，也不要指望它完整保留。
