🖥️ Agent Interactive Terminal
有状态、跑在真实 PTY 上的终端会话,让智能体和人在同一块屏幕前协作。
🌐 简体中文 · English
状态:M1 的主体、M2 的会话层(控制租约、密码挂起、密钥注入)、M3 的 shell 保真和会话胶囊都已经落地,右侧边栏的 xterm 终端也已经接上同一条 attach 流, 五十个测试在 Linux 的 Node 20 和 Node 22 上通过。仓库同时以 DSH bundle 的形式 发布,并进入了插件市场索引。还没做的条目在 ROADMAP.md。
✨ 为什么需要它
今天智能体能够调用的“终端”,本质上都是 exec:启动子进程、等它退出、收集
stdout 和 stderr。这个模型是无状态的、一次性的、基于管道的。它没有 TTY,所以
程序不会进入交互模式,shell 的启动配置不会按人的预期加载,长任务看不到过程,
人和智能体也没法在同一个终端里协作,只能靠复制粘贴。
常见的绕法都卡在同一个地方。往 stdin 里灌 yes 不等于回答提示;轮询管道等密码
提示,会把密码泄漏进对话记录;等输出安静区分不了“跑完了”和“跑得慢”;而无论
怎么解析 ANSI 字节,智能体都拿不到“此刻屏幕上是什么”。
这个项目把终端会话变成一份有身份、有屏幕、有相位、有控制租约的资源,让智能体 读取离散的屏幕快照,而不是订阅一条字节流。
🧩 作为 DSH 插件
这个仓库同时是一个 DeepSeek Harness 的 bundle。启用之后,它向当前 profile 里的
每个智能体注册八个 terminal_* 工具,并且在 Plugins 页给出一张配置卡:默认 shell
可以选跟随 $SHELL 的自动模式、zsh 或者 bash,同时打开的会话数默认是四。
在 Plugins 的 Discover 标签页里添加这个索引:
https://raw.githubusercontent.com/AyanamiiiRei/agent-interactive-terminal/main/plugin-market.json
然后搜索 agent-interactive-terminal 安装。也可以跳过市场,在 Add plugin 里直接
填 Git 规格:
git+https://github.com/AyanamiiiRei/agent-interactive-terminal.git#e0345b2fea5be460e9d41a0fc9995800e22442ea
安装时会提示批准 node-pty 的构建脚本,同意之后才会编译出原生模块。插件市场
本身是 DSH 的实验性 bundle,第一次启用需要重启 DSH;终端插件是普通的 bundle
行,装好之后由 loader 加载。配置卡里改的 shell 只影响之后新开的会话,已经
打开的会话保持原来的 shell。从旧版本升级到 0.3.0 之后要重启一次 DSH:attach 的
WebSocket 路由在 Host 启动时注册,浏览器半边的 chunk 也在启动时进入模块表。
🖥️ 共享终端面板
启用之后,对话页会话标题栏会出现一个终端图标。点它,右侧边栏会打开一页“交互式
终端”:xterm.js 直接接管键盘,没有额外的输入框,也没有“发送”按钮。这一页里面自带
一条标签条,Host 上每一条会话对应一个编号,从 1 开始依次增加。agent 自己开的终端会
出现在同一条标签条里,不需要再等它额外打开别的页面;你点“+”新建的会话也会立刻被
agent 的 terminal_list 看到。关掉某个编号会结束那条会话,agent 那边同样会看到它
消失;关掉整个页面只是收起视图,会话继续留在 Host 上,重新打开还能接上。
键盘归谁不需要按钮来决定。鼠标点进终端只移动焦点,不会抢走 agent 的控制权;你
真的敲下一个键,租约才转给你,状态变成绿色的“你正在输入”。你停手几秒(默认四秒)
之后,租约自动回到 agent,状态变回灰蓝色的“可操作”;它真正写入命令时是金色的
“正在操作”,标题栏会扫过一道光;它调用 terminal_snapshot 或 terminal_wait
查看时是青色的“正在查看”,一两秒后自动消退。顶栏下面那条细线会在你打字时收短,
让你看见还剩多久。密码提示会让状态变红并显示“需要你输入密码”,你输入的内容不会
进入模型上下文。你敲的每条命令,
agent 下一轮用 terminal_snapshot 看到的都是同一块屏幕;agent 写进去的东西,你也
会立刻在面板里看到。浏览器断开时租约也会自动交还,不会把 agent 锁在门外。
右上角还有两个权限按钮,它们决定 agent 能对这个终端做什么。锁形按钮把会话切到
只读,agent 还能看,但写入会得到 READ_ONLY,状态显示“Agent 只读”;插头按钮把
会话断开,agent 既看不到也写不了,状态显示“Agent 已断开”,边框变红,不再额外显示
提示条,再点插头就能恢复。这两档都由服务端执行,不是前端把按键丢掉,所以 agent 的
工具调用同样会被拦住;断开连接会立刻中止它正在进行中的等待,terminal_list 里只剩
下一条没有名字和内容的脱敏记录。你本人永远可以输入,因为终端是你的。
键盘、复制粘贴和尺寸变化都走 WebSocket attach 流。选中文本即复制,Ctrl+Shift+V
或右键粘贴走括号粘贴,窗口变化时自动 fit 并向 PTY 发 resize。快捷键沿用 Ptyxis
的习惯:Ctrl+Shift+T 新建,Ctrl+Shift+W 关闭当前编号,Ctrl+Shift+C/V 复制粘贴,
Ctrl+=、Ctrl+-、Ctrl+0 缩放字号。终端获得焦点时会拦下这些组合键并调用
preventDefault,尽量不让 Ctrl+- 同时把整个 DSH 页面缩小;如果浏览器仍然优先处理
自己的缩放快捷键,那就是浏览器把组合键截走了,终端这边无法再拦。
终端默认跟随 DSH 的浅色/深色主题,DSH 切换主题时终端跟着换,不需要手动再选一次; 设置里也可以固定成某一套调色板。面板右上角的齿轮打开设置,分成“主题与字体” “光标与行为”“快捷键”三组:跟随 DSH 加上七套固定主题(深色、浅色、Solarized 两种、 Dracula、One Dark、GNOME、Tango,都带完整的 16 色 ANSI 调色板)、字体与字号、行高、字重、连字、光标形状与闪烁、回滚行数、响铃、 选中即复制、右键粘贴,以及可以逐项改键的快捷键表;交互一组里还能改停止输入后 自动交还的秒数。点击组合键重新绑定,按 Esc 取消录制,Esc 也能直接关掉设置。改动 立即生效并保存在 localStorage,底部有“恢复默认”。设置面板是嵌在终端里的浮层, 不会弹模态框挡住内容。终端连上之前会显示“正在连接”和加载指示,复制成功时底部 短暂提示“已复制”。
命令拦截的设计写在 docs/command-interception.zh.md:三档模式(危险命令审批、
不拦截、全部确认),复用 DSH 已有的审批服务。本轮只写方案,没有实现。
配置卡里新增了“默认执行方式”。“优先交互式终端”会给 agent 一段系统提示,让它 优先把 REPL、包管理器、数据库客户端这类工作放进共享终端;“优先一次性命令”则 反过来。自动模式不额外加提示,由模型按工具描述自己选。
🚀 快速开始
需要 Node.js 20 或更新版本,以及用于编译 node-pty 的 C++ 工具链。开发和 CI
目前只针对 Linux:前台进程观测依赖 /proc,macOS 和 Windows 尚未验证。
# 克隆并安装
git clone https://github.com/AyanamiiiRei/agent-interactive-terminal.git
cd agent-interactive-terminal
npm install
npm test
# 跑一条命令,看智能体视角的屏幕和状态行
npm run demo -- "git status"
# 交互循环:直接输入命令,另有 /snap、/wait、/key、/resize、/quit
npm run demo
# 浏览器接入:打开终端里打印的地址
npm run attach
演示循环长这样:
› ls
› /snap
› /wait 5000
› /key ctrl-c
› /resize 120 40
› /quit
🧰 在代码里使用
这个库不依赖任何智能体框架。如果你清楚要等什么,可以直接用会话对象:
import { TerminalRegistry } from 'agent-interactive-terminal'
const sessions = new TerminalRegistry({ maxSessions: 2 })
const session = await sessions.open({ name: 'build', cols: 100, rows: 30 })
await session.write('npm run dev')
const result = await session.wait({ match: ['ready in'], timeoutMs: 60_000 })
console.log(result.outcome, result.phase)
console.log(result.view.text)
await session.close()
也可以把这些工具定义注册到你自己的模型运行时里:
import { createTerminalTools, toolIndex, TerminalRegistry } from 'agent-interactive-terminal'
const registry = new TerminalRegistry()
const tools = toolIndex(createTerminalTools(registry))
// 把每个工具的名称、描述和参数 schema 交给模型,
// 再把调用分发到 tools.get(name).execute(args)。
🤝 人机协作
租约决定键盘归谁。人可以随时接管;接管之后智能体的写入会得到 LEASE_DENIED,
等待会得到 human_took_over,而且它不能自己抢回来:
session.takeOver() // 现在由人操作
await session.write('ls', { origin: 'human' })
session.handBack() // 人把控制权交回
出现凭据提示时,会话挂起智能体,把等待结算成 human_required,并一直持有租约,
直到有人回答:
const asked = await session.wait({ timeoutMs: 30_000 })
if (asked.outcome === 'human_required') {
// 把提示展示给人。永远不要让模型去要这个密码。
await session.write(secretFromHuman, { origin: 'human' })
}
密钥可以在启动时注入,值不会经过终端:
const session = await sessions.open({
secrets: { GITHUB_TOKEN: () => readCredential('github') },
})
🔐 访问级别
人拥有终端,智能体只是访客。“看得见”和“动得了”是两项独立的授权,由人在面板上 随时切换:
| 级别 | 智能体能读 | 智能体能写 | 适用场景 |
|---|---|---|---|
collaborate |
能 | 能,受租约仲裁 | 默认,一起干活 |
observe |
能 | 不能 | 人刚接手,不希望智能体插话 |
sealed |
不能 | 不能 | 完全断开智能体,连内容都不让它看 |
session.setAgentAccess('observe') // 只读:读取照常,写入得到 READ_ONLY
session.setAgentAccess('sealed') // 隔离:读写都得到 SEALED,列表里只剩一条脱敏记录
隔离会立刻中止正在进行的 wait,之后的输出不会再交给智能体。已经进入它上下文
的内容无法收回,这是这个模型必须诚实对待的边界。
🌐 浏览器接入
npm run attach 会启动一个本地演示:用 xterm.js 和 WebSocket 把同一条会话渲染
到浏览器里。页面默认只读,按下 “Take over” 之后键盘输入才会以
session.write(..., { origin: 'human' }) 送进终端;点 “Hand back” 把控制权交回
智能体。演示服务只绑定 127.0.0.1,xterm.js 从 node_modules 提供,也没有任何
鉴权,所以请把它当成示例而不是部署方案。
📦 现在已经能用的部分
- 🖥️ 每个会话一条真正的 PTY,从
$SHELL(或指定路径)启动,带TERM=xterm-256color、工作目录和额外的环境变量。 - 🧠 headless xterm 屏幕模型,支持备用屏幕、滚动回溯、软换行合并、光标与标题, 以及去掉 shell 画在光标之后、用暗色格子渲染的补全建议。
- 🎯 同一份屏幕的两种投影:给模型的文本屏幕,和给浏览器 attach 流的序列化 ANSI。
- 🚦 相位
starting、running、waiting_input、idle、exited。bash 和 zsh 都在加载完用户自己的启动配置之后,通过包装的 rcfile 装提示符标记;fish 和其他 shell 退回前台进程组加短暂静默窗口的判定。 - ⏳ 等待引擎支持字面量命中、静默、相位稳定、超时和退出,按固定优先级判定; 另外支持 Ctrl-C 式中断和括号粘贴。
- ⌨️ 具名按键覆盖中断、EOF、挂起、方向键、翻页和常用 Ctrl 组合,另外支持调整 尺寸和关闭时清理整棵进程树。
- 🔒 单写者控制租约,支持人工接管与交回;密码提示检测会挂起智能体,并把等待
结算成
human_required;密钥在启动时注入。 - 🧾 每次写入和控制权变更都会记入审计,凭据提示期间的人工输入会打码。
- 🧩
registry.capsule()提供每轮注入用的会话胶囊:每个会话一行,外加一个只在 摘要变化时才改变的版本号。 - 🌐 一个浏览器接入演示,用 xterm.js 共享同一条活动会话,人工接管之前保持只读。
- 🖥️ 右侧边栏的真终端:xterm.js 加 WebSocket attach,有主题、字体、光标、回滚、 响铃、复制粘贴和可改键的快捷键表,租约横幅区分“Agent 正在操作”和“你已接管”。
- 🧪 五十个测试,覆盖屏幕模型、会话生命周期、shell 集成、租约、密钥、审计、 胶囊、工具层和 attach 流。
🧩 八个工具
| 工具 | 作用 |
|---|---|
terminal_open |
打开会话,返回 id、状态行和初始屏幕 |
terminal_send |
写入文本(默认回车提交),并等待结算 |
terminal_wait |
只等待,不写入 |
terminal_snapshot |
读取当前屏幕或输出尾部,可按版本号要增量 |
terminal_key |
按下具名按键,例如 ctrl-c、enter、page-down |
terminal_resize |
调整终端尺寸 |
terminal_close |
关闭会话并清理整棵进程树 |
terminal_list |
列出打开中的会话、相位、尺寸和目录 |
🏗️ 设计分层
flowchart LR
agent["🤖 智能体"] --> tools["八个工具"]
human["🧑 人"] --> browser["浏览器 attach"]
tools --> session["会话:相位 / 等待 / 租约 / 审计"]
browser --> session
session --> pty["PTY 与 shell"]
session --> screen["屏幕模型"]
screen --> tools
screen --> browser
整个设计分五层:
- PTY 原语:启动、写入、调整尺寸、发信号、观测前台进程组、清理整棵进程树。
- 会话核心:每个会话一份屏幕模型、活动观测、等待器、控制租约、打码与审计。
- 会话服务:身份、发布、授权、清理,以及状态变更事件。
- 工具面:一组面向模型的工具,负责写入、等待、读屏、按键、调整尺寸和关闭。
- 传输与界面:浏览器终端通过 attach 流接入,和智能体共享同一条会话记录。
三层之间有三条不变量。屏幕状态只有一份,两种投影:给智能体的是文本,给浏览器 的是序列化后的 ANSI。写入是动作,等待是查询,状态是事实,三者分开,长任务才能 只看不写。授权在创建会话时一次给定,而不是每次按键重新审批。
完整设计,包括工具 schema、数据结构、租约协议和已知的尖角,在 docs/design.zh.md。
🗂️ 仓库结构
src/screen/model.ts headless xterm 屏幕模型与两种投影
src/pty/pty.ts node-pty 包装
src/pty/proc.ts Linux 前台进程组与 stdin 等待观测
src/session/session.ts TerminalSession:写入、等待、按键、快照、租约、审计
src/session/registry.ts 会话 id、并发上限、查找、清理与会话胶囊
src/session/shell.ts $SHELL 解析、bash 与 zsh 的启动文件包装
src/tools/index.ts 八个面向模型的工具定义
src/attach.ts 浏览器与 Host 之间的双工 attach 流
src/bridge.ts 仅回环的 HTTP 桥,面板用来开会话、读屏幕
src/cli/demo.ts 走工具面的交互演示
client/index.js bundle 的浏览器半边:配置卡、单页终端和标题栏按钮
client/terminal.src.js xterm 终端与设置面板,构建成 client.terminal.js
test/ 屏幕、会话、shell、租约、胶囊与工具测试
docs/design.zh.md 完整设计文档
docs/command-interception.zh.md 命令拦截方案(只写方案,未实现)
ROADMAP.md 里程碑、范围与明确的非目标
🗺️ 状态与路线图
M1 做单智能体的可读屏幕和判定准确的等待。M2 加共享会话、控制租约和密码交接。 M3 打磨 shell 保真与多会话上下文。ROADMAP.md 列出了每一项和每一个 明确的非目标。
尚未实现的部分:审计之外的打码区间、空闲会话回收、按模式唤醒空闲智能体的事件 通道、fish 集成,以及 Windows 和 macOS 的对齐。
📚 相关工作
设计是对着 DeepSeek Harness 写的,建立在它已有的原语上:基于 node-pty 的终端 启动器、按所有者隔离的会话注册表、带提示符标记的 shell 后端、headless xterm 屏幕、双工 attach 流,以及能唤醒空闲智能体的后台任务通道。方案是把这些拼成 同一个会话模型,而不是推倒重来。
值得一读的前作包括 tmux 与 screen 的会话复用、expect 的脚本化交互,以及 libvterm、xterm.js、pyte 在终端仿真上的工作。
🛠️ 参与
欢迎提 issue 和设计意见,尤其是做过智能体工具或终端复用器的人。想实现某个 里程碑的话,先开 issue 对一下范围,避免和路线图脱节。
📄 许可证
MIT,见 LICENSE。
如果这个项目帮你省了点事,点个 ⭐ 就是最好的支持。
No comments yet. Be the first to write one.