tomori0nao/sym-service
Public
wasmer run tomori0nao/sym-service

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 读不到)。

版本

组件版本说明
bun1.4.4见 docs/toolchain.md
hono4.13.14HTTP 路由与响应
@modelcontextprotocol/sdk1.32.2MCP 面(Web Standards transport)。产物 +388 KB,值不值见 docs/mcp.md §3
zod4.6.7SDK 的工具入参 schema(peer 依赖,不是我们额外选的)
astro7.4.4只做构建期静态产出,不进 worker
@astrojs/starlight0.42.8站点主题,与 rs-site-tools 同款;0.41+ 要 Astro 7
@astrojs/sitemap3.7.4配了 SITE_URL 才启用;跟 astro 7 同代(都用 zod/v4)
wasmer cli7.4.07.4.1+ 要 glibc 2.38,默认镜像只有 2.36
wasmer/winterjs1.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 /mcpMCP(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.mdMCP 面 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.mdMCP 面:为什么用官方 SDK、无状态/JSON 响应的理由、两个工具、错误翻译、/mcp 与 /v1 的边界、给模型的 instructions(§9)、两轮复查(§10–§11)、代价与没做的事
docs/archive/mcp-sdk-evaluation.mdMCP SDK 封装的一次性评估:三个设计决定、备选方案与否因、仍然成立的缺口(时点数据以 mcp.md 为准)
docs/winterjs.mdc.json() 500 与请求体读一次就废两条根因与补丁、HEAD 不可用、打包 target
docs/toolchain.mdbun / wasmer 的版本钉法与 glibc 分界、脚本做了什么
docs/deploy.mdwasmer.toml 三处不能省、本地跑真 runtime、部署流程
docs/env.md三个地址(本站 + 两上游)、.env 的优先级与空值语义、谁在读它
docs/verification.md一次部署 + 经网关验证的记录、修掉的五个运行时差异、冷启动与已知限制
docs/test-coverage.md332 条用例的覆盖矩阵、公式语料格式(每 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/errors12 共用码 + 3 特殊情况码,detail 结构,上游故障四态
/api/ops26 个算子按组列全,args 含义
/examples每个算子 simple + complex 各一条可 curl 的请求与期望值

docs/ 是仓库维护视角(yaml 逐段、版本钉法、排错记录、测试口径),不上站点。 两侧讲同一件事时以 docs/ 为准,站点在它上面精简成调用方需要的最小面。

用 VSCode 连接

  1. 本地装 VSCode + Remote-SSH 插件
  2. 分支页点「云原生开发」启动环境
  3. 点页面上的 VSCode 入口按提示连接

窗口被覆盖时把 Window › New Window › Open Folders in New Window 设成 on。 想换镜像才需要写 docker.image;写了未装 code-server 的镜像会退化成双容器模式。 开发环境默认 10 分钟没心跳回收,可给 services 的 vscode 加 options.keepAliveTimeout。

文档:云原生开发 · 默认开发环境 · 流水线语法 · 文件引用与鉴权

Hono on Wasmer Edge (WinterJS / WASIX), built by Bun