语义网络与 SNL 语言

语义网络把「软件之间要交换什么、怎么理解彼此」表达成一张可运行的关系图。本页是站内完整文档: 核心概念 → 三步上手 → SNL 语法 → 实现与执行 → 打包化 → 对外服务 → 常见报错。

核心概念

事物 object被建模的实体或值:如 手机号、钉钉用户、通用用户。
关系 relation带方向的边,值沿边流动:手机号 → 钉钉用户。
值沿边流动的数据,如 13800000001。
网络一个系统/平台对自己的完整模型描述。
转换方法把两个网络的语义打通(字段等同 / 事物等同)。
实现给网络的边挂上真实数据(映射表 / 请求 / 能力)。

最小示例

# 一段 SNL:声明包、事物、关系,并把关系落到具体事物上
package demo.dingtalk
object 钉钉用户 userid 手机号
relation 钉钉定位(A, B)
    A->B=1
钉钉定位(userid, 钉钉用户)
钉钉定位(手机号, 钉钉用户)

不同人开发的软件各自描述自己的 网络,再用 转换方法 打通,网络之间凭关系自动路由——不再逐家对接 API。

三步上手

01

注册网络

一段 SNL 描述你的模型,到工作台「注册新网络」,校验通过即入库。

02

注册转换方法

一段整包转换 SNL(字段等同 / 事物等同),说明你的网络与别的网络如何打通。

03

登记实现

录入整段实现语句(如 @buildMap),用「就绪检查」看缺实现边,用「执行区域」跑脚本验证。

SNL 语法速查

package

声明包。package com.example.erp

import

导入其它包,可起别名。import com.example.common as c,之后用 c:用户 限定寻址。

object

声明事物。object 省份 城市

relation

声明关系与数量约束。relation 包含(A, B)
A->B=[0,)
(一省多市)。

实例化

把关系落到具体事物。包含(省份, 城市)、钉钉定位(userid, 钉钉用户)。

包限定名

同名事物跨包分立,歧义时用 包名:事物 钉定。demo.bridge:客户。

@buildMap

映射表实现(给边挂数据)。@buildMap((源, 目标), ((源值, 目标值), …))。

@buildChainMap

具体路径映射:把一条边实现为「沿指定链行走」。@buildChainMap((国家, 城市), with(国家).get(省份).get(城市))。

@buildConstMap

常量实现。@buildConstMap((状态, 默认状态), "激活")。

@buildEqualMap

等同实现(目标取来源之一的值)。@buildEqualMap((A, B, A), A)。

script curl(源, 目标)

网络请求实现(内置求解器,引 cURL/jq 语法)。正文一行管道命令,${源概念} 执行时替换为源值。JSON 提取 | jq -r '.a.b[0]'。

script capability(源, 目标)

能力引用:把一条边实现为一次能力调用,正文恰一行「能力名 + 可选版本」。未注册 = 挂起(就绪检查列缺实现)。

@take

查询。@take(省份: "浙江", 城市)。

@hideObject

隐藏中间事物。@hideObject(中间概念)。

@multiObj / @zoomIn / @zoomOut

多源注入与视图缩放(进阶)。

// 与 #

注释。

实现与执行脚本

实现不是逐关系录入,而是给网络的一个实现录入整段 SNL 实现语句:

package test

@buildMap((aa, bb), ((u1, 张甲)))

执行区域在「网络 + 该实现」装配出的图上跑取数脚本(一行一条,ITakeCommand 直译,行末打印):

from(aa, u1).get(bb)                  # 单跳:aa 为 u1,取 bb -> 张甲
from(a, a1).get(m).get(c)             # 连跳:值逐跳流转
from(a, a1).get(m).with(b, b2).get(c) # 中途补源:该跳源集 = 上一结果 + (b, b2)

打包化:网络 = 清单 + 包集合

当一个平台有几百上千个接口(如飞书 1513 个接口),就把它组织成一个网络而不是一堆网络:

包 packageSNL 的最小文件单元,通常「一包一接口」。
模块 module包名第三段:com.feishu.contact_v3.user.create → contact_v3。
公共包 common以 .common 结尾,存放共享事物,装配时随模块一起加载。
清单 manifest网络的 JSON 定义:成员包规则 / 模块 / 入口 / capabilities。
内容寻址包文本按 sha256 去重,只存一份。
版本快照每个版本只存「包名 → sha256」引用,可完整还原任一版本。

按模块切片加载

大网络整段装配很慢,编辑页「结构图」与脚本执行只装配「公共包 + 当前模块」: 在 network-edit.html 三页签(包编辑 / 清单 / 结构图)里选模块即可;工具栏支持新增模块、删除模块、新增包、删除包。

能力脚本:四种解释器与统一框架

SNL 里一条边可以声明为能力引用(script capability(源, 目标) begin 能力名 v版本 end), 执行时把「当前关系 + 来源值」交给能力脚本,脚本算出目标值返回。

四种解释器

JavaScript(js)Nashorn,沙箱(封 Java 访问);纯计算。
WebAssembly(wasm)Chicory,沙箱(只读 env.hostInfo 面);纯计算 / 宿主画像。
MicroPython(mpy)Chicory + micropython-wasi,沙箱;纯计算。
GraalPy(graalpy)真 Python 进 JVM,非沙箱(可读宿主、import java);读宿主 / 复杂逻辑。

统一脚本框架(所有解释器同契约)

能力脚本执行时能拿到「当前关系 / 来源值」,并设置目标值:

fromNames当前关系的源概念名数组(如 ["手机号"])。
toName当前关系的目标概念名(如 "钉钉用户")。
sources源概念名 → 值(字符串)。
values按源声明顺序的值列表(与 fromNames 同序)。

设置目标值 = 脚本返回列表(JS 的 invoke() 返回值;独立脚本 print 结果 JSON)。

各解释器完整样例(获取关系 / 来源值 / 返回目标值)

统一口径:获取关系=fromNames(源概念名)/ toName(目标概念名); 来源值=sources(按名)/ values(按位置,与 fromNames 同序); 返回目标值=列表(单值包一层 [x])。

① JavaScript(Nashorn)

// 全局已注入:fromNames, toName, sources, values;invoke() 的返回值 = 目标值
function invoke() {
    var from = fromNames;            // 获取关系:源概念名数组,如 ["手机号"]
    var to   = toName;               // 获取关系:目标概念名,如 "钉钉用户"
    var v0   = values[0];            // 获取来源值:按位置(与 fromNames 同序)
    var byName = sources["手机号"];  // 获取来源值:按源概念名
    return [v0 + " -> " + to];       // 返回目标值:数组
}

② MicroPython / ③ GraalPy(独立脚本,stdin → stdout)

# 读 stdin 请求 {"fromNames","toName","sources","values"},写 stdout 结果
import sys, json
req = json.loads(sys.stdin.read())
from_names = req["fromNames"]       # 获取关系:源概念名
to_name    = req["toName"]          # 获取关系:目标概念名
v0         = req["values"][0]       # 获取来源值:按位置
by_name    = req["sources"].get(from_names[0], "")
print(json.dumps({"ok": True, "values": [v0 + " -> " + to_name]}))   # 返回目标值

MicroPython 与 GraalPy 脚本形态完全一致;区别只在解释器能力—— MicroPython 是沙箱(子集标准库),GraalPy 可 os/platform/socket 读宿主、import java 互操作。

④ WebAssembly(Chicory,内存 JSON)

WASM 能力不是「源码脚本」而是模块:导出 alloc(len) / invoke(ptr,len) / result_len(), 请求 JSON(含 fromNames/toName/sources/values)已写入模块内存,模块解析后把结果 JSON 写回内存。 宿主信息面:显式 import env.hostInfo/env.hostInfoLen 才注入(只读)。WAT 模板见 data/capabilities/本机信息.*/module.wat(本机信息已改 GraalPy,模板仍在引擎 HostInfoWasmModule)。

服务:网络 + 实现 对外调用

服务是把「网络(契约)+ 实现(应答)」组合发布成的一个可开关的对外调用单元—— 外部系统不接触 SNL,只按服务名查值。工作台「我的服务」可发布、停用/启用。

外部调用协议(POST,无需登录)

# 结构化查询:from(手机号, 13800000001).get(钉钉用户) 的等价外部调用
POST /api/registry/services/通讯录服务/query
{"object":"手机号","value":"13800000001","get":["钉钉用户"]}
# -> {"success":true,"data":{"service":"通讯录服务","network":"钉钉网络","impl":"default","values":["…"]}}

# 原样脚本(进阶:连跳 / with 补源 / path 诊断)
POST /api/registry/services/通讯录服务/query
{"script":"from(手机号, 13800000001).get(钉钉用户).get(昵称)"}

实现文本由网络实现提供(@buildMap 表或 script curl 指向你自己的 HTTP 端点)——服务查询实际执行的就是「该网络 + 该实现」装配图上的链式取数。

常见报错与参考

SNL 校验失败时错误信息会附带正确参考示例:

# 报错:buildConstMap 参数不足。参考示例:@buildConstMap((源对象, 目标对象), 常量值)
@buildConstMap((aa, bb), "激活")    # 正确:源、目标两个对象 + 一个常量值

# 报错:buildMap 来源/目标参数不足。参考示例:@buildMap((源, 目标), ((源值, 目标值)))
@buildMap((aa, bb), ((u1, 张甲)))   # 正确:多源时平铺为 (源1, 源2, ..., 目标)

完整的 SNL 语言规范见仓库《语义网络语言规范_SNL.md》。