> ## 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)。宿主负责拉起进程、分配端口、鉴权和回收。

入口、`context`、Connector 与工具的完整契约见 [Node API 参考](https://agent.minimaxi.com/docs/miniapp/reference/node-api.md)，会话方法见 [Agent API 参考](https://agent.minimaxi.com/docs/miniapp/reference/agent-api.md)。

## 入口契约

* 入口是一个导出 `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` | 运行生命周期信号，在停止或启动失败时触发；需显式传入请求选项，或注册后台工作的清理逻辑。它不会自动取消请求或关闭插件资源 |
| `logger` | 结构化日志，传稳定的事件码和不含敏感信息的字段 |
| `hostConnector?` | 在 `hostConnectorAccess.providers` 中声明至少一个 provider 后存在，见[接入数据](https://agent.minimaxi.com/docs/miniapp/authors/data.md#host-connector) |
| `agent?` | `agentAccess` 声明 `session.run` 或 `session.read` 后存在，见 [Agent API](https://agent.minimaxi.com/docs/miniapp/authors/agent.md#agent-api) |
| `tool?` | 声明 `toolAccess: ["call"]` 后存在，用于调用宿主已挂载的工具，见[统一工具入口](https://agent.minimaxi.com/docs/miniapp/authors/data.md#tools) |
| `capabilities` | `has(name)` 同步检查宿主是否实现了某项接口，见[版本与能力探测](https://agent.minimaxi.com/docs/miniapp/reference/manifest.md#compat) |

`hostConnector`、`agent` 和 `tool` 是否注入由 manifest 决定。用户在权限面板关闭权限后，对象仍在，但对应调用会被拒绝；不要只靠对象存在来判断是否获准使用。`capabilities.has(name)` 只说明宿主实现了接口，也不代表当前插件获得授权。兼容旧版时使用可选链，例如 `context.capabilities?.has('tool')`。

## 最小入口

```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 与会话](https://agent.minimaxi.com/docs/miniapp/sessions.md)。
* **容量：**同时最多运行 3 个不同的 Mini App 服务。第 4 个会返回 `BUSY`；更新已在运行的 Mini App 不额外占名额。
* **停止**：`stop` 只停服务，插件和同包能力都保留。关闭运行中 Mini App 的标签并确认后，会停止它的共享服务，并关闭所有会话里这个插件的页面。
* **更新和重启：**先停旧进程，再起新进程。同一个 Mini App 任何时候都只有一个进程，不要自行启动第二个入口。

市场更新仍提供兼容当前宿主的 Mini App 定义、且需要新进程时，宿主先保留运行中的版本，直到你从工具栏或插件详情确认更新，或服务停止后采用新版定义。如果新版删除了定义，或宿主因最低版本要求而屏蔽该定义，旧服务会被退役。详见[市场更新](https://agent.minimaxi.com/docs/miniapp/host/lifecycle.md#marketplace-updates)。

## 多语言与子进程

Node 入口后的服务可自行选择实现方式。需要 Python、Go 等后端时，由 Node 入口拉起它，让它监听自己的内部端口，再由入口在分配的端口上做反向代理。

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

## 端口鉴权

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

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

## 日志

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

当前日志桥会截断消息，并仅传递 `fields` 的键名，不保留字段值；方法与限制见 [logger 参考](https://agent.minimaxi.com/docs/miniapp/reference/node-api.md#logger)。
