sym-service — 多 runtime 符号计算网关
一个 worker 干三件事:出静态文档站、给两个不同 runtime / 架构的符号计算后端做统一入口,
并把这套能力按 MCP 暴露给模型(POST /mcp,两个工具)。
┌─ SYMBOLICA_URL → Unikraft Cloud(Rust + Symbolica,x86_64)
客户端 → WinterJS (WASIX) ──────┤
dist/worker.js └─ SYMPY_URL ────→ Wasmer Edge(CPython + SymPy,wasm32-wasip1)
├─ 静态页(Astro 构建产物,内联成资源表)
├─ POST /v1/evaluate · GET /v1/capabilities · GET /healthz · GET /
└─ POST /mcp ← 同一个网关的 MCP 面(2 个工具,无状态)
网关不发明协议。 两个上游早已是同一套形状(单端点 + op 闭集 + 五段信封 +
result.values[] + verification 三态 + 12 个共用错误码),本项目只补它们做不了的四件事:
能力并集、按 op 路由、在信封里补一格 provenance(谁算的、跑在哪个 runtime / 架构)、
把两端的 latex 印刷统一到 sympy 的打印机(\arctan{\left(x \right)}、运算符两侧空格、无裸 atan)。
契约按「输入 WL,输出看 latex」用:wl 是引擎自己的书写,跨引擎不可比。
契约的形状只写在 src/schema/gateway.schema.yml 一份文件里。
src/schema/contract.js 是它的派生视图(改 op / 限额 / 码表 = 改 YAML,不改代码),
构建时校验后内嵌进 worker(WASIX 上没有 fs)。
调用方把 SYMPY_URL 换成网关地址,程序一个字不用改。理由、差异清单、判定顺序见
docs/gateway-schema.md。
同一份代码在 Bun 里调试,打包成单文件 worker 后交给 WinterJS。依赖用 Bun 管,
.cnb.yml 在进开发环境前把工具链和依赖装好。
快速开始
bun install
bun run dev # http://127.0.0.1:8787,改 src/ 自动重打并重启(改 src/site 会先 astro build)
bun test # 332 条 / 23 个文件;其中 78 条联网(sympy 56 + symbolica 22),未配上游地址时跳过
# 静态页那 12 条依赖 dist/site:构建过才真跑,否则 skip(顺序见 docs/test-coverage.md §5)
bun run build # rm -rf dist/site → astro build → 打包成 dist/worker.js(约 2.0 MB,含内联静态站)
bun run wasmer # build 后用真 runtime 跑:wasmer/winterjs@1.2.0,端口 8080
# 网关的三条接口(未配上游时:能力表为空、evaluate 一律 422 op_unroutable)
curl -s localhost:8787/v1/capabilities
curl -s localhost:8787/healthz
curl -s -XPOST localhost:8787/v1/evaluate \
-H 'content-type: application/json' \
-d '{"op":"integrate","expr":"x^2"}'
# MCP 面:一条命令一次调用(header 已带齐,回显请求 + 响应 + 结论,退出码可当门禁)
bun run mcp # 默认 tools/list:不需要上游就能过
bun run mcp simplify '(x^2-1)/(x-1)' # 真实求解:op + expr [args...]
bun run mcp diff 'x^2' 2 # 带算子参数
bun run mcp --raw '{"jsonrpc":"2.0","id":7,"method":"ping"}'
bun run mcp --url https://<域名> simplify 'x+x' --wait
# 手写 curl:两条 header 都不能省(少一半是 406,看起来像服务坏了)
MCP=http://localhost:8787/mcp
CT='content-type: application/json'
AC='accept: application/json, text/event-stream'
curl -s -H "$CT" -H "$AC" -XPOST $MCP -d '{"jsonrpc":"2.0","id":7,"method":"ping"}' # 最小成功
curl -s -H "$CT" -H "$AC" -XPOST $MCP -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"sym_evaluate","arguments":{"op":"simplify","expr":"(x^2-1)/(x-1)"}}}'
# 全部命令(含五种常见错的对照)见 docs/mcp-curl.md
# 线上冒烟(部署后验证;--wait 等冷实例就绪)
bun run smoke:http https://<域名> # 只测 API:/healthz、/v1/*、错误路径 + 双上游真实求解(并发 2)
bun run smoke:mcp https://<域名> # 只测 MCP 面:握手、工具清单、两个工具 + 双上游真实求解
# 三者分工:bun run mcp 回话(单发调试)· smoke:http / smoke:mcp 判生死(断言、非零退出)
cp .env.example .env # 填 SITE_URL(构建期出 sitemap 用)
bun run deploy # 读 .env → build → wasmer deploy(需 WASMER_TOKEN)
变量
三地址即可跑通:SITE_URL(本站域名,构建期)+ SYMBOLICA_URL / SYMPY_URL(两个上游,
期)。上游地址只从环境读并在构建期注入产物——域名可能带 token,不进仓库;缺哪个,哪个后端的 op 就回
422 op_unroutable(合法状态,不是错误配置)。
cp .env.example .env # 模板里的注释就是说明书
规矩、优先级、排错见 docs/env.md。可选开关只有 WASMER_APP_DOMAIN(SITE_URL
兜底)、BASE_PATH(子路径)、SYM_VERSION(调版本回显)、WASMER_TOKEN(部署凭证,云原生
开发已注入)。BASE_PATH 只管静态资源前缀;API 路径不变,/sym/healthz 仍需部署层改写。
目录
分层理由、依赖方向与冻结契约见 docs/architecture.md;这里只列位置。
src/ 浏览器 bundle(Bun.build 入口 src/worker.js)
worker.js WinterCG 入口,注册 fetch 事件
transport/ 装配层
app.js Hono 装配:MCP → API(闭集)→ 静态表
mcp.js MCP 面:两个工具,内部打同一个 gateway(判定/错误码不分叉)
core/ 网关与公共件
gateway.js 网关四条路由 + 判定顺序 + 上游故障四态
envelope.js 统一错误信封(两端的逐字拷贝)+ request_id
static.js 静态资源表:查表 + 本地读盘兜底
version.js 包版本(构建期 define,跑源码时读 package.json)
winterjs-compat.js WinterJS 的 Response 补丁 + 请求体预取(关键,见 docs/mcp.md §5)
schema/ 契约层(YAML 是唯一真源)
gateway.schema.yml 协议真源(YAML):版本 / 字段 / 限额 / 码表 / 判定顺序 / 上游表
load.js 读 YAML + 校验 + 内置子集解析器(产物走 __SYM_SCHEMA__ 内嵌)
contract.js schema 的派生视图:常量与查表,一条协议事实都不自己写
request.js 请求闭集解析(未知键 / 缺字段 / 值域,各自给 detail)
upstream/ 上游层
client.js 转发:35 s 预算、连不上 vs 答了、透传不改写
capabilities.js 能力并集:可达性与能力分开报,冲突留痕
latex.js latex 规范化
site/ Astro 站(srcDir,见 astro.config.mjs)
content/docs/ Starlight 页面(Markdown/MDX,构建期出 HTML)
content.config.ts docs 集合(docsLoader + docsSchema)
scripts/ 工具脚本;所有路径的 ROOT 锚点统一在 scripts/paths.mjs
paths.mjs 唯一 ROOT 锚点(dist/site、.env、dist/worker.js 全从这里取)
build/ 构建
build.mjs loadEnv → 校验 schema → cleanSite → astro build → 收集产物 → Bun.build 单文件 worker
env.mjs 读 .env(空值视为未设置;给 astro 子进程传 --env-file)
astro.mjs cleanSite(rm -rf dist/site)+ 产物收集(base64 内联)
dev/ 本地
dev.mjs fs.watch + 重启,site 变动先重建静态页
serve.mjs Bun.serve 模拟 worker 环境
deploy/deploy.mjs 读 .env → build → wasmer deploy,域名只需配一次
verify/ runtime 冒烟与单发调试
mcp-call.mjs `bun run mcp`:单发一条 JSON-RPC,回显请求/响应 + 一句结论(排错用,不做断言)
mcp-smoke.mjs `bun run smoke:mcp`:MCP 面断言
http-smoke.mjs `bun run smoke:http`:HTTP 面断言
toolchain/ 云原生开发环境的安装脚本
install-bun.sh 装 bun(钉 1.4.4,走 GitHub release + sha256)
install-wasmer.sh 装 wasmer cli(钉 7.4.0,原因见 docs/toolchain.md)
test/ bun test;按职责分 unit/ gateway/ mcp/ live/,helper 在 support/
wasmer.toml Wasmer 包清单
app.yaml Wasmer Edge 应用清单
.cnb.yml 云原生开发环境
云原生开发
.cnb.yml 只有一条 vscode 流水线:分支页点「云原生开发」时启动,stages 在进 IDE 之前跑完。
| 项 | 值 |
|---|---|
| 触发 | vscode(页面「云原生开发」按钮) |
| 环境 | 默认开发环境 cnbcool/default-dev-env(不声明 docker 即默认镜像) |
| 服务 | vscode(单容器,WebIDE + VSCode Remote-SSH) |
| 资源 | runner.cpus: 2(2 核 / 4 GiB) |
| 密钥 | imports 引用 Tomori0Nao/TOKENS 的 wasmer/env.dev.yml(注入 WASMER_TOKEN) |
stages[0] toolchain jobs 对象 → 两路并行
├─ install bun scripts/toolchain/install-bun.sh
└─ install wasmer WASMER_VERSION=7.4.0 scripts/toolchain/install-wasmer.sh
stages[1] deps bun install --frozen-lockfile
stages[2] verify wasmer --version + bun test + bun run build(allowFailure)
三个语法点,写错就静默失效:
jobs数组 = 串行,jobs对象 = 并行;stages之间永远串行。bun install不能和「装 bun」并行——它用的就是刚装到/usr/local/bin的 bun,不是镜像自带的。- 每个 stage 是新 shell,
/etc/profile.d不保证被读到,所以deps/verify里显式补PATH。
imports 读的是文件:必须写到 wasmer/env.dev.yml 这一级且用 /-/blob/。/-/tree/ 是目录页、
/-/raw/ 会被判 Invalid file resource。密钥仓库默认只允许负责人/管理员触发的流水线引用,
放开要写 allow_slugs + allow_events(含 vscode,只写 push 读不到)。
版本
| 组件 | 版本 | 说明 |
|---|---|---|
| bun | 1.4.4 | 见 docs/toolchain.md |
| hono | 4.13.14 | HTTP 路由与响应 |
| @modelcontextprotocol/sdk | 1.32.2 | MCP 面(Web Standards transport)。产物 +388 KB,值不值见 docs/mcp.md §3 |
| zod | 4.6.7 | SDK 的工具入参 schema(peer 依赖,不是我们额外选的) |
| astro | 7.4.4 | 只做构建期静态产出,不进 worker |
| @astrojs/starlight | 0.42.8 | 站点主题,与 rs-site-tools 同款;0.41+ 要 Astro 7 |
| @astrojs/sitemap | 3.7.4 | 配了 SITE_URL 才启用;跟 astro 7 同代(都用 zod/v4) |
| wasmer cli | 7.4.0 | 7.4.1+ 要 glibc 2.38,默认镜像只有 2.36 |
| wasmer/winterjs | 1.2.0 | 模块名是 winterjs,不是子命令 serve |
接口
| 方法 + 路径 | 做什么 |
|---|---|
POST /v1/evaluate | 求解。op 决定去哪:integrate → symbolica,其余 25 个 → sympy |
GET /v1/capabilities | 两边 ops 的并集(现拉现并),每个 op 标出 upstream |
GET /healthz | 网关自身 + 每个上游的 reachable / ready / engine / version;?deep=1 才真打上游 |
GET / | 服务信息;静态站建过就出首页(平台的就绪探测打这里) |
POST /mcp | MCP(JSON-RPC)面:sym_evaluate / sym_capabilities 两个工具,内部打上面两条路由。无状态、一次一个 JSON 响应 |
/mcp 只是这两条 API 的壳:工具内部拼 Request 打同一个 gateway(),判定顺序、限额、码表、latex 规范化不分叉。要 content-type: application/json + accept: application/json, text/event-stream 两条 header,initialize 的 params 三件不能省(少一个回 -32603 而不是 serverInfo)——细节见 docs/mcp.md;服务端还在 initialize 回一段 instructions(给模型的用法说明,见 §9),两个工具都声明 outputSchema、成功调用带 structuredContent。
错误统一 {request_id, error:{code, message, detail}}:12 个共用码 + 上游的
503 engine_unavailable / 504 timeout(原样透传)+ 网关自产的 422 op_unroutable。
文档
| 文件 | 内容 |
|---|---|
| docs/architecture.md | 先读这篇:分层与依赖方向、目录约定、scripts/paths.mjs 的 ROOT 锚点、冻结契约、命令地图、本轮整理的验证与回滚 |
| docs/gateway-schema.md | 两端协议对账、YAML 载体与派生模型、网关加的四样(含 latex 规范化)与刻意不加的四样、判定顺序、故障四态、改协议的正确姿势 |
| docs/astro-integration.md | 方案对比、Starlight 主题、构建两段式、三处硬细节、路由 |
| docs/cnb-pipeline.md | .cnb.yml 逐段解释、并行/顺序的判据、密钥接入 |
| docs/mcp-curl.md | MCP 面 curl 速查:先定 MCP / CT / AC 三个变量,之后每条直接粘;ping / initialize / tools/list / 求解 / 能力表各一条,五种常见错(406 / 415 / 400 -32700 / 200 包 -32603 / 405)各一条,输出都是实跑原文 |
| docs/edge-latency.md | 线上「有时候没响应」的判定法:connect / ttfb / x-wasmer-request-id 三条线索、实测抖动的分布、以及应用侧为此做的两件事(门禁前置、探针 3 s) |
| docs/mcp.md | MCP 面:为什么用官方 SDK、无状态/JSON 响应的理由、两个工具、错误翻译、/mcp 与 /v1 的边界、给模型的 instructions(§9)、两轮复查(§10–§11)、代价与没做的事 |
| docs/archive/mcp-sdk-evaluation.md | MCP SDK 封装的一次性评估:三个设计决定、备选方案与否因、仍然成立的缺口(时点数据以 mcp.md 为准) |
| docs/winterjs.md | c.json() 500 与请求体读一次就废两条根因与补丁、HEAD 不可用、打包 target |
| docs/toolchain.md | bun / wasmer 的版本钉法与 glibc 分界、脚本做了什么 |
| docs/deploy.md | wasmer.toml 三处不能省、本地跑真 runtime、部署流程 |
| docs/env.md | 三个地址(本站 + 两上游)、.env 的优先级与空值语义、谁在读它 |
| docs/verification.md | 一次部署 + 经网关验证的记录、修掉的五个运行时差异、冷启动与已知限制 |
| docs/test-coverage.md | 332 条用例的覆盖矩阵、公式语料格式(每 op 简单+复杂各一条)、归一化比对规则、剩余缺口 |
| docs/site-audit.md | 站点内容核对记录:4 处与线上不符、示例页形态、HEAD 500、构建告警 |
| docs/archive/ | 已归档的时点记录(上游 bug 排查、MCP SDK 评估);结论以对应 active 文档为准 |
本站(bun run wasmer 后访问 /)只写给调用方看的东西,不写实现内幕:
| 路径 | 内容 |
|---|---|
/ | 一句话说清是什么 + 三条必知规矩 |
/guide/start | 一次 curl、换语言怎么调、HTTP 状态分三类 |
/api | 端点、五个请求键、五段信封、26 个 op 两组 |
/api/response | 逐字段,以及「200 但没有答案」的三种情况 |
/api/errors | 12 共用码 + 3 特殊情况码,detail 结构,上游故障四态 |
/api/ops | 26 个算子按组列全,args 含义 |
/examples | 每个算子 simple + complex 各一条可 curl 的请求与期望值 |
docs/ 是仓库维护视角(yaml 逐段、版本钉法、排错记录、测试口径),不上站点。
两侧讲同一件事时以 docs/ 为准,站点在它上面精简成调用方需要的最小面。
用 VSCode 连接
- 本地装 VSCode + Remote-SSH 插件
- 分支页点「云原生开发」启动环境
- 点页面上的 VSCode 入口按提示连接
窗口被覆盖时把 Window › New Window › Open Folders in New Window 设成 on。
想换镜像才需要写 docker.image;写了未装 code-server 的镜像会退化成双容器模式。
开发环境默认 10 分钟没心跳回收,可给 services 的 vscode 加 options.keepAliveTimeout。