@mortiseai/stem
v0.0.21
Published
MortiseAI Stem CLI, built on MSC Engine
Maintainers
Readme
@mortiseai/stem
Stem — MortiseAI 构建的 Self-Evolving AI Agent,基于 MSCE(MSC Engine)的自我进化智能体
核心三种业务模式(详见「业务模式」):
- Agent 模式 — 面向通用智能体形态,默认模式,当前唯一开放。启动:
stem/stem --agent;核心能力:业务图谱记忆 - Coding 模式 — 面向编码工作流的形态(暂未开放)。启动:
stem --coding;核心能力:业务图谱记忆,基于 MortiseAI Spec Code Engine 编码 - FDE 模式 — 面向企业交付场景的形态(暂未开放)。启动:
stem --fde;核心能力:业务图谱记忆,私有图谱记忆,基于 MortiseAI Spec Code Engine 编码
快速开始
前置依赖
- Node.js >= 22(源码开发/构建另需 Bun,见
MSTEM.md)
npm 快速安装和启动(发布后)
npm install -g @mortiseai/stem
stemmacOS / Linux / Windows 均可,仅需 Node >= 22,无需 bun。
退出
在 Welcome 屏按 q · Esc · 或 Ctrl+C。
启动方式与命令行参数
启动范式(npm 全局安装)
npm install -g @mortiseai/stem
stem # 默认:agent 模式 · llms.json default 模型 · 系统语言
stem --lang en # 英文界面
stem --model BaiLian:glm-5.2 # 会话默认模型(platform:model 完整引用)
stem --model glm-5.2 # 裸模型 id(目录唯一命中,否则落默认平台)
stem --lang ja --model BaiLian:glm-5.2 # 组合使用
stem --resume <sessionId> # 恢复历史会话(续写同一份会话日志)
echo "一句话问答" | stem --bare # SIMPLE 模式:stdin → 单次回答(non-TTY)开发启动(仓库源码 bun run dev,含 --tenant/--user 的 npm/bun flag 透传规则)与构建产物启动见 MSTEM.md。
tenant / user 隔离启动
stem # platform 作用域:落盘在 .mstem/platform 与 mstem-storage/platform
stem --user <用户ID> # platform + 用户隔离:用户空间 mstem-storage/platform/<用户ID>/
stem --tenant <租户代码> # 租户模式:全部落盘状态按 tenant/<租户代码> 隔离
stem --tenant <租户代码> --user <用户ID> # 租户内再按用户隔离:用户空间 mstem-storage/tenant/<租户代码>/<用户ID>/四个作用域(platform / platform+user / tenant / tenant+user)两两绝对隔离;租户码/用户码只认启动 flag,不认 env。细则见下文「多租户隔离 · --tenant」与「用户隔离 · --user」两节。
空间初始化
一条命令升级全局 CLI 并补齐 .mstem / mstem-storage 目录骨架(platform 空间 + 已存在或 -t 指定的 tenant 空间;幂等,绝不覆盖已有文件;租户配置不播种,各租户的 llms.json / settings*.json / mcp.json 需手工放入 .mstem/tenant/<code>/,见「多租户隔离」节):
./update_pro.sh [目标项目目录] [-u <name>]... [-t <code>]... [--version <ver>] [--skip-upgrade|--skip-space]运行形态总览
| 形态 | 启动方式 | 说明 |
|---|---|---|
| TUI 交互(默认) | stem | Welcome + Repl 全功能终端界面 |
| SIMPLE | stem --bare | TTY 下裸 prompt 简化 REPL;non-TTY + stdin pipe 走单次问答链路 |
| 无头(SDK 桥) | stem --sdk-url ws://… | 不渲染 TUI,WebSocket NDJSON 全双工驱动,见下节 |
| WS telemetry tap | stem --telemetry-url ws://… | TUI 正常交互,遥测帧单向推给外部 WS 观察端,见下节 |
| MCP stdio server | ./dist/mcp.mjs | 把 stem 工具集暴露为 MCP server(stdio JSON-RPC),供其他 LLM 客户端接入 |
| 后台 daemon | ./dist/daemon.mjs [run\|status] | 无 UI 常驻容器;status 输出一行 JSON 状态;SIGTERM/SIGINT 优雅退出 |
MCP / daemon 是独立构建产物,与 TUI 共用同一 MSC 容器逻辑(构建方式见 MSTEM.md「构建与发布」节)。
无头模式(headless SDK 桥)· --sdk-url
被 sidecar server(FastAPI / desktop server 等)spawn,协议为 WebSocket 上的 NDJSON(每帧单行 JSON),与 Claude Code SDK stream-json 消息同构;内部与 TUI 走完全相同的 MSC agent loop:
stem --sdk-url ws://127.0.0.1:8123/ws/<sid> # headless 全双工
stem --sdk-url ws://127.0.0.1:8123/ws/<sid> --sdk-telemetry # 追加 telemetry 观测帧(默认关)- 下行帧:
system(init:model / tools / slash_commands / workflows / permissionMode)、stream_event(Anthropic streaming 同构)、result、control_request(权限卡)、command_result/workflow_result/telemetry_event - 上行帧:
user(消息,入队串行)、control_request(interrupt / set_permission_mode)、control_response(权限裁决)、command(slash 命令)、workflow(直接触发 workflow,不经 LLM) - 会话生命周期 = 连接生命周期:ws 断开 → 容器销毁 +
exit 0 --sdk-telemetry(或 envSTEM_TELEMETRY=1)开启八源观测帧(tool/hook/mcp/subagent/workflow/skill/command/plugin)
人工驱动与协议细节:bun scripts/ws-manual-server.ts --spawn(交互式驱动器)· 协议文档 docs/sdk-telemetry-protocol.md · 全量测试手册 stem_cli_test/ws/Stem-WS.md。
WebSocket telemetry tap · --telemetry-url
与无头模式不同,tap 是 TUI 共存的单向观测通道:TUI 正常渲染交互,telemetry 帧单向推给外部 WS 观察端;忽略一切下行帧,断开只记日志、不影响 TUI、不重连。env STEM_TELEMETRY_WS 兜底。
stem --telemetry-url ws://127.0.0.1:8123/ws/manual语言 · --lang
--lang <zh|en|ja|ko>(或 --lang=zh)。缺省时按优先级回退:
--langflag- env
STEM_DEF_LANGUAGE(shell export 或.mstem/settings.local.json的env段,见「运行时开关」) - POSIX env
LC_ALL/LC_MESSAGES/LANG(前缀匹配) - 默认
zh
模型 · --model
--model <ref>(或 --model=<ref>)设置会话级默认模型,不落盘:
ref 形式:
platform:model(如BaiLian:glm-5.2,平台名做存在性校验,打错立即报错退出并列出已知平台)或裸模型 id(目录唯一命中,否则落默认平台,不做白名单)平台与模型目录来自
.mstem/llms.json(见下节)解析优先级(
llms.json的default是唯一的持久来源,settings 不参与模型选择):/fast开启 > 会话级覆盖(--model启动参数 / 会话内/model,同一个槽)>llms.json的default(工程层.mstem/<scope>/> 用户层~/.mstem[/tenant/<code>]/)> envSTEM_DEF_LLM_MODEL> 首平台首模型/model <ref>是会话级的:与--model写同一个槽,不落盘,退出即失效,后写胜出/model default清掉该槽(连--model启动参数一起撤销),回落llms.json的default要改持久默认模型,就改
llms.json的default—— 没有别的持久化入口/model current可查看当前模型与来源
模型目录 · .mstem/llms.json
多平台 LLM 目录,搜索顺序:$STEM_LLMS_PATH → <cwd>/.mstem/llms.json → ~/.mstem/llms.json。
protocol:anthropic(/v1/messages)或openai(/chat/completions)apiKeyEnv:密钥所在 env 变量名,缺省STEM_LLM_<平台大写>_API_KEY;也可写apiKey字段(值为全大写下划线视为 env 变量名,否则视为密钥字面量)authHeader:缺省 anthropic →x-api-key、openai →bearercontext:支持'200k'/'1M'/ 数字;pricing单位 USD/1M tokens(供/cost分账)vision:该模型是否支持图片理解(布尔);未声明视为能力未知default:全局默认模型;roles.fast:/fast快速模式用的模型
per-model 深度思考开关(thinking)—— 控制这个模型默认思不思考:
| 字段 | 必填 | 说明 |
|---|---|---|
| thinking | 否 | 布尔。true = 默认开启深度思考,false = 默认关闭。不写 = 一个字段都不发,由网关自己的默认决定 |
| thinking_flag | 否 | 开关字段名,缺省 enable_thinking。仅 protocol: "openai" 有效(见下)。写坏了整条开关不发 |
| thinking_budget | 否 | 思考预算 tokens,仅在 thinking: true 时同发;不写则不发 |
两条协议发的形状不同(2026-08-08 实测,别改回统一形状):
| 协议 | 关 | 开 | |---|---|---| |
openai|enable_thinking: false|enable_thinking: true+thinking_budget| |anthropic|thinking: {"type":"disabled"}|thinking: {"type":"enabled","budget_tokens":N}|anthropic 侧必须用原生对象:百炼
/apps/anthropic对顶层enable_thinking: false静默忽略(照样回 thinking 块),换成{"type":"disabled"}才真关掉(同一问题 output_tokens 121 → 4);minimax/anthropic同样认这个形状。budget_tokens受网关硬校验max_tokens > budget_tokens(不满足直接 400),本端会自动压到max_tokens - 1以内。
thinking: false也会发关闭指令 —— 这一档的主要用途就是关掉 qwen3 / glm 这类默认开思考的模型,不发字段就关不掉。开关按当前模型的目录条目取,子 agent 带自己的 model 时各取各的。例外:
api.anthropic.com(及*.anthropic.com)不发。官方端点上budget_tokens必填且有下限、还与temperature/max_tokens互相牵制,这些约束当前配置表达不了;官方端点的推理深度由/effort的 system prompt 注入承担。与内置搜索同样是静默生效的能力:网关不认时不报错也不降级,配完请实测一次(响应里有没有
thinking块 /reasoning_content/usage.completion_tokens_details.reasoning_tokens)。写错的值一律静默忽略,/doctor会列出具体问题。
per-model 联网搜索(web_search 工具)—— 各家模型的搜索形态不同,故按模型配置。不写 search = 该模型没有搜索能力,工具压根不上送给模型(保持"出网搜索由用户显式选择"的默认):
| 字段 | 适用档 | 必填 | 说明 |
|---|---|---|---|
| search | — | — | model / mcp / api |
| search_flag | model | 否 | 请求体上那路布尔开关的字段名,缺省 enable_search(百炼 / DashScope 口径);写 false 显式关掉这一路(只留工具形状) |
| search_model_url | model | 否 | 写了就走独立请求档:搜索单独打这个地址(OpenAI Chat Completions 形状),不再挂在主对话那次请求上。收 baseUrl 或完整 /chat/completions 两种写法 |
| search_model_key / search_model | model | 否 | 独立请求档的密钥与模型;key 省略回落平台 apiKey,model 省略 = 该条目自己的模型 id |
| search_model_options | model | 否 | 独立请求档的 search_options 子配置(enable_source / forced_search / search_strategy …),外加 enable_thinking(见下) |
| search_mcp | mcp | 是 | "<server>" 或 "<server>/<tool>";server 名取自 mcp.json。只给 server 时自动挑搜索工具,有歧义会报错并列出候选,请按提示补 /<tool> |
| search_mcp_key | mcp | 否 | 该 MCP server 的鉴权密钥。与平台 apiKey 可以不是同一把 —— 托管搜索服务常另发一把(百炼:对话走 Token Plan 的 sk-sp-,联网搜索认另一把)。这里只是声明,真正送进请求头/环境变量要在 mcp.json 里用 $<平台>$<模型>$search_mcp_key 引用,见下方「MCP 配置」节。值为全大写下划线时视为 env 变量名 |
| search_api_url | api | 是 | 三方搜索 API 端点。按 host 自动识别 api.tavily.com / google.serper.dev / api.search.brave.com;其它 host 走通用约定形状:POST {query, max_results} → {results:[{title,url,snippet}]} |
| search_api_key | api | 否 | 可不设(自建 / 免 key 端点不会带 Authorization)。上面三家已知 vendor 没有免 key 模式,缺 key 会直接报错。值为全大写下划线时视为 env 变量名——llms.json 随仓库提交,密钥请优先用 env 变量名或写在 ~/.mstem/llms.json |
search: "model"= 这个模型自己会搜,把它打开。各家开法不同,所以这一档两路都发,网关各取所需、忽略不认识的那一路:
- 以
web_search_20250305工具形状上送(Anthropic 直连及兼容该 type 的网关)- 请求体加
enable_search: true(百炼 qwen 口径;字段名用search_flag改,search_flag: false关掉这一路)唯一例外:
api.anthropic.com(及*.anthropic.com)不加第 2 路 —— 官方端点对未知顶层字段直接 400。搜与不搜由模型自己按问题判断,结果内联在回复里,客户端不派发web_search。两路都不被网关认时表现为静默无搜索——不报错、也不降级,配完请实测一次。搜索请求的深度思考 = 独立一档,由
search_model_options.enable_thinking定,与模型条目上那个管主对话的thinking互不相干 —— 独立请求档打的是search_model_url,可能是另一家网关、另一个模型,没理由跟着对话走。不写 = 不发这个字段,跟搜索端点自己的默认。注意配置位置与线上位置不一致:配置写在
search_model_options里(它属于搜索端点这一摊),本端在拼请求体时会把它提到顶层 —— 网关不读search_options子对象里的开关。自己手写 curl 时别搞错层级。建议配
false:这一轮的任务只是把网关注入的「参考资料」整理成一段带来源的摘要,推理链对结果没有贡献,只是让用户干等。2026-08-08 同一条搜索实测 —— 开:84.9s / completion 4040 tokens(其中 reasoning 3853);关:3.9s / 178 tokens,正文长度与注入的prompt_tokens都没变。排查「搜索怎么这么慢」先看日志modelSearch:done那行的thinking字段。反过来,写进内联档的
search_options.enable_thinking是无效的:那个对象进的是主对话那次请求,那里的思考由模型条目上的thinking管。/doctor会点名这种层级错。mcp 档的密钥不写进
mcp.json—— 用$<平台>:apiKey(平台那把)或$<平台>$<模型>$search_mcp_key(这个 server 专用的那把)引用回llms.json,见下方「MCP 配置」节。配置写错(如
search拼错、mcp档漏了search_mcp)一律静默降级为"该模型无搜索能力",/doctor会列出具体问题。免 key 的 DuckDuckGo / Brave SERP 抓取已于 2026-08-08 移除,
STEM_WEBSEARCH_ENABLED/STEM_WEBSEARCH_PROVIDER两个 env 及同名 settings 一并删除。此前开启过STEM_WEBSEARCH_ENABLED=1的用户升级后会失去搜索能力,需改配search。
完整示例:
{
"platforms": {
"anthropic": {
"protocol": "anthropic",
"baseUrl": "https://api.anthropic.com",
"apiKeyEnv": "STEM_LLM_ANTHROPIC_API_KEY",
"models": {
"claude-sonnet-4-6": { "context": "200k", "search": "model" },
"claude-haiku-4-5": { "context": "200k" }
}
},
"bailian": {
"protocol": "anthropic",
"baseUrl": "https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic",
"apiKeyEnv": "STEM_LLM_BAILIAN_API_KEY",
"authHeader": "bearer",
"models": {
"qwen3.8-max-preview": {
"context": "1M",
"vision": true,
"pricing": { "input": 1.3, "output": 7.8, "cacheRead": 0.26, "cacheWrite": 0 },
"search": "model"
},
"glm-5.2": {
"context": "1M",
"search": "api",
"search_api_url": "https://api.tavily.com/search",
"search_api_key": "STEM_TAVILY_API_KEY"
}
}
},
"minimax": {
"protocol": "anthropic",
"baseUrl": "https://api.minimax.io/anthropic",
"apiKeyEnv": "STEM_LLM_MINIMAX_API_KEY",
"authHeader": "bearer",
"models": {
"MiniMax-M2.7": { "context": "200k", "search": "mcp", "search_mcp": "brave/brave_web_search" },
"MiniMax-M2.7-highspeed": { "context": "200k" }
}
},
"deepseek": {
"protocol": "openai",
"baseUrl": "https://api.deepseek.com",
"apiKeyEnv": "STEM_LLM_DEEPSEEK_API_KEY",
"models": {
"deepseek-chat": { "context": "128k" }
}
}
},
"default": "bailian:qwen3.8-max-preview",
"roles": {
"fast": "minimax:MiniMax-M2.7-highspeed"
}
}MCP 配置 · .mstem/mcp.json
MCP server 清单,搜索顺序:$STEM_MCP_CONFIG_PATH → ~/.mstem/mcp.json(用户层)→ <cwd>/.mstem/<scope>/mcp.json(工程层,高优先)。顶层键 servers(也兼容 CC 布局的 mcpServers)。
每项是本地 stdio 或远程 HTTP 二选一:
| 形态 | 写法 | 说明 |
|---|---|---|
| stdio | { command, args?, env? } | spawn 子进程 |
| 远程 | { type: "http", url, headers? } | Streamable HTTP(现行 spec);"streamable-http" 同义 |
| 远程 | { type: "sse", url, headers? } | 旧的 HTTP+SSE 传输。服务方尚未全部迁完,百炼联网搜索目前只能走这一档 |
不写
type但有url时按路径推断:/sse结尾当 SSE,其余当 Streamable HTTP。认不出来的条目(既无command又无url、url不是 http(s)、type: "ws")会被跳过并在日志里记一条mcp:configwarn ——不再静默丢弃。
env 与 headers 的值都支持引用 llms.json,两种语法:
| 语法 | 取什么 |
|---|---|
| $<平台>:apiKey / $<平台>:baseUrl | 平台级 —— 该平台的对话密钥 / baseUrl |
| $<平台>$<模型>$search_mcp_key | 模型级 —— 该模型条目上的 MCP 专用密钥(另有 search_api_key / search_model_key) |
模型级用 $ 而非 : 作分隔,是因为模型 id 里 . / - 很常见(MiniMax-M3、qwen3.8-max),再用 : 会与平台级写法撞上。引用可以内嵌在字符串中间,所以 "Bearer $x:apiKey" 这种写法是合法的。
{
"servers": {
"minimax": {
"command": "uvx",
"args": ["minimax-coding-plan-mcp", "-y"],
"env": {
"MINIMAX_API_KEY": "$minimax$MiniMax-M3$search_mcp_key",
"MINIMAX_API_HOST": "https://api.minimaxi.com"
}
},
"bailian-websearch": {
"type": "sse",
"url": "https://dashscope.aliyuncs.com/api/v1/mcps/WebSearch/sse",
"headers": {
"Authorization": "Bearer $bailian$qwen3.8-max$search_mcp_key"
}
}
}
}- 引用在建连之前解析,所以对所有 MCP 消费方一致生效:
MCPTool、web_search的 mcp 档、/mcp面板 - 平台 / 模型的密钥本身可以是字面量或 env 变量名,引用侧不用关心 —— 走的是同一套凭据解析。平台名与模型名都大小写不敏感
- 平台名未知、模型名未知、字段与该模型的
search档不匹配、或拿不到可用密钥 → 不建连,直接报错点名问题(带着一个字面量"$minimax:apiKey"去连,只会换来上游一句语义不明的鉴权错)。这类失败不落连接失败缓存,改完llms.json下次调用即可重试 - 报错文案里不会出现密钥本身,
/mcp面板也只展示 url / command,不展示headers/env - 要写以
$开头的字面量值,用$$转义("$$FOO"→$FOO)
MCP 工具动态注册:连接成功的 server,其工具会自动注册成 mcp__<server>__<tool> 形态直接进工具池(与 ClaudeCode 命名约定一致),模型可直接调用、权限规则可精确到单个工具。启动时连不上的 server 不注册,其工具仍可经 MCPTool({server, tool, input}) 元工具兜底调用。
| 配置项 | 默认 | 说明 |
|---|---|---|
| mcp.dynamicTools | true | 关掉则不做动态注册,退回纯 MCPTool 元工具 |
| mcp.dynamicToolsMaxTools | 64 | 注册总数上限,溢出的工具仍可经元工具调用 |
工具名里的非法字符会被清洗成 _(server 名 my.browser → mcp__my_browser__*),实际名字可经 /mcp 面板查看。
浏览器自动化 · /chrome
让模型能启动 Chrome、导航、点击、填表、截图、读控制台与网络请求。
实现走 MCP 路线:把 Google 官方的 chrome-devtools-mcp 作为 stdio MCP server 接入,其工具按上节规则动态注册成 mcp__chrome-devtools__<tool>。stem 不自己驱动浏览器 —— 可执行文件发现、profile 管理、attach 与退出清理都由 chrome-devtools-mcp 用 puppeteer-core 完成,因此 stem 侧零新依赖。
快速开始
/chrome setup # 写入 ~/.mstem/mcp.json
/chrome connect # 连接(首次会用 npx 拉包,约 30–60 秒)然后直接说「打开 example.com,告诉我页面标题」即可。
前置条件:Node ≥ 20.19(^20.19 || ^22.12 || >=23)、已安装 Chrome、首次运行需联网。
子命令
| 命令 | 说明 |
|---|---|
| /chrome | 状态卡:配置来源、命令行、连接状态、本机 Chrome、Node 版本。不发起连接 |
| /chrome setup [flags] | 幂等合并写入 mcp.json(保留同级其他 server) |
| /chrome connect | 断开重连并刷新动态工具目录 |
| /chrome tools [过滤词] | 列出工具(人读版) |
| /chrome remove | 从所属配置层删除条目 |
setup 的可选参数:
| 参数 | 作用 |
|---|---|
| --headless | 无头模式。默认不开 —— 参考体验是用户看着 Chrome 自己动 |
| --isolated | 每次用全新临时 profile。默认不开 —— 默认持久 profile 保留登录态,「在我账号里填这个表」才可用 |
| --channel <c> | stable / beta / canary / dev |
| --executable-path <p> | 指定 Chrome 可执行文件 |
| --browser-url <url> | attach 到已在运行的 Chrome(需其带 --remote-debugging-port 启动) |
| --project | 写到 <cwd>/.mstem/mcp.json 而非用户级 |
| --force | 条目已存在且与默认不同时才需要 |
写入的配置
{
"servers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest",
"--viewport", "1280x720",
"--screenshotFormat", "webp",
"--screenshotMaxWidth", "1568"]
}
}
}几个参数是踩过坑才定下来的,手改时别删:
-y—— MCP 的 stdio transport 以shell: falsespawn 子进程。没有-y时 npx 会把 "Ok to proceed?" 提示写进握手用的同一条 stdio,连接挂死直到超时,然后落成 failed- Windows 上是
npx.cmd—— 同样因为shell: false,"npx"在 Windows 会 ENOENT。/chrome setup会按平台自动写对 --screenshotFormat webp+--screenshotMaxWidth 1568—— 截图直接决定 token 成本,这是主要的尺寸控制手段。stem 侧还有一道 5MB 兜底,超限会丢图并提示改用 webp
配置改动无需重启 —— MCP 配置按 mtime/size 指纹热更新。
权限
动态注册后每个浏览器动作都是独立工具名,可精确配规则(写在 .mstem/settings.local.json 或 ~/.mstem.json):
{
"permissions": {
"allow": ["mcp__chrome-devtools__*"],
"ask": [
"mcp__chrome-devtools__evaluate_script",
"mcp__chrome-devtools__upload_file"
]
}
}裁决优先级 deny > ask > allow,所以上面的 ask 条目会压过通配 allow —— 常规操作免弹卡,执行任意 JS 与上传文件仍逐次确认。
工具名尾部 * 是前缀匹配,为动态 MCP 工具新增的语法;带括号的 pattern(如 Bash(npm *))语义不变。注意兜底路径:未注册成功的工具经 MCPTool 调用时,按字面名 MCPTool 鉴权,mcp__* 规则对它不生效。
故障排查
/chrome 与 /chrome connect 会把常见错误翻成一句能照做的提示:
| 现象 | 处理 |
|---|---|
| spawn npx ENOENT | 装 Node.js 并确保在 PATH 上 |
| 首次 connect 超时 | 正在下载包。重试,或先跑 npx -y chrome-devtools-mcp@latest --help 预热 |
| Could not find Chrome | 装 Chrome,或 /chrome setup --executable-path <路径> |
| SingletonLock / profile 被占用 | /chrome setup --isolated |
| ECONNREFUSED(用了 --browser-url) | 目标端口没有 Chrome 在监听,先带 --remote-debugging-port 启动 Chrome |
| 会话中途杀掉 Chrome 后一直报错 | 等 60 秒(连接失败缓存 TTL)或直接 /chrome connect |
| 模型说「我截图了」但描述不出画面 | 图没送到。查日志 mcpTool:result 的 images 字段是否为 0 |
实现要点
改动集中在这几处,排查问题时按图索骥(均在 src/stem-msce/msc-project/mai-cli-server/ 下):
logic/services/mcp-logic/source-stem/clientManager.ts—McpContentBlock类型、tool annotations、失败态 TTL、连接/调用超时、reconnectServer/disconnectServerlogic/tools/mcp-tool-logic/source/mcpToolCore.ts—splitContent()把 MCP image 块抬成ToolInvokeResult.imageBlocks(此前一律降级成字面量[image],截图直接蒸发)logic/tools/_shared/mcpToolCatalog.ts— 动态注册目录:名称清洗、readOnlyHint→isReadOnly、描述截断、数量上限、总开关logic/tools/registry.ts—ToolDescriptor.mcp字段、getTool兜底查目录、buildToolDefs追加logic/session/session-logic/MaiCliServerSessionLogic.ts— 构建 toolDefs 前刷新目录(带 3s 预算,连不上不阻塞会话);派发时把扁平入参包回{server, tool, input}logic/core/permission-logic/source/permissionRules.ts— 工具名尾*前缀匹配logic/core/system-prompt-logic/source/systemPrompt.ts—buildBrowserPromptSection(),仅在工具池真有浏览器工具时注入「先取快照拿 uid、再按 uid 操作」的用法约定
自检脚本 bun scripts/smoke-chrome-mcp.ts(不需要 Chrome、不需要联网)覆盖截图直通、动态注册、连接失败 TTL、配置写入器与权限通配。
阿里百炼联网搜索 · /bailian
把百炼托管的联网搜索 MCP 接进来,作为 search: "mcp" 档的后端。与 /chrome 同构,差别在于它是远程 SSE server 而非本地 stdio。
快速开始
/bailian setup # 写入 mcp.json(条目名 bailian-websearch)
/bailian connect # 连接
/bailian tools # 确认工具已就绪(只列工具,不计搜索次数)再到 llms.json 给模型加上路由:
"qwen3.8-max": {
"search": "mcp",
"search_mcp": "bailian-websearch/bailian_web_search",
"search_mcp_key": "sk-ws-…"
}search_mcp_key 与平台 apiKey 不是同一把 —— 实测平台的 Token Plan key(sk-sp-)打这个端点直接 401 InvalidApiKey,要用开通联网搜索时拿到的那把。setup 默认写平台级引用,发现模型上已有 search_mcp_key 时用 --model <id> 让它改写成模型级引用。
子命令
| 命令 | 说明 |
|---|---|
| /bailian | 状态卡:配置来源、端点、引用能否解析、连接状态、平台密钥、当前模型的搜索是否真的指向本 server。不发起连接 |
| /bailian setup [flags] | 幂等合并写入 mcp.json(保留同级其他 server) |
| /bailian connect | 断开重连并刷新动态工具目录 |
| /bailian tools [过滤词] | 列出工具(人读版) |
| /bailian remove | 从所属配置层删除条目 |
setup 的可选参数:--platform <id>(引用哪个平台,缺省 bailian)、--model <id>(改用该模型的 search_mcp_key)、--project(写工程层)、--force(覆盖已有条目)。
端点用
/WebSearch/mcp(Streamable HTTP,与官方文档一致)。百炼在 2026-08-08 ~ 08-10 间完成了 SSE → Streamable HTTP 的服务端迁移(此前/mcp回 405、只有/sse通;迁移后倒过来,/sse只回空流)。若连接报Invalid content type一类错,先用 curl 对照两个路径确认服务端协议,再改type。套餐专属 workspace 域名(llm-<id>.<region>.maas.aliyuncs.com)下同路径也可用。连上之后
bailian_web_search不会出现在模型的工具池里 —— 被search_mcp指为搜索后端的工具由hideSearchBackends摘掉,搜索统一走web_search门面按当前模型路由。这是预期行为,不是漏注册。计费:前 2000 次调用免费,之后按次计。
/bailian tools只列工具不计费,是验证连通性最便宜的一步。
业务模式 · --agent / --coding / --fde
Stem 的三种核心业务模式:
| 模式 | 启动命令 | 状态 | 说明 | 核心能力 |
|---|---|---|---|---|
| Agent | stem / stem --agent | ✅ 开放(默认) | 面向通用智能体形态 | 业务图谱记忆 |
| Coding | stem --coding | ⬜ 暂未开放 | 面向编码工作流的形态 | 业务图谱记忆 · 基于 MortiseAI Spec Code Engine 编码 |
| FDE | stem --fde | ⬜ 暂未开放 | 面向企业交付场景的形态 | 业务图谱记忆 · 私有图谱记忆 · 基于 MortiseAI Spec Code Engine 编码 |
三选一布尔开关,缺省回退 env STEM_DEF_MODE,默认 agent。显式请求 --coding / --fde 会拒绝启动并提示(不做静默回退)。
会话 · --resume
--resume <id> / -r <id> / --resume=<id>:恢复指定会话(退出提示里有 --resume <id> 可直接复制),复用原会话日志文件续写。
多租户隔离 · --tenant
--tenant <code>(或 --tenant=<code>)以租户模式启动:全部落盘状态按租户
完全隔离,近似独立实例;不带 flag 时行为与以往完全一致。
- flag-only:租户码只认启动参数。shell
export STEM_TENANT/.env/ settingsenv段里的STEM_TENANT一律被入口清除,不作为输入 (STEM_TENANT是内部传播通道,不要手工设置)。自拉起 stem 子进程时须显式 转发--tenant。 - 经 npm/bun 脚本启动(仓库源码开发):flag 透传规则见
MSTEM.md「开发启动」节。 - 租户码:1–64 字符,小写字母/数字开头,允许
._-(拒绝大写与路径 分隔符);非法直接报错退出。
目录布局(H = 租户段 tenant/<code>):
| 数据 | 无租户 | 租户模式 |
|---|---|---|
| Group 数据(session/causal/distill/evolve) | <cwd>/.mstem/platform/<kind>/ | <cwd>/.mstem/H/<kind>/ |
| 用户空间(--user 时的全部数据,见下节) | <cwd>/mstem-storage/platform/<user>/ | <cwd>/mstem-storage/H/<user>/ |
| 家目录状态(projects/plans/keybindings/memory/plugins/skills/mcp/hooks/llms/workflows/agents;输入历史为进程内内存,不落盘) | ~/.mstem/... | ~/.mstem/H/... |
| 用户 settings | ~/.mstem.json | ~/.mstem/H/settings.json |
| 项目配置(settings/skills/plugins/agents/workflows/hooks/mcp/llms/memory) | <cwd>/.mstem/platform/... | <cwd>/.mstem/H/... |
| debug 日志 | <cwd>/logs/ | <cwd>/logs/(统一,不分目录;sessionId 唯一) |
- env 覆盖 × 租户:目录形根只有两个 env(
STEM_STORAGE_DIR重定向存储根、STEM_DIR重定向项目配置根;不再有逐 kind 的独立重定向)。即使显式 设置,租户段照插(如STEM_STORAGE_DIR=/data+--tenant acme→/data/tenant/acme/...)— 隔离是边界,不随根重定向失效。文件形显式指针 env(STEM_SETTINGS_PATH/STEM_CMD_LOG/STEM_LLMS_PATH/STEM_HOOKS_PATH/STEM_MCP_CONFIG_PATH/STEM_PLUGINS_DIR)按字面生效。 - 首启为空 + 自动建空间:租户首次以正常路径启动(TUI / daemon run /
mcp serve;
--help等 fast-echo 不算)时自动创建其目录骨架 (.mstem/tenant/<code>/及其 kind 四目录、~/.mstem/tenant/<code>/projects/),但不播种任何配置 — 新租户的 settings/llms/mcp/skills 全部独立,需逐租户放入(llms.json 凭据不跨租户 共享是特性,不是缺陷;想共享凭据可用字面生效的STEM_LLMS_PATH)。 - 租户会话与个人历史互不可见:
/resume在租户模式下只枚举该租户自己的会话。
用户隔离 · --user
--user <id>(或 --user=<id>)在 platform 或 tenant 作用域基础上再追加一层
用户隔离,--tenant 有没有传都生效。四个作用域(平台 / 平台+用户 /
租户 / 租户+用户)两两绝对隔离:每个用户拥有独立的用户空间
<存储根>/platform/<id>/ 或 <存储根>/tenant/<code>/<id>/,.mstem 配置根
本身不含任何用户段:
- 用户数据落用户空间;不传
--user时数据属 Group 维度,落 STEM_DIR (.mstem/<scope>/<kind>/),mstem-storage只承载用户空间。会话数据的 口径是完整的:会话/洞察四目录、task 快照与 workflow 日志(projects/)、plan 文件(plans/)、洞察报告与洞察记忆 —— 用户会话的全部落盘都在自己空间内,Group 与其他用户不可见。 (debug 日志例外:统一落<cwd>/logs/,按唯一 sessionId 区分文件。) - 五类资源(workflows / skills / plugins / memory / agents)同样绝对隔离:
--user会话只读自己用户空间下的对应子目录(如mstem-storage/platform/<id>/skills/),不读 Group 的.mstem/家目录层; Group 会话也看不到用户空间的资源。 llms.json、settings.json、settings.local.json(以及 mcp/hooks 等配置 文件)仅存在于平台/租户目录 —— 模型与 settings 只能按 Group(平台/租户) 设置,用户维度没有自己的配置副本,--user会话读到的是所属 Group 的配置。- Group 配置对用户会话只读:
--user会话里的一切设置写入(/model、/config、技能/插件开关等)都落进程内的 session 临时层 —— 会话内立即生效、 优先级最高,但不落盘,退出即消失,Group 的配置文件一个字节都不会被改。 - 用户码格式与租户码相同(1–64 字符,小写字母/数字开头,允许
._-); 非法直接报错退出,与--tenant是否合法/是否传入无关。 - 不做 npm 中继(与
--tenant的npm_config_tenant中继不同):npm 脚本下需要--user时用npm run dev -- --user <id>透传,细则见MSTEM.md「开发启动」节。
举例:stem --user 1000 → 用户空间 <cwd>/mstem-storage/platform/1000/
(会话/洞察/skills/agents/workflows/memory/plugins 全部在内),llms/settings
读 <cwd>/.mstem/platform/;stem --tenant acme --user 1000 → 用户空间
<cwd>/mstem-storage/tenant/acme/1000/,llms/settings 读
<cwd>/.mstem/tenant/acme/。
图片粘贴 · 图文混合理解(macOS)
对齐 Claude Code:剪贴板里有图片(如 Cmd+Shift+Ctrl+4 截图)时按 Ctrl+V,输入框插入 [Image #N] 占位;提交后文本与图片按占位位置交错组成混合消息发给模型。模型也可以直接 Read 图片文件(png/jpg/gif/webp)"看到"内容。
- 依赖模型能力位:
llms.json模型条目的vision字段。vision: false的模型粘贴时直接拦截提示;粘贴后切到不支持的模型,发送前自动剥离图片并提示「已省略 N 张图片」。 - 剪贴板读取优先用
pngpaste(brew 可选安装,更快),否则走系统osascript— 首次调用可能弹 macOS 自动化权限框,拒绝后图片粘贴不可用(等同剪贴板无图)。 - 超限图片(>5MB 或长边 >8000px)自动用系统
sips缩放到长边 1568px。 - 已知限制:
--resume恢复的历史会话中图片不回灌,模型只见[Image #N]字面。
其他参数
| 参数 | 说明 |
|---|---|
| --bare | SIMPLE 模式(见「运行形态总览」) |
| --plugin-dir <dir> | 会话级插件根,可重复传入(相对路径按真实调用目录解析) |
| --sdk-url / --sdk-telemetry / --telemetry-url | 无头 SDK 桥 / 观测帧 / TUI tap(见上文两节) |
| --worktree/-w + --tmux | git worktree + tmux 并排会话(需 git 仓库) |
| --update / --upgrade | 自更新 |
| --version/-v · --help/-h | 版本 / 完整参数帮助 |
完整参数清单以 stem --help 输出为准。
运行时开关 · .mstem/settings.local.json 的 env 段
CLI 的全部运行时开关统一放 settings 级联的 env 段(不再使用 .env):
入口启动时把 env 对象逐 key 注入 process.env,仅补缺不覆盖 — shell
显式 export 的同名变量仍最优先(CI/测试钩子用,如 STEM_LLMS_PATH /
STEM_SETTINGS_PATH)。
层级(高层同 key 覆盖低层):
<cwd>/.mstem/settings.local.json— 个人项目级(应 gitignore)<cwd>/.mstem/settings.json— 团队项目级(随仓库提交)~/.mstem.json— 用户全局
示例 .mstem/settings.local.json:
{
"env": {
"STEM_DEF_LANGUAGE": "zh",
"STEM_API_RETRY_INTERVAL_MS": "3000",
"STEM_DEBUG": "1",
"STEM_LOOP_CRON_ENABLED": "1"
}
}常用开关一览(全部可放 env 段,也可 shell export 临时覆盖):
| 变量 | 作用 |
|---|---|
| STEM_DEF_LANGUAGE | 界面语言 zh\|en\|ja\|ko(--lang 的兜底) |
| STEM_DEF_MODE | 业务模式(--agent 等的兜底) |
| STEM_DEF_LLM_MODEL | 模型引用;优先级低于 --model / /model 与 llms.json 的 default —— 只在没配 default 时兜底 |
| STEM_DEBUG | 1 = 渲染 tool-use/tool-result 调试卡片 + Debug banner |
| STEM_API_RETRY_INTERVAL_MS | LLM 网络错误重试间隔(默认 60000,最多 10 次) |
| STEM_API_TIMEOUT_MS | LLM 请求全局超时(平台未配 timeoutMs 时生效,默认 600000) |
| STEM_PERMISSION_MODE | 工具权限模式 default\|allow\|plan\|bypassPermissions\|... |
| STEM_MCP_CONNECT_TIMEOUT_MS | MCP server 连接超时(默认 120000;首次 npx 拉包 + 浏览器冷启动可能很久) |
| STEM_MCP_CALL_TIMEOUT_MS | MCP 单次工具调用超时(默认 120000;性能 trace 一类会很慢) |
| STEM_MCP_FAILED_TTL_MS | MCP 连接失败后多久允许重试(默认 60000) |
| STEM_LOOP_CRON_ENABLED | 1 = 开启 /loop 后台 cron(默认关,opt-in 安全门) |
| STEM_HUD_SHOW_USAGE / STEM_HUD_SHOW_WEEK | HUD 用量行 / 本周行显示开关 |
| STEM_TELEMETRY | 1 = 无头模式开启 telemetry 观测帧(--sdk-telemetry 的 env 形式) |
| STEM_TELEMETRY_WS | telemetry WS tap 地址 |
| STEM_STORAGE_DIR | 用户空间根(缺省 <cwd>/mstem-storage;优先于 settings storage.dir;自动追加作用域+用户段 —— platform/<user>,租户模式下 tenant/<code>/<user>)。只承载 --user 用户空间;Group 数据(session/causal/distill/evolve)落 STEM_DIR 项目配置根,跟随 STEM_DIR 重定向 |
| STEM_DIR | 项目配置根(缺省 <cwd>/.mstem;取值形态与作用域追加规则同 STEM_STORAGE_DIR) |
| STEM_TENANT | 内部通道,勿手工设置 — 只由 --tenant flag 写入,env 段里的值会被入口忽略 |
| STEM_USER | 内部通道,勿手工设置 — 只由 --user flag 写入,env 段里的值会被入口忽略 |
STEM_STORAGE_DIR / STEM_DIR 的取值支持两种形态:绝对路径、相对路径
(相对当前工程路径 cwd 解析)——不做 ~ home 展开,~/... 不会被特殊
处理,直接按相对路径拼接。会话聊天文件默认落
<cwd>/.mstem/platform/session/<sessionId>.jsonl(--user 时落用户空间
<cwd>/mstem-storage/platform/<user>/session/);升级前写在
~/.mstem/projects/<项目>/ 或 mstem-storage/<scope>/common/ 的历史会话
经惰性迁移仍可被 /resume 找到并续写原文件。
配置结构说明
两个配置根均落在目标项目目录下,可用 env STEM_DIR / STEM_STORAGE_DIR 重定向(见「运行时开关」):
.mstem/— 项目配置根:配置文件 + Group 数据(session/causal/distill/evolve 等)mstem-storage/— 用户空间根:只承载--user用户隔离数据,不放配置文件
.mstem/ # 项目配置根(STEM_DIR)
├── platform/ # platform 作用域(./update_pro.sh 或 scripts/update-platform-space.sh 补齐+播种)
│ ├── settings.json # 团队项目级配置(随仓库提交)
│ ├── settings.local.json # 个人项目级配置(应 gitignore;env 段放运行时开关)
│ ├── llms.json # 多平台 LLM 目录(见「模型目录」节)
│ ├── mcp.json # MCP server 配置
│ └── agents/ causal/ distill/ evolve/ memory/ plugins/ session/ skills/ workflows/
└── tenant/<租户代码>/ # tenant 作用域:结构同 platform,首启自动建目录
└── … # 但配置文件不播种,需手工放置
mstem-storage/ # 用户空间根(STEM_STORAGE_DIR)
├── platform/
│ ├── common/ # 未指定 --user 时的公共用户空间
│ └── <用户ID>/ # stem --user <用户ID> 的用户空间
│ └── agents/ causal/ … workflows/(九个子目录同上)
└── tenant/<租户代码>/
├── common/ # 租户内公共用户空间
└── <用户ID>/ # stem --tenant <code> --user <id> 的用户空间- 四个作用域(platform / platform+user / tenant / tenant+user)两两绝对隔离,详见「多租户隔离」「用户隔离」两节
- 会话聊天文件默认落
.mstem/platform/session/<sessionId>.jsonl;--user时落mstem-storage/platform/<用户ID>/session/ - settings 级联(对全部 settings key 生效,不止
env/权限段):.mstem/settings.local.json>.mstem/settings.json>~/.mstem.json(用户全局)——「当前工程.mstem配置 >~/.mstem.json」 - 写入侧防遮蔽:设置默认仍落用户层(
theme/advisor这类跨工程偏好),但工程层已定义该 key 时就地改工程层;取消设置则两侧一起清 - 骨架补齐与配置播种统一走
./update_pro.sh(幂等,绝不覆盖已有文件,见「空间初始化」节)
Windows 使用说明
macOS / Linux 用户可以跳过本节。以下是 Windows(尤其中文系统 + 独立 PowerShell / cmd 窗口,即 conhost 而非 Windows Terminal)上遇到过的问题与开关。
画面串行错乱(欢迎横幅 / 思考中 / 状态栏挤在同一行互相覆盖)
原因是「东亚歧义宽度」字符 —— ● · ▁ │ ─ … → — ↑ ◇ 这些状态栏和横幅的主力符号,
Unicode 规定"上下文不明时按 1 格算"(xterm / iTerm / Windows Terminal 都照办),
但中文 Windows 的 conhost 按控制台字体的全角字形画成 2 格。渲染器是单元格模型,
模型算的宽度和终端实际推进的列数一旦对不上,光标就逐列漂移。
现在会自动判定(裸 conhost + CJK 代码页 → 按 2 格),启动后还会用 DECXCPR 实测一次
校准。判定结果记在会话日志的 [termProfile] / [termProfile:probe] 里。
判错了可以手动压:
| 变量 | 作用 |
| --- | --- |
| STEM_AMBIGUOUS_WIDTH=wide\|narrow | 强制歧义宽度按 2 格 / 1 格算,优先级高于自动判定与探针 |
| STEM_CODE_PAGE=936 | 覆盖代码页探测(否则跑 chcp 取) |
| STEM_COLUMNS / STEM_ROWS | 覆盖终端列/行数,见下 |
画面右侧像被截断 / 窗口底部出现水平滚动条
conhost 的屏幕缓冲区可以比可见窗口宽,而 Node 报告的列数取的是缓冲区宽度
(libuv 用 dwSize.X),于是 UI 会往你看不见的列里画。两种解法:
- PowerShell 窗口 → 右键标题栏 → 属性 → 布局 → 把「屏幕缓冲区大小」的宽度改成与 「窗口大小」宽度一致(推荐,一劳永逸);
- 或用
STEM_COLUMNS=<实际可见列数>压住。
刻意不认
COLUMNS/LINES:Git Bash / MSYS 导出的是启动时的静态值,窗口 放大后不更新,拿它当真相源本身就是个 bug。
中文变成 ���
工具输出(PowerShell / Bash)和 MCP server 的 stderr 现在都按正确编码处理:能控制的
子进程强制它输出 UTF-8(PowerShell 注入 [Console]::OutputEncoding,cmd 前缀
chcp 65001),控制不了的(MCP server)按 OEM 代码页解码。
如果仍有乱码,把 [termProfile] 那行里的 codePage / oemEncoding 一并反馈。
MCP server 起不来
Windows 上命令解析失败时不会报 ENOENT —— cross-spawn 会把它交给 cmd.exe,
于是你只会看到一句本地化的 "不是内部或外部命令"(还是 OEM 编码的)。现在会在
spawn 前先按 PATHEXT 沿 PATH 解析一遍,解析不到直接给 command not found on
PATH: "uvx"。常见修法:装上对应工具,或把 command 写成 npx.cmd 这样带后缀的形式。
报障时请带上
logs/<sessionId>/*.txt(含[termProfile],一眼能看出终端画像)- 复现时加
STEM_DEBUG_REPAINTS=1,日志里会多出全屏重画的归因 - 画面问题可加
STEM_TTY_RECORD=<路径>,把原始终端字节流录下来一并发回 —— 我们能在 本地虚拟终端里按不同口径回放,把"猜"变成"量"
技术栈
| 维度 | 选择 |
|---|---|
| 语言 | TypeScript 5(严格模式 + 装饰器) |
| 开发运行时 | Bun |
| 发行格式 | ESM 单文件 bundle,Node >= 22 |
| UI | Ink 6 + React 19 |
| 架构引擎 | @mortiseai/mai_msc_engine_ts_module@^0.0.3 |
| 许可证 | Apache-2.0 |
