内容
# dudoxx-ai-terminal
**共享多终端桥接** — 一个 tmux 会话,多个可寻址终端,
由**人类**(网页 / xterm.js)和**AI 代理**(MCP)**同时**连接,
两者都看到完全相同的终端。架构一句话:一个单一的固定大小的 `tmux` 会话是共享状态;
一个 NestJS **代理**拥有规范的终端注册表,并通过 WebSocket 将输出扇出到每个终端的网页 UI;
一个 **MCP** 服务器让 Claude shell 输出到同一个 tmux 会话(它不持有 PTY);
一个 Next.js **网页**应用使用 xterm.js 渲染终端。
## 软件包
| 软件包 | 角色 | 端口 / 传输 |
|---|---|---|
| `@ddx/term-contract` (`packages/ddx-term-contract`) | 共享 zod 类型 — WS 帧,MCP 工具 I/O,终端/会话描述符 | 库(ESM+CJS+类型) |
| `ddx-term-broker` | 人类通道 + 规范状态:tmux 控制模式附件,注册表,REST CRUD,每个 terminalId 的 WS | HTTP/WS **13330** (`DDX_TERM_BROKER_PORT`) |
| `@dudoxx/ddx-term-mcp` | 代理通道:MCP stdio 服务器,薄 tmux 客户端(无 PTY) | stdio (`node dist/server.js`) |
| `ddx-term-web` | Next.js 16 xterm.js UI,每个 terminalId 一个选项卡 | HTTP **13340** (`DDX_TERM_WEB_PORT`) |
> 端口默认使用高 **133XX** 波段 — 清除 `3000`/`5173`/`8080` 以避免与您自己的运行代码库冲突。通过 env 或 `.env` 文件覆盖(参见 [配置](#配置))。
> `ddx-cli-py` / `ddx-cli-ts` 是交互式 e2e 目标夹具,**不是**工作区软件包 — 它们永远不会从此处构建或发布。
## 启动
正常路径是**代理驱动** — 您不需要手动启动开发服务器。构建一次,
注册 MCP(如下),然后调用任何 `term_*` 动词:MCP 服务器的监视器自动生成
代理(`13330`)和网页(`13340`),并附加到它们。
```sh
pnpm install
pnpm build # 构建所有软件包,包括 ddx-term-mcp/dist/server.js
# 或仅 MCP 服务器及其合约依赖:
pnpm --filter @dudoxx/ddx-term-mcp... build
```
然后在 **http://localhost:13340** 打开网页 UI,观看代理驱动的相同终端。
`~/.ddx-term/{broker,web}.lock` 处的锁定文件使堆栈成为机器范围的单例;
设置 `DDX_TERM_WEB=0` 以仅运行代理(无头)。
> **在代理/网页上进行黑客攻击?** `pnpm dev` 仍然通过 turbo(`broker 13330 + web 13340`)并发运行两者,以实现实时编辑循环 — 但对于正常的代理使用,不再需要。参见 [`SETUP.md`](./SETUP.md)。
## 注册 MCP 服务器(代理通道)
将以下内容添加到 Claude Code 项目的 `.mcp.json`(或您的 Claude Desktop 配置)中。
副本位于 [`.mcp.json.example`](./.mcp.json.example) — 用此存储库的绝对路径替换 `/abs/path`:
```json
{
"mcpServers": {
"ddx-term": {
"command": "node",
"args": ["/abs/path/dudoxx-ai-terminal/ddx-term-mcp/dist/server.js"],
"env": {
"DDX_TERM_SOCKET": "/tmp/ddx-term.sock",
"DDX_TERM_SESSION": "ddx-shared",
"DDX_TERM_DEFAULT": "t01",
"DDX_TERM_BROKER_PORT": "13330",
"DDX_TERM_WEB_PORT": "13340"
}
}
}
}
```
| 环境变量 | 默认值 | 目的 |
|---|---|---|
| `DDX_TERM_SOCKET` | `/tmp/ddx-term.sock` | tmux `-S` 套接字的共享会话 |
| `DDX_TERM_SESSION` | `ddx-shared` | 托管所有终端(窗口)的会话 |
| `DDX_TERM_DEFAULT` | `t01` | 在动词省略 `terminalId` 时使用的 terminalId |
| `DDX_TERM_BROKER_PORT` | `13330` | 代理 HTTP/WS 端口 |
| `DDX_TERM_WEB_PORT` | `13340` | 网页 UI HTTP 端口 |
| `DDX_TERM_HOST` | `127.0.0.1` | 绑定/连接主机(设计为环回) |
| `DDX_TERM_WEB` | (未设置 → 网页开启) | 设置 `0` 以仅运行代理(不生成网页) |
`env:` 块是**每客户端**的重叠层 — 它优于 `.env` 文件。完整的
表格(读取上限,允许列表,监视器写入的 URL)位于 [`DEPENDENCIES.md`](./DEPENDENCIES.md)。
### 全局安装(每个 Claude Code 会话)
软件包是 `bin` 形状的(`ddx-term-mcp` → `dist/server.js`),具有 `prepare`
脚本,在安装时构建。要为**所有**项目注册它,请通过 CLI 将其添加到您的
用户范围的 MCP 配置中(从此存储库运行,以便 `$PWD` 解析):
```sh
pnpm -F @dudoxx/ddx-term-mcp build # 生成可执行的 dist/server.js
claude mcp add ddx-term --scope user \
--env DDX_TERM_SOCKET=/tmp/ddx-term.sock \
--env DDX_TERM_SESSION=ddx-shared \
--env DDX_TERM_DEFAULT=t01 \
--env DDX_TERM_BROKER_PORT=13330 \
--env DDX_TERM_WEB_PORT=13340 \
-- node "$PWD/ddx-term-mcp/dist/server.js"
```
使用 `claude mcp list` 验证(查找 `ddx-term … ✔ Connected`)。
## 配置
每个设置都有一个内置代码默认值和**三个重叠层** — 最高优先,`override: false` 表示显式设置的变量始终优于文件:
1. **MCP 客户端 `env:` 块**(`.mcp.json` / `claude mcp add --env`)— 每个代理/每个客户端。
2. **项目本地 `.env`** 在当前工作目录中。
3. **全局 `~/.ddx-term/.env`** — 适用于机器上的每个客户端。
复制 [`.env.example`](./.env.example) 到 `.env`(项目)或 `~/.ddx-term/.env`(全局)并
编辑。MCP 服务器在解析端口之前加载两个文件(`src/load-dotenv.ts`);
代理和网页也在独立的 `pnpm dev` 路径中加载它们。示例 — 将整个
堆栈移动到不同的波段:
```sh
# ~/.ddx-term/.env
DDX_TERM_BROKER_PORT=14330
DDX_TERM_WEB_PORT=14340
```
`.env` 被 gitignore;`.env.example` 是提交的模板。
> **发布功能已连接 — `@dudoxx/ddx-term-mcp` 作为自包含的 npm
> 包发布。** 其发布包(`build:bundle`,tsup)内联 `@ddx/term-contract`
> 从源代码到单个 `dist/server.js`(`noExternal: ['@ddx/term-contract']`),
> 仅保留 `zod` + `@modelcontextprotocol/sdk` 作为外部运行时依赖项。该
> 包作为 `prepublishOnly` 连接,因此发布的 tarball 携带 **no
> `workspace:*` reference** — 合约保持 `"private": true` 且从不
> 单独发布(按设计;它存在于 changeset `ignore[]` 中)。一旦版本发布到注册表(参见下面的 **发布和版本控制**),
> `npx -y @dudoxx/ddx-term-mcp` 表单解析。在第一次注册表发布之前,
> 通过绝对路径注册,如上所述。
MCP 服务器是一个**薄 tmux 客户端 — 它从不拥有 PTY**(无 `node-pty`);
它 shell 输出到 tmux 对话,与代理和人类附加到同一个会话。
该共享状态属性是重点:代理输入的命令在网页 UI 中实时可见。
## 从真正的终端模拟器(人类通道,无浏览器)原生附加
由于桥接器是一个标准的 tmux 会话,您也可以使用本地的
tmux 控制模式客户端附加 — iTerm2 和 WezTerm 原生渲染 tmux `-CC`:
```sh
tmux -CC attach -t ddx-shared
```
这在 iTerm2 / WezTerm 中将共享终端作为原生选项卡/窗口打开,
与网页 UI 和代理一起 — 所有三个都看到相同的终端。
## 验证
```sh
pnpm typecheck # turbo 在所有软件包上运行 typecheck
pnpm lint
pnpm test # turbo 运行测试(单元测试 + e2e;e2e 在临时套接字上生成 tmux)
```
## 发布和版本控制(Changesets → npm)
只有 **`@dudoxx/ddx-term-mcp`** 发布到公共 npm;代理、网页和
合约都是 `"private": true` 并在 `.changeset/config.json` `ignore[]` 中列出。
```sh
pnpm changeset # 1. 描述更改 + 选择增量(补丁/次要/主要)
git commit && push # 2. 打开一个包含 .changeset/*.md 的 PR
```
在合并到 `main` 上,`.github/workflows/release.yml` 运行 Changesets 的操作:
1. 它打开(或更新)一个 **"版本包" PR**,该 PR 变更 `package.json`,
编写 `CHANGELOG.md`,并消耗 changeset 文件。
2. 合并 **该** PR 触发 `pnpm release` → `build:bundle`(tsup,内联合约)→ `changeset publish` → `@dudoxx` 范围内具有发布权限的 npm 自动化
令牌(`NPM_TOKEN`)和发出 provenance
(`NPM_CONFIG_PROVENANCE=true`)。在第一个版本发布后,
`npx -y @dudoxx/ddx-term-mcp` 表单解析。
> 在发布前验证 tarball:`cd ddx-term-mcp && pnpm build:bundle &&
> npm pack --dry-run` — 它必须列出 `dist/server.js`(自包含,无
> `workspace:*`)、`package.json`、`README.md`、`INSTALLATION.md`、`LICENSE` 和
> 两个 `assets/*.png`。
有关完整的发布演练、增量类型指导和 CI 详细信息,请参阅
[`CLAUDE_PUBLISHING.md`](./CLAUDE_PUBLISHING.md) 和
[`ddx-documentation/05-publishing/release-flow.md`](./ddx-documentation/05-publishing/release-flow.md)。
---
Dudoxx UG / Acceleate Consulting - Walid Boudabbous <walid@acceleate.com>